playback speed issues

This commit is contained in:
2026-06-04 15:44:38 +05:30
parent 5ada2d773b
commit 436afcae4c

View File

@@ -87,6 +87,11 @@ export function ExtractMode({ username, settings }: Props) {
// stuck in "playing" while the canvas freezes. // stuck in "playing" while the canvas freezes.
const decodeErrorRef = useRef<string | null>(null); const decodeErrorRef = useRef<string | null>(null);
const [playbackError, setPlaybackError] = useState<string | null>(null); const [playbackError, setPlaybackError] = useState<string | null>(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(() => { const clearBitmapCache = useCallback(() => {
for (const bitmap of bitmapCacheRef.current.values()) { 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 // optionally at the user's Preview-quality preset. Halves drawImage
// cost on 4K-into-1080p (the common slow-system case). Coords below // cost on 4K-into-1080p (the common slow-system case). Coords below
// are in image-pixel space — the transform maps to backing pixels so // are in image-pixel space — the transform maps to backing pixels so
// overlay.ts is unchanged. // overlay.ts is unchanged. The rect is cached by the ResizeObserver
const rect = canvas.getBoundingClientRect(); // 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; const dpr = window.devicePixelRatio || 1;
let maxW = video.width; let maxW = video.width;
let maxH = video.height; let maxH = video.height;
@@ -282,7 +293,28 @@ export function ExtractMode({ username, settings }: Props) {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas || !video) return; if (!canvas || !video) return;
let scheduled = false; 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; if (scheduled) return;
scheduled = true; scheduled = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -326,10 +358,6 @@ export function ExtractMode({ username, settings }: Props) {
const loop = async () => { const loop = async () => {
while (!cancelled && playing) { while (!cancelled && playing) {
const skip = Math.max(1, frameSkipRef.current | 0); 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; const current = frameIdxRef.current;
let target = Math.min(video.total_frames - 1, current + skip); let target = Math.min(video.total_frames - 1, current + skip);
if (target <= current) { if (target <= current) {
@@ -348,8 +376,19 @@ export function ExtractMode({ username, settings }: Props) {
} }
} }
requestedIdx.current = target; requestedIdx.current = target;
pump(); // 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); await waitForFrame(target);
}
if (cancelled) return; if (cancelled) return;
// Decode failed mid-play: stop and surface so the UI doesn't claim to // 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. // be playing while the canvas is frozen on the last good frame.
@@ -358,6 +397,17 @@ export function ExtractMode({ username, settings }: Props) {
setPlaying(false); setPlaying(false);
return; 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); setFrameIdx(target);
// Pause AFTER rendering so the user actually sees the annotated frame. // Pause AFTER rendering so the user actually sees the annotated frame.
if (reviewMode && annotatedFrames.current.has(target)) { if (reviewMode && annotatedFrames.current.has(target)) {