diff --git a/web/src/components/preview/Preview.tsx b/web/src/components/preview/Preview.tsx index bf83a80..97db0b1 100644 --- a/web/src/components/preview/Preview.tsx +++ b/web/src/components/preview/Preview.tsx @@ -93,16 +93,50 @@ export function Preview() { // The playhead still moves instantly; the composite is fetched only once the // playhead has settled for ~140ms (i.e. you stop scrubbing). See the perf // issue for the proper fix (a streaming playback/scrub engine). - const composeFrame = useDebounced( - Math.min(Math.round(activeFrame), Math.max(0, timelineTotal - 1)), - 140, - ); - const timelineFrameUrl = useTimelineFrame( + // + // The frame the composite should show: rounded, clamped playhead. Computed once + // and shared by the request AND the readiness gate so `readyFrame === targetFrame` + // can actually match. + const targetFrame = Math.min(Math.round(activeFrame), Math.max(0, timelineTotal - 1)); + // Debounce ONLY for paused scrubbing. The play→pause settle bypasses it below. + const scrubFrame = useDebounced(targetFrame, 140); + + // Settle window: spans the play→pause edge until the composite for the EXACT + // stop frame is ready. During it we keep the played