From 2236ae5d3bb5cd0cded352487460ad14918dd2fd Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 20 Dec 2023 07:34:27 -0700 Subject: [PATCH] Add jsmpeg support to new webUI and make birdseye default for live page (#8995) * Add jsmpeg and make birdseye default for live view * Fix jsmpeg * Fix --- .../components/player/BirdseyeLivePlayer.tsx | 36 +++++++ web/src/components/player/JSMpegPlayer.tsx | 94 +++++++++++++++++++ web/src/components/player/LivePlayer.tsx | 7 +- web/src/pages/Live.tsx | 20 ++-- web/src/types/frigateConfig.ts | 26 ++--- 5 files changed, 164 insertions(+), 19 deletions(-) create mode 100644 web/src/components/player/BirdseyeLivePlayer.tsx create mode 100644 web/src/components/player/JSMpegPlayer.tsx diff --git a/web/src/components/player/BirdseyeLivePlayer.tsx b/web/src/components/player/BirdseyeLivePlayer.tsx new file mode 100644 index 000000000..a43e2266a --- /dev/null +++ b/web/src/components/player/BirdseyeLivePlayer.tsx @@ -0,0 +1,36 @@ +import WebRtcPlayer from "./WebRTCPlayer"; +import { BirdseyeConfig } from "@/types/frigateConfig"; +import ActivityIndicator from "../ui/activity-indicator"; +import JSMpegPlayer from "./JSMpegPlayer"; + +type LivePlayerProps = { + birdseyeConfig: BirdseyeConfig; + liveMode: string; +}; + +export default function BirdseyeLivePlayer({ + birdseyeConfig, + liveMode, +}: LivePlayerProps) { + if (liveMode == "webrtc") { + return ( +
+ +
+ ); + } else if (liveMode == "mse") { + return
Not yet implemented
; + } else if (liveMode == "jsmpeg") { + return ( +
+ +
+ ); + } else { + ; + } +} diff --git a/web/src/components/player/JSMpegPlayer.tsx b/web/src/components/player/JSMpegPlayer.tsx new file mode 100644 index 000000000..2abf23049 --- /dev/null +++ b/web/src/components/player/JSMpegPlayer.tsx @@ -0,0 +1,94 @@ +import { baseUrl } from "@/api/baseUrl"; +import { useResizeObserver } from "@/hooks/resize-observer"; +// @ts-ignore we know this doesn't have types +import JSMpeg from "@cycjimmy/jsmpeg-player"; +import { useEffect, useMemo, useRef } from "react"; + +type JSMpegPlayerProps = { + camera: string; + width: number; + height: number; +}; + +export default function JSMpegPlayer({ + camera, + width, + height, +}: JSMpegPlayerProps) { + const url = `${baseUrl.replace(/^http/, "ws")}live/jsmpeg/${camera}`; + const playerRef = useRef(null); + const containerRef = useRef(null); + const [{ width: containerWidth }] = useResizeObserver(containerRef); + + // Add scrollbar width (when visible) to the available observer width to eliminate screen juddering. + // https://github.com/blakeblackshear/frigate/issues/1657 + let scrollBarWidth = 0; + if (window.innerWidth && document.body.offsetWidth) { + scrollBarWidth = window.innerWidth - document.body.offsetWidth; + } + const availableWidth = scrollBarWidth + ? containerWidth + scrollBarWidth + : containerWidth; + const aspectRatio = width / height; + + const scaledHeight = useMemo(() => { + const scaledHeight = Math.floor(availableWidth / aspectRatio); + const finalHeight = Math.min(scaledHeight, height); + + if (finalHeight > 0) { + return finalHeight; + } + + return 100; + }, [availableWidth, aspectRatio, height]); + const scaledWidth = useMemo( + () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), + [scaledHeight, aspectRatio, scrollBarWidth] + ); + + useEffect(() => { + if (!playerRef.current) { + return; + } + + console.log("player ref exists and creating video"); + const video = new JSMpeg.VideoElement( + playerRef.current, + url, + {}, + { protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 } + ); + + const fullscreen = () => { + if (video.els.canvas.webkitRequestFullScreen) { + video.els.canvas.webkitRequestFullScreen(); + } else { + video.els.canvas.mozRequestFullScreen(); + } + }; + + video.els.canvas.addEventListener("click", fullscreen); + + return () => { + if (playerRef.current) { + try { + video.destroy(); + } catch (e) {} + playerRef.current = null; + } + }; + }, [url]); + + return ( +
+
+
+ ); +} diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index a6e61c03c..a850d575a 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; import { Switch } from "../ui/switch"; import { Label } from "../ui/label"; import { usePersistence } from "@/hooks/use-persistence"; +import JSMpegPlayer from "./JSMpegPlayer"; const emptyObject = Object.freeze({}); @@ -66,7 +67,11 @@ export default function LivePlayer({ } else if (liveMode == "jsmpeg") { return (
- Not Yet Implemented +
); } else if (liveMode == "debug") { diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index 264bb2b93..bc7d35fe3 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -1,3 +1,4 @@ +import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer"; import LivePlayer from "@/components/player/LivePlayer"; import { Button } from "@/components/ui/button"; import { @@ -21,18 +22,19 @@ function Live() { const { camera: openedCamera } = useParams(); const [camera, setCamera] = useState( - openedCamera ?? "Select A Camera" + openedCamera ?? (config?.birdseye.enabled ? "birdseye" : "Select A Camera") ); const cameraConfig = useMemo(() => { - return config?.cameras[camera]; + return camera == "birdseye" ? undefined : config?.cameras[camera]; }, [camera, config]); const sortedCameras = useMemo(() => { if (!config) { return []; } - return Object.values(config.cameras) - .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); + return Object.values(config.cameras).sort( + (aConf, bConf) => aConf.ui.order - bConf.ui.order + ); }, [config]); const restreamEnabled = useMemo(() => { return ( @@ -56,7 +58,7 @@ function Live() { }, [cameraConfig, restreamEnabled]); const [viewSource, setViewSource, sourceIsLoaded] = usePersistence( `${camera}-source`, - defaultLiveMode + camera == "birdseye" ? "jsmpeg" : defaultLiveMode ); return ( @@ -74,7 +76,7 @@ function Live() { Select A Camera - {(sortedCameras).map((item) => ( + {sortedCameras.map((item) => (
+ {config && camera == "birdseye" && sourceIsLoaded && ( + + )} {cameraConfig && sourceIsLoaded && (