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
This commit is contained in:
@@ -30,6 +30,7 @@ export function ControlScenePreview({ session, videoRef, onContentRectChange }:
|
||||
const rot = scene?.previewRotationDeg ?? 0;
|
||||
const isVideo = scene?.previewAssetType === 'video';
|
||||
const assetId = scene?.previewAssetType === 'video' ? scene.previewAssetId : null;
|
||||
const autostart = scene?.previewVideoAutostart ?? false;
|
||||
|
||||
const [tick, setTick] = useState(0);
|
||||
const dur = useMemo(
|
||||
@@ -38,7 +39,7 @@ export function ControlScenePreview({ session, videoRef, onContentRectChange }:
|
||||
if (!v) return 0;
|
||||
return Number.isFinite(v.duration) ? v.duration : 0;
|
||||
},
|
||||
// tick: перечитываем duration из video ref на каждом кадре RAF
|
||||
// tick: timeupdate / loadedmetadata перечитывают duration и currentTime
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- намеренно
|
||||
[tick, videoRef],
|
||||
);
|
||||
@@ -55,23 +56,15 @@ export function ControlScenePreview({ session, videoRef, onContentRectChange }:
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVideo) return;
|
||||
let raf = 0;
|
||||
const loop = () => {
|
||||
setTick((x) => x + 1);
|
||||
raf = window.requestAnimationFrame(loop);
|
||||
};
|
||||
raf = window.requestAnimationFrame(loop);
|
||||
return () => window.cancelAnimationFrame(raf);
|
||||
}, [isVideo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVideo) return;
|
||||
if (!assetId) return;
|
||||
// `target.set` bumps revision and resets anchors; avoid firing on every render.
|
||||
if (vp?.targetAssetId === assetId) return;
|
||||
void video.dispatch({
|
||||
kind: 'target.set',
|
||||
assetId,
|
||||
autostart: scene.previewVideoAutostart,
|
||||
autostart,
|
||||
});
|
||||
}, [assetId, isVideo, scene, video]);
|
||||
}, [assetId, isVideo, autostart, vp?.targetAssetId, video]);
|
||||
|
||||
useEffect(() => {
|
||||
const v = videoRef.current;
|
||||
@@ -88,7 +81,8 @@ export function ControlScenePreview({ session, videoRef, onContentRectChange }:
|
||||
} else {
|
||||
v.pause();
|
||||
}
|
||||
}, [assetId, vp, videoRef]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- avoid reruns on 500ms heartbeats (serverNowMs-only updates)
|
||||
}, [assetId, url, vp?.revision, vp?.targetAssetId, vp?.playing, vp?.playbackRate, videoRef]);
|
||||
|
||||
const scrubClass = [styles.scrub, dur ? styles.scrubPointer : styles.scrubDefault].join(' ');
|
||||
|
||||
@@ -105,6 +99,8 @@ export function ControlScenePreview({ session, videoRef, onContentRectChange }:
|
||||
src={url}
|
||||
playsInline
|
||||
preload="auto"
|
||||
onTimeUpdate={() => setTick((x) => x + 1)}
|
||||
onLoadedMetadata={() => setTick((x) => x + 1)}
|
||||
>
|
||||
<track kind="captions" srcLang="ru" label="Превью без субтитров" />
|
||||
</video>
|
||||
|
||||
Reference in New Issue
Block a user