Smooth transitions between preview scrubbing (#10636)

* Use canvas to save video state before switching to smooth transitions between previews

* Smooth current hour as well
This commit is contained in:
Nicolas Mowen 2024-03-23 17:11:35 -06:00 committed by GitHub
parent bb50b2b6f4
commit e3a7aa6b6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -35,6 +35,8 @@ export default function PreviewPlayer({
onControllerReady, onControllerReady,
onClick, onClick,
}: PreviewPlayerProps) { }: PreviewPlayerProps) {
const [currentHourFrame, setCurrentHourFrame] = useState<string>();
if (isCurrentHour(timeRange.end)) { if (isCurrentHour(timeRange.end)) {
return ( return (
<PreviewFramesPlayer <PreviewFramesPlayer
@ -44,6 +46,7 @@ export default function PreviewPlayer({
startTime={startTime} startTime={startTime}
onControllerReady={onControllerReady} onControllerReady={onControllerReady}
onClick={onClick} onClick={onClick}
setCurrentHourFrame={setCurrentHourFrame}
/> />
); );
} }
@ -56,8 +59,10 @@ export default function PreviewPlayer({
cameraPreviews={cameraPreviews} cameraPreviews={cameraPreviews}
startTime={startTime} startTime={startTime}
isScrubbing={isScrubbing} isScrubbing={isScrubbing}
currentHourFrame={currentHourFrame}
onControllerReady={onControllerReady} onControllerReady={onControllerReady}
onClick={onClick} onClick={onClick}
setCurrentHourFrame={setCurrentHourFrame}
/> />
); );
} }
@ -83,8 +88,10 @@ type PreviewVideoPlayerProps = {
cameraPreviews: Preview[]; cameraPreviews: Preview[];
startTime?: number; startTime?: number;
isScrubbing: boolean; isScrubbing: boolean;
currentHourFrame?: string;
onControllerReady: (controller: PreviewVideoController) => void; onControllerReady: (controller: PreviewVideoController) => void;
onClick?: () => void; onClick?: () => void;
setCurrentHourFrame: (src: string | undefined) => void;
}; };
function PreviewVideoPlayer({ function PreviewVideoPlayer({
className, className,
@ -93,8 +100,10 @@ function PreviewVideoPlayer({
cameraPreviews, cameraPreviews,
startTime, startTime,
isScrubbing, isScrubbing,
currentHourFrame,
onControllerReady, onControllerReady,
onClick, onClick,
setCurrentHourFrame,
}: PreviewVideoPlayerProps) { }: PreviewVideoPlayerProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -134,6 +143,7 @@ function PreviewVideoPlayer({
// initial state // initial state
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const [hasCanvas, setHasCanvas] = useState(false);
const initialPreview = useMemo(() => { const initialPreview = useMemo(() => {
return cameraPreviews.find( return cameraPreviews.find(
(preview) => (preview) =>
@ -187,22 +197,57 @@ function PreviewVideoPlayer({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [controller, timeRange]); }, [controller, timeRange]);
// canvas to cover preview transition
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [videoWidth, videoHeight] = useMemo(() => {
if (!previewRef.current) {
return [0, 0];
}
return [previewRef.current.videoWidth, previewRef.current.videoHeight];
// we know the video size will be known on load
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loaded]);
// handle switching sources
useEffect(() => { useEffect(() => {
if (!currentPreview || !previewRef.current) { if (!currentPreview || !previewRef.current) {
return; return;
} }
if (canvasRef.current) {
canvasRef.current
.getContext("2d")
?.drawImage(previewRef.current, 0, 0, videoWidth, videoHeight);
setHasCanvas(true);
}
previewRef.current.load(); previewRef.current.load();
// we only want this to change when current preview changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPreview, previewRef]); }, [currentPreview, previewRef]);
return ( return (
<div <div
className={`relative w-full ${className ?? ""} ${onClick ? "cursor-pointer" : ""}`} className={`relative w-full rounded-2xl overflow-hidden ${className ?? ""} ${onClick ? "cursor-pointer" : ""}`}
onClick={onClick} onClick={onClick}
> >
{currentHourFrame && (
<img
className="absolute size-full object-contain"
src={currentHourFrame}
/>
)}
<canvas
ref={canvasRef}
width={videoWidth}
height={videoHeight}
className={`absolute h-full left-1/2 -translate-x-1/2 bg-black ${!loaded && hasCanvas ? "" : "hidden"}`}
/>
<video <video
ref={previewRef} ref={previewRef}
className={`size-full rounded-2xl bg-black`} className="size-full"
preload="auto" preload="auto"
autoPlay autoPlay
playsInline playsInline
@ -210,6 +255,7 @@ function PreviewVideoPlayer({
disableRemotePlayback disableRemotePlayback
onSeeked={onPreviewSeeked} onSeeked={onPreviewSeeked}
onLoadedData={() => { onLoadedData={() => {
setCurrentHourFrame(undefined);
setLoaded(true); setLoaded(true);
if (controller) { if (controller) {
@ -227,7 +273,9 @@ function PreviewVideoPlayer({
<source src={currentPreview.src} type={currentPreview.type} /> <source src={currentPreview.src} type={currentPreview.type} />
)} )}
</video> </video>
{!loaded && <Skeleton className="absolute inset-0" />} {!loaded && !hasCanvas && !currentHourFrame && (
<Skeleton className="absolute inset-0" />
)}
{cameraPreviews && !currentPreview && ( {cameraPreviews && !currentPreview && (
<div className="absolute inset-0 bg-black text-white rounded-2xl flex justify-center items-center"> <div className="absolute inset-0 bg-black text-white rounded-2xl flex justify-center items-center">
No Preview Found No Preview Found
@ -329,12 +377,14 @@ type PreviewFramesPlayerProps = {
startTime?: number; startTime?: number;
onControllerReady: (controller: PreviewController) => void; onControllerReady: (controller: PreviewController) => void;
onClick?: () => void; onClick?: () => void;
setCurrentHourFrame: (src: string) => void;
}; };
function PreviewFramesPlayer({ function PreviewFramesPlayer({
className, className,
camera, camera,
timeRange, timeRange,
startTime, startTime,
setCurrentHourFrame,
onControllerReady, onControllerReady,
onClick, onClick,
}: PreviewFramesPlayerProps) { }: PreviewFramesPlayerProps) {
@ -365,7 +415,12 @@ function PreviewFramesPlayer({
return undefined; return undefined;
} }
return new PreviewFramesController(camera, imgRef, frameTimes); return new PreviewFramesController(
camera,
imgRef,
frameTimes,
setCurrentHourFrame,
);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [imgRef, frameTimes, imgRef.current]); }, [imgRef, frameTimes, imgRef.current]);
@ -430,15 +485,18 @@ class PreviewFramesController extends PreviewController {
frameTimes: number[]; frameTimes: number[];
seeking: boolean = false; seeking: boolean = false;
private timeToSeek: number | undefined = undefined; private timeToSeek: number | undefined = undefined;
private setCurrentFrame: (src: string) => void;
constructor( constructor(
camera: string, camera: string,
imgController: MutableRefObject<HTMLImageElement | null>, imgController: MutableRefObject<HTMLImageElement | null>,
frameTimes: number[], frameTimes: number[],
setCurrentFrame: (src: string) => void,
) { ) {
super(camera); super(camera);
this.imgController = imgController; this.imgController = imgController;
this.frameTimes = frameTimes; this.frameTimes = frameTimes;
this.setCurrentFrame = setCurrentFrame;
} }
override scrubToTimestamp(time: number): boolean { override scrubToTimestamp(time: number): boolean {
@ -478,6 +536,7 @@ class PreviewFramesController extends PreviewController {
if (this.imgController.current.src != newSrc) { if (this.imgController.current.src != newSrc) {
this.imgController.current.src = newSrc; this.imgController.current.src = newSrc;
this.setCurrentFrame(newSrc);
} else { } else {
this.timeToSeek = undefined; this.timeToSeek = undefined;
this.seeking = false; this.seeking = false;