From e2d4a1ec4e428ee3ee02b1d6df2181e68a86abfb Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:45:15 +1000 Subject: [PATCH] Preserve paused preview camera and defer composition until seek completion --- src/components/video-editor/VideoPlayback.tsx | 9 ++++++--- .../videoPlayback/sceneMotion.test.ts | 17 +++++++++++++++-- .../video-editor/videoPlayback/sceneMotion.ts | 7 +++++-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 842343dc..7f4f5d77 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -1044,9 +1044,9 @@ const VideoPlayback = forwardRef( }); cropBoundsRef.current = result.cropBounds; - // Reset camera container to identity - cameraContainer.scale.set(1); - cameraContainer.position.set(0, 0); + // Layout updates the media geometry, not the composed camera pose. + // In particular, a ResizeObserver notification while paused must not + // replace the exported spring position with an unzoomed frame. const selectedId = selectedZoomIdRef.current; const activeRegion = selectedId @@ -1893,6 +1893,7 @@ const VideoPlayback = forwardRef( video, getClips: () => clipRegionsRef.current, onTime: (time, source) => { + timelineTimeRef.current = time; if (source !== null) currentTimeRef.current = source * 1000; onTimeUpdate(time); }, @@ -1909,6 +1910,7 @@ const VideoPlayback = forwardRef( transport.seek(timelineTimeRef.current); const handleSeeked = () => { isSeekingRef.current = false; + shouldSnapPausedFrameRef.current = true; }; const handleSeeking = () => { isSeekingRef.current = true; @@ -2016,6 +2018,7 @@ const VideoPlayback = forwardRef( if ( !shouldComposePreviewFrame({ motionMode, + isSeeking: isSeekingRef.current || Boolean(videoRef.current?.seeking), contentTimeChanged, shouldSnapPausedFrame: shouldSnapPausedFrameRef.current, }) diff --git a/src/components/video-editor/videoPlayback/sceneMotion.test.ts b/src/components/video-editor/videoPlayback/sceneMotion.test.ts index 3c42588b..e6043729 100644 --- a/src/components/video-editor/videoPlayback/sceneMotion.test.ts +++ b/src/components/video-editor/videoPlayback/sceneMotion.test.ts @@ -43,13 +43,13 @@ describe("resolveSceneZoomTarget", () => { }); describe("resolvePreviewMotionMode", () => { - it("preserves the composed frame on a plain pause", () => { + it.each([false, true])("preserves a plain pause with classic mode %s", (zoomClassicMode) => { expect( resolvePreviewMotionMode({ isPlaying: false, isSeeking: false, shouldSnapPausedFrame: false, - zoomClassicMode: false, + zoomClassicMode, }), ).toBe("preserve"); }); @@ -97,3 +97,16 @@ describe("shouldComposePreviewFrame", () => { ).toBe(true); }); }); + + +describe("preview seek completion", () => { + it("holds the composed frame until seeking finishes, even with a pending refresh", () => { + const pending = { + motionMode: "snap" as const, + contentTimeChanged: true, + shouldSnapPausedFrame: true, + }; + expect(shouldComposePreviewFrame({ ...pending, isSeeking: true })).toBe(false); + expect(shouldComposePreviewFrame({ ...pending, isSeeking: false })).toBe(true); + }); +}); diff --git a/src/components/video-editor/videoPlayback/sceneMotion.ts b/src/components/video-editor/videoPlayback/sceneMotion.ts index 7d4930ac..20e7c374 100644 --- a/src/components/video-editor/videoPlayback/sceneMotion.ts +++ b/src/components/video-editor/videoPlayback/sceneMotion.ts @@ -32,7 +32,7 @@ export function resolvePreviewMotionMode({ shouldSnapPausedFrame: boolean; zoomClassicMode: boolean; }): PreviewMotionMode { - if (isSeeking || shouldSnapPausedFrame || zoomClassicMode) { + if (isSeeking || shouldSnapPausedFrame || (isPlaying && zoomClassicMode)) { return "snap"; } @@ -42,14 +42,17 @@ export function resolvePreviewMotionMode({ /** Match export's one-composition-per-media-frame behavior. */ export function shouldComposePreviewFrame({ motionMode, + isSeeking = false, contentTimeChanged, shouldSnapPausedFrame, }: { motionMode: PreviewMotionMode; + isSeeking?: boolean; contentTimeChanged: boolean; shouldSnapPausedFrame: boolean; }): boolean { - if (motionMode === "preserve") { + // Do not consume the pending composition against the old decoded image. + if (isSeeking || motionMode === "preserve") { return false; }