blakeblackshear.frigate/web/src/components/player/JSMpegPlayer.tsx
2024-07-18 10:50:30 -05:00

189 lines
5.2 KiB
TypeScript

import { baseUrl } from "@/api/baseUrl";
import { useResizeObserver } from "@/hooks/resize-observer";
import { cn } from "@/lib/utils";
// @ts-expect-error we know this doesn't have types
import JSMpeg from "@cycjimmy/jsmpeg-player";
import React, { useEffect, useMemo, useRef, useState } from "react";
type JSMpegPlayerProps = {
className?: string;
camera: string;
width: number;
height: number;
containerRef: React.MutableRefObject<HTMLDivElement | null>;
playbackEnabled: boolean;
onPlaying?: () => void;
};
export default function JSMpegPlayer({
camera,
width,
height,
className,
containerRef,
playbackEnabled,
onPlaying,
}: JSMpegPlayerProps) {
const url = `${baseUrl.replace(/^http/, "ws")}live/jsmpeg/${camera}`;
const videoRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const internalContainerRef = useRef<HTMLDivElement | null>(null);
const onPlayingRef = useRef(onPlaying);
const [showCanvas, setShowCanvas] = useState(false);
const [hasData, setHasData] = useState(false);
const hasDataRef = useRef(hasData);
const [dimensionsReady, setDimensionsReady] = useState(false);
const selectedContainerRef = useMemo(
() => (containerRef.current ? containerRef : internalContainerRef),
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
[containerRef, containerRef.current, internalContainerRef],
);
const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(selectedContainerRef);
const stretch = true;
const aspectRatio = width / height;
const fitAspect = useMemo(
() => containerWidth / containerHeight,
[containerWidth, containerHeight],
);
const scaledHeight = useMemo(() => {
if (selectedContainerRef?.current && width && height) {
const scaledHeight =
aspectRatio < (fitAspect ?? 0)
? Math.floor(
Math.min(
containerHeight,
selectedContainerRef.current?.clientHeight,
),
)
: aspectRatio >= fitAspect
? Math.floor(containerWidth / aspectRatio)
: Math.floor(containerWidth / aspectRatio) / 1.5;
const finalHeight = stretch
? scaledHeight
: Math.min(scaledHeight, height);
if (finalHeight > 0) {
return finalHeight;
}
}
return undefined;
}, [
aspectRatio,
containerWidth,
containerHeight,
fitAspect,
height,
width,
stretch,
selectedContainerRef,
]);
const scaledWidth = useMemo(() => {
if (aspectRatio && scaledHeight) {
return Math.ceil(scaledHeight * aspectRatio);
}
return undefined;
}, [scaledHeight, aspectRatio]);
useEffect(() => {
if (scaledWidth && scaledHeight) {
setDimensionsReady(true);
}
}, [scaledWidth, scaledHeight]);
useEffect(() => {
onPlayingRef.current = onPlaying;
}, [onPlaying]);
useEffect(() => {
if (!selectedContainerRef?.current || !url) {
return;
}
const videoWrapper = videoRef.current;
const canvas = canvasRef.current;
let videoElement: JSMpeg.VideoElement | null = null;
setHasData(false);
if (videoWrapper && playbackEnabled) {
// Delayed init to avoid issues with react strict mode
const initPlayer = setTimeout(() => {
videoElement = new JSMpeg.VideoElement(
videoWrapper,
url,
{ canvas: canvas },
{
protocols: [],
audio: false,
disableGl: true,
disableWebAssembly: true,
videoBufferSize: 1024 * 1024 * 4,
onVideoDecode: () => {
if (!hasDataRef.current) {
setHasData(true);
onPlayingRef.current?.();
}
},
},
);
}, 0);
return () => {
clearTimeout(initPlayer);
if (videoElement) {
try {
// this causes issues in react strict mode
// https://stackoverflow.com/questions/76822128/issue-with-cycjimmy-jsmpeg-player-in-react-18-cannot-read-properties-of-null-o
videoElement.destroy();
// eslint-disable-next-line no-empty
} catch (e) {}
}
};
}
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playbackEnabled, url]);
useEffect(() => {
setShowCanvas(hasData && dimensionsReady);
}, [hasData, dimensionsReady]);
useEffect(() => {
hasDataRef.current = hasData;
}, [hasData]);
return (
<div className={cn(className, !containerRef.current && "size-full")}>
<div
className="internal-jsmpeg-container size-full"
ref={internalContainerRef}
>
<div
ref={videoRef}
className={cn(
"jsmpeg flex h-full w-auto items-center justify-center",
!showCanvas && "hidden",
)}
>
<canvas
ref={canvasRef}
className="rounded-lg md:rounded-2xl"
style={{
width: scaledWidth,
height: scaledHeight,
}}
></canvas>
</div>
</div>
</div>
);
}