playback speed issues
This commit is contained in:
@@ -87,6 +87,11 @@ export function ExtractMode({ username, settings }: Props) {
|
||||
// stuck in "playing" while the canvas freezes.
|
||||
const decodeErrorRef = useRef<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(() => {
|
||||
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)) {
|
||||
|
||||
Reference in New Issue
Block a user