feat: keep clock ticking on server drops

This commit is contained in:
Carlos Valente
2026-03-28 08:01:34 +01:00
committed by Carlos Valente
parent 9e771a2f24
commit 37dc9a620d
8 changed files with 61 additions and 18 deletions
@@ -0,0 +1,33 @@
import { useEffect, useRef, useState } from 'react';
import { useRuntimeStore } from '../stores/runtime';
const takeOverTimeMs = 1500;
/**
* Keeps the clock value ticking if the server drops updates
*/
export function useAutoTickingClock(): number {
const clock = useRuntimeStore((state) => state.clock);
const baselineRef = useRef({ clock, time: performance.now() });
const [, setTick] = useState(0);
// Update baseline when server clock changes
useEffect(() => {
baselineRef.current = { clock, time: performance.now() };
}, [clock]);
// Force periodic re-renders
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 1000);
return () => clearInterval(id);
}, []);
// Always derive from baseline + elapsed at render time
const elapsed = performance.now() - baselineRef.current.time;
if (elapsed < takeOverTimeMs) {
return clock;
}
return baselineRef.current.clock + Math.round(elapsed);
}
@@ -143,8 +143,6 @@ export const useTimer = createSelector((state: RuntimeStore) => ({
...state.timer,
}));
export const useClock = createSelector((state: RuntimeStore) => state.clock);
export const useNextFlag = createSelector((state: RuntimeStore) => ({
id: state.eventFlag?.id ?? null,
expectedStart: state.offset.expectedFlagStart,
@@ -13,8 +13,8 @@ import {
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { useEntry } from '../../../common/hooks-query/useRundown';
import { useAutoTickingClock } from '../../../common/hooks/useAutoTickingClock';
import {
useClock,
useCurrentGroupId,
useFlagTimerOverView,
useGroupTimerOverView,
@@ -342,7 +342,7 @@ export function OffsetOverview() {
}
export function ClockOverview({ shouldFormat, className }: OverviewTimeElementsProps & { className?: string }) {
const clock = useClock();
const clock = useAutoTickingClock();
const formattedClock = shouldFormat ? formatTime(clock) : millisToString(clock);
return (
@@ -10,7 +10,8 @@ import EmptyPage from '../../common/components/state/EmptyPage';
import TitleCard from '../../common/components/title-card/TitleCard';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useBackstageSocket, useClock } from '../../common/hooks/useSocket';
import { useAutoTickingClock } from '../../common/hooks/useAutoTickingClock';
import { useBackstageSocket } from '../../common/hooks/useSocket';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils';
import { formatTime, getDefaultFormat } from '../../common/utils/time';
@@ -214,7 +215,7 @@ function ExtraInfo({ projectData, size, source }: ExtraInfoProps) {
function BackstageClock() {
const { getLocalizedString } = useTranslation();
const clock = useClock();
const clock = useAutoTickingClock();
// gather timer data
const formattedClock = formatTime(clock);
@@ -7,7 +7,7 @@ import Empty from '../../common/components/state/Empty';
import EmptyPage from '../../common/components/state/EmptyPage';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useClock } from '../../common/hooks/useSocket';
import { useAutoTickingClock } from '../../common/hooks/useAutoTickingClock';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
import { formatTime, getDefaultFormat } from '../../common/utils/time';
@@ -140,7 +140,7 @@ function CountdownContents({ playableEvents, subscriptions, goToEditMode }: Coun
function CountdownClock() {
const { getLocalizedString } = useTranslation();
const clock = useClock();
const clock = useAutoTickingClock();
// gather timer data
const formattedClock = formatTime(clock);
+3 -1
View File
@@ -1,5 +1,6 @@
import { Playback } from 'ontime-types';
import { useAutoTickingClock } from '../../common/hooks/useAutoTickingClock';
import { useIsSmallScreen } from '../../common/hooks/useIsSmallScreen';
import { useStudioClockSocket } from '../../common/hooks/useSocket';
import { cx } from '../../common/utils/styleUtils';
@@ -18,7 +19,8 @@ interface StudioClockProps {
export default function StudioClock({ hideCards }: StudioClockProps) {
const isSmallScreen = useIsSmallScreen();
const { clock, playback } = useStudioClockSocket();
const clock = useAutoTickingClock();
const { playback } = useStudioClockSocket();
const onAir = playback !== Playback.Stop;
// if we are on mobile and have to show the cards
@@ -4,7 +4,8 @@ import { useMemo } from 'react';
import EmptyPage from '../../common/components/state/EmptyPage';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useClock, useSelectedEventId } from '../../common/hooks/useSocket';
import { useAutoTickingClock } from '../../common/hooks/useAutoTickingClock';
import { useSelectedEventId } from '../../common/hooks/useSocket';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { formatTime, getDefaultFormat } from '../../common/utils/time';
import { useTranslation } from '../../translation/TranslationProvider';
@@ -72,7 +73,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD
function TimelineClock() {
const { getLocalizedString } = useTranslation();
const clock = useClock();
const clock = useAutoTickingClock();
// gather timer data
const formattedClock = formatTime(clock);
+15 -7
View File
@@ -7,6 +7,7 @@ import EmptyPage from '../../common/components/state/EmptyPage';
import TitleCard from '../../common/components/title-card/TitleCard';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useAutoTickingClock } from '../../common/hooks/useAutoTickingClock';
import { useTimerSocket } from '../../common/hooks/useSocket';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { cx } from '../../common/utils/styleUtils';
@@ -101,7 +102,6 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
// gather timer data
const totalTime = getTotalTime(time.duration, time.addedTime);
const formattedClock = formatTime(clock);
const stageTimer = getTimerByType(freezeOvertime, timerTypeNow, clock, time, timerType);
const display = getFormattedTimer(stageTimer, viewTimerType, localisedMinutes, {
removeSeconds: hideTimerSeconds,
@@ -164,12 +164,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
</div>
)}
{showClock && (
<div className='clock-container'>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<SuperscriptTime time={formattedClock} className='clock' />
</div>
)}
{showClock && <TimerAutoTickingClock />}
<div className={cx(['timer-container', message.timer.blink && !showOverlay && 'blink'])}>
{showEndMessage ? (
@@ -216,3 +211,16 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
</div>
);
}
function TimerAutoTickingClock() {
const autoTickingClock = useAutoTickingClock();
const formattedClock = formatTime(autoTickingClock);
const { getLocalizedString } = useTranslation();
return (
<div className='clock-container'>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<SuperscriptTime time={formattedClock} className='clock' />
</div>
);
}