From 436afcae4c6fa77f1817a81e5d52c0bce915fdf7 Mon Sep 17 00:00:00 2001 From: ravi Date: Thu, 4 Jun 2026 15:44:38 +0530 Subject: [PATCH] playback speed issues --- sam-tool-tauri/src/modes/ExtractMode.tsx | 68 ++++++++++++++++++++---- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/sam-tool-tauri/src/modes/ExtractMode.tsx b/sam-tool-tauri/src/modes/ExtractMode.tsx index c431bc0..ad7c88e 100644 --- a/sam-tool-tauri/src/modes/ExtractMode.tsx +++ b/sam-tool-tauri/src/modes/ExtractMode.tsx @@ -87,6 +87,11 @@ export function ExtractMode({ username, settings }: Props) { // stuck in "playing" while the canvas freezes. const decodeErrorRef = useRef(null); const [playbackError, setPlaybackError] = useState(null); + // Cached canvas CSS rect. drawBitmap used to call getBoundingClientRect() + // every frame, which forces a synchronous layout flush each time. We now + // refresh it from the ResizeObserver below and fall back to a one-shot + // query if it isn't populated yet. + const canvasRectRef = useRef<{ width: number; height: number } | null>(null); const clearBitmapCache = useCallback(() => { for (const bitmap of bitmapCacheRef.current.values()) { @@ -131,8 +136,14 @@ export function ExtractMode({ username, settings }: Props) { // optionally at the user's Preview-quality preset. Halves drawImage // cost on 4K-into-1080p (the common slow-system case). Coords below // are in image-pixel space — the transform maps to backing pixels so - // overlay.ts is unchanged. - const rect = canvas.getBoundingClientRect(); + // overlay.ts is unchanged. The rect is cached by the ResizeObserver + // effect; we only fall back to a live query on the first frame. + let rect = canvasRectRef.current; + if (!rect) { + const dom = canvas.getBoundingClientRect(); + rect = { width: dom.width, height: dom.height }; + canvasRectRef.current = rect; + } const dpr = window.devicePixelRatio || 1; let maxW = video.width; let maxH = video.height; @@ -282,7 +293,28 @@ export function ExtractMode({ username, settings }: Props) { const canvas = canvasRef.current; if (!canvas || !video) return; let scheduled = false; - const ro = new ResizeObserver(() => { + // Seed the rect cache once on mount so the first drawBitmap doesn't + // have to force a layout flush itself. + { + const dom = canvas.getBoundingClientRect(); + canvasRectRef.current = { width: dom.width, height: dom.height }; + } + const ro = new ResizeObserver((entries) => { + // Update the cached rect from the observer entry — it carries the + // CSS-box size without forcing a sync layout. + const last = entries[entries.length - 1]; + const box = last?.contentBoxSize?.[0]; + if (box) { + canvasRectRef.current = { + width: box.inlineSize, + height: box.blockSize, + }; + } else if (last?.contentRect) { + canvasRectRef.current = { + width: last.contentRect.width, + height: last.contentRect.height, + }; + } if (scheduled) return; scheduled = true; requestAnimationFrame(() => { @@ -326,10 +358,6 @@ export function ExtractMode({ username, settings }: Props) { const loop = async () => { while (!cancelled && playing) { const skip = Math.max(1, frameSkipRef.current | 0); - // Real-time pacing regardless of skip: skip controls density, - // `speed` controls speed. Without the skip factor here, skip=3 would - // play at 3× wall-clock — which is what the speed slider is for. - const interval = (skip * 1000) / (video.fps * speed); const current = frameIdxRef.current; let target = Math.min(video.total_frames - 1, current + skip); if (target <= current) { @@ -348,8 +376,19 @@ export function ExtractMode({ username, settings }: Props) { } } requestedIdx.current = target; - pump(); - await waitForFrame(target); + // Fast path: await pump directly. When pump owns the queue it + // resolves the instant target is drawn (no 16ms rAF latency tax). + // If another caller (ResizeObserver / overlay-change effect) was + // already running pump, our call short-circuits — fall back to + // rAF polling in that case so we still wait correctly. + await pump(); + if ( + !cancelled && + renderedIdx.current !== target && + decodeErrorRef.current === null + ) { + await waitForFrame(target); + } if (cancelled) return; // Decode failed mid-play: stop and surface so the UI doesn't claim to // be playing while the canvas is frozen on the last good frame. @@ -358,6 +397,17 @@ export function ExtractMode({ username, settings }: Props) { setPlaying(false); return; } + const advanced = Math.max(1, target - current); + // Real-time pacing regardless of skip: skip controls density, + // `speed` controls speed. Without the advanced-frame factor here, + // skip=3 would play at 3x wall-clock, which is what the speed slider + // is for. Compute after review-mode clamping so annotated stops do not + // wait for the full skip window. + const interval = (advanced * 1000) / (video.fps * speed); + // Keep the fast playback loop's cursor in sync immediately. Waiting + // for React's frameIdx effect can make a slow decode path replay the + // same target once and add an extra frame interval per rendered frame. + frameIdxRef.current = target; setFrameIdx(target); // Pause AFTER rendering so the user actually sees the annotated frame. if (reviewMode && annotatedFrames.current.has(target)) {