diff --git a/apps/client/src/common/hooks/useAutoTickingClock.ts b/apps/client/src/common/hooks/useAutoTickingClock.ts new file mode 100644 index 000000000..454707cbc --- /dev/null +++ b/apps/client/src/common/hooks/useAutoTickingClock.ts @@ -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); +} diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 34fbb16a3..ab14f259e 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -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, diff --git a/apps/client/src/features/overview/composite/TimeElements.tsx b/apps/client/src/features/overview/composite/TimeElements.tsx index 2397b5172..27e8c8f50 100644 --- a/apps/client/src/features/overview/composite/TimeElements.tsx +++ b/apps/client/src/features/overview/composite/TimeElements.tsx @@ -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 ( diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index 326938b57..ec87ebfcd 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -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); diff --git a/apps/client/src/views/countdown/Countdown.tsx b/apps/client/src/views/countdown/Countdown.tsx index b26240b76..f657b3cbc 100644 --- a/apps/client/src/views/countdown/Countdown.tsx +++ b/apps/client/src/views/countdown/Countdown.tsx @@ -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); diff --git a/apps/client/src/views/studio/StudioClock.tsx b/apps/client/src/views/studio/StudioClock.tsx index 01bfcd8c9..f764aee45 100644 --- a/apps/client/src/views/studio/StudioClock.tsx +++ b/apps/client/src/views/studio/StudioClock.tsx @@ -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 diff --git a/apps/client/src/views/timeline/TimelinePage.tsx b/apps/client/src/views/timeline/TimelinePage.tsx index a4cd0be45..8882ec81a 100644 --- a/apps/client/src/views/timeline/TimelinePage.tsx +++ b/apps/client/src/views/timeline/TimelinePage.tsx @@ -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); diff --git a/apps/client/src/views/timer/Timer.tsx b/apps/client/src/views/timer/Timer.tsx index a36e1a990..4605b3d3a 100644 --- a/apps/client/src/views/timer/Timer.tsx +++ b/apps/client/src/views/timer/Timer.tsx @@ -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, )} - {showClock && ( -