From 7e00cdb1a9eb9da5fb9637921fa1dc4bd6dce54a Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sun, 3 May 2026 11:41:03 -0700 Subject: [PATCH] preview intentional perf optimizations --- src/components/video-editor/VideoPlayback.tsx | 31 ++++++- .../videoPlayback/videoEventHandlers.ts | 35 ++++++++ .../videoPlayback/zoomRegionUtils.ts | 87 ++++++++++++++----- 3 files changed, 131 insertions(+), 22 deletions(-) diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index a69c8d7..c35c0c7 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -232,6 +232,9 @@ const VideoPlayback = forwardRef( const maskGraphicsRef = useRef(null); const isPlayingRef = useRef(isPlaying); const isSeekingRef = useRef(false); + const isScrubbingRef = useRef(false); + const scrubEndTimerRef = useRef(null); + const [isScrubbing, setIsScrubbing] = useState(false); const allowPlaybackRef = useRef(false); const lockedVideoDimensionsRef = useRef<{ width: number; @@ -611,6 +614,24 @@ const VideoPlayback = forwardRef( }; }, [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( onTimeUpdate: (time) => onTimeUpdateRef.current(time), trimRegionsRef, speedRegionsRef, + isScrubbingRef, + scrubEndTimerRef, + onScrubChange: (scrubbing) => setIsScrubbing(scrubbing), }); video.addEventListener("play", handlePlay); @@ -1088,7 +1112,8 @@ const VideoPlayback = forwardRef( } } - 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( cancelAnimationFrame(videoReadyRafRef.current); videoReadyRafRef.current = null; } + if (scrubEndTimerRef.current !== null) { + window.clearTimeout(scrubEndTimerRef.current); + scrubEndTimerRef.current = null; + } }; }, []); diff --git a/src/components/video-editor/videoPlayback/videoEventHandlers.ts b/src/components/video-editor/videoPlayback/videoEventHandlers.ts index 5542d67..a26107d 100644 --- a/src/components/video-editor/videoPlayback/videoEventHandlers.ts +++ b/src/components/video-editor/videoPlayback/videoEventHandlers.ts @@ -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; @@ -12,6 +17,9 @@ interface VideoEventHandlersParams { onTimeUpdate: (time: number) => void; trimRegionsRef: React.MutableRefObject; speedRegionsRef: React.MutableRefObject; + isScrubbingRef?: React.MutableRefObject; + scrubEndTimerRef?: React.MutableRefObject; + 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(); } diff --git a/src/components/video-editor/videoPlayback/zoomRegionUtils.ts b/src/components/video-editor/videoPlayback/zoomRegionUtils.ts index e5c16e1..ce31e0e 100644 --- a/src/components/video-editor/videoPlayback/zoomRegionUtils.ts +++ b/src/components/video-editor/videoPlayback/zoomRegionUtils.ts @@ -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; }