Files
Ivan Fontosh add699a320 fix(video): prevent preview/presentation playback loop
- Avoid per-render target.set dispatch that reset playback

- Stop timer-based and per-frame React ticking while playing

- Add regression tests for render-loop sources

Made-with: Cursor
2026-04-20 18:30:47 +08:00

49 lines
1.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useRef, useState } from 'react';
import { ipcChannels } from '../../../shared/ipc/contracts';
import type { VideoPlaybackEvent, VideoPlaybackState } from '../../../shared/types';
import { getDndApi } from '../dndApi';
export function useVideoPlaybackState(): readonly [
VideoPlaybackState | null,
{ dispatch: (event: VideoPlaybackEvent) => Promise<void> },
] {
const api = getDndApi();
const [playback, setPlayback] = useState<VideoPlaybackState | null>(null);
/** serverNowMs Date.now() at last IPC sync; lets us compute a live clock without React timers. */
const timeOffsetMsRef = useRef(0);
useEffect(() => {
void api.invoke(ipcChannels.video.getState, {}).then((r) => {
timeOffsetMsRef.current = r.state.serverNowMs - Date.now();
setPlayback(r.state);
});
return api.on(ipcChannels.video.stateChanged, ({ state: next }) => {
timeOffsetMsRef.current = next.serverNowMs - Date.now();
setPlayback((prev) => {
if (prev?.revision === next.revision) return prev;
return next;
});
});
}, [api]);
const state = useMemo((): VideoPlaybackState | null => {
if (!playback) return null;
return {
...playback,
get serverNowMs(): number {
return Date.now() + timeOffsetMsRef.current;
},
};
}, [playback]);
return [
state,
{
dispatch: async (event) => {
await api.invoke(ipcChannels.video.dispatch, { event });
},
},
] as const;
}