preview intentional perf optimizations

This commit is contained in:
Siddharth
2026-05-03 11:41:03 -07:00
parent f7d1bc6f05
commit 7e00cdb1a9
3 changed files with 131 additions and 22 deletions
+30 -1
View File
@@ -232,6 +232,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
const maskGraphicsRef = useRef<Graphics | null>(null);
const isPlayingRef = useRef(isPlaying);
const isSeekingRef = useRef(false);
const isScrubbingRef = useRef(false);
const scrubEndTimerRef = useRef<number | null>(null);
const [isScrubbing, setIsScrubbing] = useState(false);
const allowPlaybackRef = useRef(false);
const lockedVideoDimensionsRef = useRef<{
width: number;
@@ -611,6 +614,24 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
};
}, [pixiReady, videoReady, layoutVideoContent]);
// Drop the PIXI canvas resolution to 1.0 while scrubbing (the user is
// navigating, not previewing) and restore native DPR on play/idle so the
// preview stays faithful. Mutating renderer.resolution per-frame would
// thrash texture uploads; we only do it on scrub-state transitions.
useEffect(() => {
if (!pixiReady) return;
const app = appRef.current;
const container = containerRef.current;
if (!app || !container) return;
const targetResolution = isScrubbing ? 1 : window.devicePixelRatio || 1;
if (app.renderer.resolution === targetResolution) return;
app.renderer.resolution = targetResolution;
app.renderer.resize(container.clientWidth, container.clientHeight);
layoutVideoContentRef.current?.();
}, [isScrubbing, pixiReady]);
useEffect(() => {
if (!pixiReady || !videoReady) return;
updateOverlayForRegion(selectedZoom);
@@ -804,6 +825,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
onTimeUpdate: (time) => onTimeUpdateRef.current(time),
trimRegionsRef,
speedRegionsRef,
isScrubbingRef,
scrubEndTimerRef,
onScrubChange: (scrubbing) => setIsScrubbing(scrubbing),
});
video.addEventListener("play", handlePlay);
@@ -1088,7 +1112,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
}
}
const isMotionBlurActive = (motionBlurAmountRef.current || 0) > 0 && isPlayingRef.current;
const isMotionBlurActive =
(motionBlurAmountRef.current || 0) > 0 && isPlayingRef.current && !isScrubbingRef.current;
if (isMotionBlurActive !== lastMotionBlurActive && videoContainerRef.current) {
if (isMotionBlurActive) {
@@ -1225,6 +1250,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
cancelAnimationFrame(videoReadyRafRef.current);
videoReadyRafRef.current = null;
}
if (scrubEndTimerRef.current !== null) {
window.clearTimeout(scrubEndTimerRef.current);
scrubEndTimerRef.current = null;
}
};
}, []);
@@ -1,6 +1,11 @@
import type React from "react";
import type { SpeedRegion, TrimRegion } from "../types";
// Keep "scrub mode" on for a brief tail after `seeked` — rapid drag-scrubbing
// fires `seeking`/`seeked` dozens of times per second, and toggling effects
// each time would flicker.
const SCRUB_END_DEBOUNCE_MS = 150;
interface VideoEventHandlersParams {
video: HTMLVideoElement;
isSeekingRef: React.MutableRefObject<boolean>;
@@ -12,6 +17,9 @@ interface VideoEventHandlersParams {
onTimeUpdate: (time: number) => void;
trimRegionsRef: React.MutableRefObject<TrimRegion[]>;
speedRegionsRef: React.MutableRefObject<SpeedRegion[]>;
isScrubbingRef?: React.MutableRefObject<boolean>;
scrubEndTimerRef?: React.MutableRefObject<number | null>;
onScrubChange?: (scrubbing: boolean) => void;
}
export function createVideoEventHandlers(params: VideoEventHandlersParams) {
@@ -26,8 +34,18 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
onTimeUpdate,
trimRegionsRef,
speedRegionsRef,
isScrubbingRef,
scrubEndTimerRef,
onScrubChange,
} = params;
const clearScrubEndTimer = () => {
if (scrubEndTimerRef && scrubEndTimerRef.current !== null) {
window.clearTimeout(scrubEndTimerRef.current);
scrubEndTimerRef.current = null;
}
};
const emitTime = (timeValue: number) => {
currentTimeRef.current = timeValue * 1000;
onTimeUpdate(timeValue);
@@ -113,6 +131,15 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
const handleSeeked = () => {
isSeekingRef.current = false;
if (isScrubbingRef && scrubEndTimerRef) {
clearScrubEndTimer();
scrubEndTimerRef.current = window.setTimeout(() => {
isScrubbingRef.current = false;
scrubEndTimerRef.current = null;
onScrubChange?.(false);
}, SCRUB_END_DEBOUNCE_MS);
}
const currentTimeMs = video.currentTime * 1000;
const activeTrimRegion = findActiveTrimRegion(currentTimeMs);
@@ -137,6 +164,14 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
const handleSeeking = () => {
isSeekingRef.current = true;
if (isScrubbingRef) {
clearScrubEndTimer();
if (!isScrubbingRef.current) {
isScrubbingRef.current = true;
onScrubChange?.(true);
}
}
if (!isPlayingRef.current && !video.paused) {
video.pause();
}
@@ -254,34 +254,79 @@ function getConnectedRegionTransition(
return null;
}
export function findDominantRegion(
regions: ZoomRegion[],
timeMs: number,
options: DominantRegionOptions = {},
): {
type DominantRegionResult = {
region: ZoomRegion | null;
strength: number;
blendedScale: number | null;
transition: ConnectedPanTransition | null;
} {
const connectedPairs = options.connectZooms ? getConnectedRegionPairs(regions) : [];
};
// Single-slot cache: the ticker calls findDominantRegion at 60fps with mostly
// unchanged inputs (especially while paused). Reusing the previous result when
// inputs match avoids the per-frame O(N) region scan + allocations.
let dominantRegionCache: {
regions: ZoomRegion[];
timeMsKey: number;
telemetry: CursorTelemetryPoint[] | undefined;
connectZooms: boolean;
viewportRatio: ViewportRatio | undefined;
result: DominantRegionResult;
} | null = null;
export function findDominantRegion(
regions: ZoomRegion[],
timeMs: number,
options: DominantRegionOptions = {},
): DominantRegionResult {
const connectZooms = !!options.connectZooms;
const telemetry = options.cursorTelemetry;
const vr = options.viewportRatio;
const timeMsKey = Math.round(timeMs);
if (options.connectZooms) {
const connectedTransition = getConnectedRegionTransition(connectedPairs, timeMs, telemetry, vr);
if (connectedTransition) {
return connectedTransition;
}
const connectedHold = getConnectedRegionHold(timeMs, connectedPairs, telemetry, vr);
if (connectedHold) {
return { ...connectedHold, transition: null };
}
if (
dominantRegionCache &&
dominantRegionCache.regions === regions &&
dominantRegionCache.timeMsKey === timeMsKey &&
dominantRegionCache.telemetry === telemetry &&
dominantRegionCache.connectZooms === connectZooms &&
dominantRegionCache.viewportRatio === vr
) {
return dominantRegionCache.result;
}
const activeRegion = getActiveRegion(regions, timeMs, connectedPairs, telemetry, vr);
return activeRegion
? { ...activeRegion, transition: null }
: { region: null, strength: 0, blendedScale: null, transition: null };
const connectedPairs = connectZooms ? getConnectedRegionPairs(regions) : [];
let result: DominantRegionResult;
if (connectZooms) {
const connectedTransition = getConnectedRegionTransition(connectedPairs, timeMs, telemetry, vr);
if (connectedTransition) {
result = connectedTransition;
} else {
const connectedHold = getConnectedRegionHold(timeMs, connectedPairs, telemetry, vr);
if (connectedHold) {
result = { ...connectedHold, transition: null };
} else {
const activeRegion = getActiveRegion(regions, timeMs, connectedPairs, telemetry, vr);
result = activeRegion
? { ...activeRegion, transition: null }
: { region: null, strength: 0, blendedScale: null, transition: null };
}
}
} else {
const activeRegion = getActiveRegion(regions, timeMs, connectedPairs, telemetry, vr);
result = activeRegion
? { ...activeRegion, transition: null }
: { region: null, strength: 0, blendedScale: null, transition: null };
}
dominantRegionCache = {
regions,
timeMsKey,
telemetry,
connectZooms,
viewportRatio: vr,
result,
};
return result;
}