Skip to content

Commit 7c4e3cc

Browse files
committed
feat: optimize video scrubbing performance by pre-computing max velocity and improving rendering logic
1 parent ef15855 commit 7c4e3cc

2 files changed

Lines changed: 84 additions & 19 deletions

File tree

app/components/TrajectoryOverlay.tsx

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,20 @@ export function TrajectoryOverlay({
248248

249249
let frameId = 0;
250250

251+
// Pre-compute max velocity magnitude per track once per metadata load.
252+
// Avoids an O(n) reduce across all samples on every rendered frame.
253+
const maxVelocityByTrack = new Map<string, number>();
254+
if (metadata) {
255+
for (const [trackName, track] of Object.entries(metadata.tracks)) {
256+
let max = 0;
257+
for (const sample of track.samples) {
258+
const m = getVectorMagnitude(sample.velocityVector2DPerSecond);
259+
if (m > max) max = m;
260+
}
261+
maxVelocityByTrack.set(trackName, max);
262+
}
263+
}
264+
251265
const resizeCanvas = () => {
252266
const rect = container.getBoundingClientRect();
253267
const devicePixelRatio = window.devicePixelRatio || 1;
@@ -308,9 +322,7 @@ export function TrajectoryOverlay({
308322
}
309323

310324
const latestSample = track.samples[endIndex];
311-
const maxVelocityMagnitude = track.samples.reduce((currentMax, sample) => (
312-
Math.max(currentMax, getVectorMagnitude(sample.velocityVector2DPerSecond))
313-
), 0);
325+
const maxVelocityMagnitude = maxVelocityByTrack.get(trackName) ?? 0;
314326

315327
const startIndex = historyWindowSec == null
316328
? 1
@@ -438,25 +450,85 @@ export function TrajectoryOverlay({
438450
});
439451
};
440452

441-
const loop = () => {
453+
// Type helper for the requestVideoFrameCallback API (not yet in lib.dom.d.ts everywhere)
454+
type VideoWithRVFC = HTMLVideoElement & {
455+
requestVideoFrameCallback: (cb: () => void) => number;
456+
cancelVideoFrameCallback: (id: number) => void;
457+
};
458+
459+
const video = videoRef.current;
460+
const supportsRVFC = video != null && 'requestVideoFrameCallback' in HTMLVideoElement.prototype;
461+
let rvcId = 0;
462+
463+
const renderOnce = () => {
442464
resizeCanvas();
443465
renderFrame();
444-
frameId = window.requestAnimationFrame(loop);
445466
};
446467

447-
const resizeObserver = new ResizeObserver(() => {
448-
resizeCanvas();
468+
// When rVFC is supported, render exactly once per decoded video frame (perfectly
469+
// GPU-synced, zero wasted work while paused or between frames).
470+
const scheduleRVFC = () => {
471+
if (!video) return;
472+
rvcId = (video as VideoWithRVFC).requestVideoFrameCallback(() => {
473+
renderOnce();
474+
if (!video.paused) scheduleRVFC();
475+
});
476+
};
477+
478+
// Fallback: plain rAF loop (only runs while playing — see handlePlay/handlePause).
479+
const fallbackLoop = () => {
449480
renderFrame();
450-
});
481+
frameId = window.requestAnimationFrame(fallbackLoop);
482+
};
483+
484+
const startLoop = () => {
485+
if (supportsRVFC) {
486+
scheduleRVFC();
487+
} else {
488+
window.cancelAnimationFrame(frameId);
489+
frameId = window.requestAnimationFrame(fallbackLoop);
490+
}
491+
};
492+
493+
const stopLoop = () => {
494+
if (supportsRVFC) {
495+
(video as VideoWithRVFC).cancelVideoFrameCallback(rvcId);
496+
} else {
497+
window.cancelAnimationFrame(frameId);
498+
}
499+
};
500+
501+
const handlePlay = () => startLoop();
502+
const handlePause = () => { stopLoop(); renderOnce(); };
503+
// Render once per completed seek (covers scrubbing while paused).
504+
const handleSeeked = () => { renderOnce(); };
505+
506+
const resizeObserver = new ResizeObserver(() => { renderOnce(); });
507+
508+
if (video) {
509+
video.addEventListener('play', handlePlay);
510+
video.addEventListener('pause', handlePause);
511+
video.addEventListener('seeked', handleSeeked);
512+
}
451513

452514
resizeObserver.observe(container);
453515
resizeCanvas();
454-
renderFrame();
455-
frameId = window.requestAnimationFrame(loop);
516+
renderOnce();
517+
518+
// If the video is already playing when this effect runs, kick off the loop.
519+
if (video && !video.paused) {
520+
startLoop();
521+
}
456522

457523
return () => {
458-
resizeObserver.disconnect();
524+
if (video) {
525+
video.removeEventListener('play', handlePlay);
526+
video.removeEventListener('pause', handlePause);
527+
video.removeEventListener('seeked', handleSeeked);
528+
stopLoop();
529+
}
459530
window.cancelAnimationFrame(frameId);
531+
resizeObserver.disconnect();
460532
};
461533
}, [containerRef, enabled, historyWindowSec, metadata, poseColor.b, poseColor.g, poseColor.r, showBlackBackground, showPose, videoRef, visibleTrackNames]);
462534

app/components/VideoControlPanel.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { VideoPlaybackSection } from './VideoPlaybackSection';
4-
import { formatVideoTime, type KeyMoment } from '../lib/key-moments';
4+
import { type KeyMoment } from '../lib/key-moments';
55

66
interface VideoControlPanelProps {
77
hasVideos: boolean;
@@ -66,7 +66,6 @@ export function VideoControlPanel({
6666
}: VideoControlPanelProps) {
6767
if (!hasVideos) return null;
6868

69-
const maxDuration = Math.max(duration1, duration2);
7069
const selectedKeyMoment = selectedKeyMomentId
7170
? keyMoments.find((keyMoment) => keyMoment.id === selectedKeyMomentId) ?? null
7271
: null;
@@ -79,12 +78,6 @@ export function VideoControlPanel({
7978
Video Controls
8079
</h2>
8180
<div className="space-y-4">
82-
<div className="flex items-center justify-between text-sm">
83-
<span className="text-gray-600 dark:text-gray-400">Max Duration:</span>
84-
<span className="font-mono text-gray-800 dark:text-gray-200">
85-
{formatVideoTime(maxDuration)}
86-
</span>
87-
</div>
8881
{showRemoveVideos && (onRemoveVideo1 || onRemoveVideo2) && (
8982
<div>
9083
<button

0 commit comments

Comments
 (0)