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.
|
// 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
|
||||||
await waitForFrame(target);
|
// 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;
|
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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user