preview intentional perf optimizations
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user