diff --git a/web/src/components/player/BirdseyeLivePlayer.tsx b/web/src/components/player/BirdseyeLivePlayer.tsx index 5157a6ed6..10d578115 100644 --- a/web/src/components/player/BirdseyeLivePlayer.tsx +++ b/web/src/components/player/BirdseyeLivePlayer.tsx @@ -3,15 +3,20 @@ import { BirdseyeConfig } from "@/types/frigateConfig"; import ActivityIndicator from "../indicators/activity-indicator"; import JSMpegPlayer from "./JSMpegPlayer"; import MSEPlayer from "./MsePlayer"; +import { LivePlayerMode } from "@/types/live"; type LivePlayerProps = { + className?: string; birdseyeConfig: BirdseyeConfig; - liveMode: string; + liveMode: LivePlayerMode; + onClick?: () => void; }; export default function BirdseyeLivePlayer({ + className, birdseyeConfig, liveMode, + onClick, }: LivePlayerProps) { let player; if (liveMode == "webrtc") { @@ -45,7 +50,10 @@ export default function BirdseyeLivePlayer({ } return ( -
+
{player}
diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index 6787f9143..c29fbed3b 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -3,6 +3,7 @@ import { usePersistedOverlayState, } from "@/hooks/use-overlay-state"; import { FrigateConfig } from "@/types/frigateConfig"; +import LiveBirdseyeView from "@/views/live/LiveBirdseyeView"; import LiveCameraView from "@/views/live/LiveCameraView"; import LiveDashboardView from "@/views/live/LiveDashboardView"; import { useMemo } from "react"; @@ -47,6 +48,10 @@ function Live() { [cameras, selectedCameraName], ); + if (selectedCameraName == "birdseye") { + return ; + } + if (selectedCamera) { return ; } diff --git a/web/src/views/live/LiveBirdseyeView.tsx b/web/src/views/live/LiveBirdseyeView.tsx new file mode 100644 index 000000000..58655e76c --- /dev/null +++ b/web/src/views/live/LiveBirdseyeView.tsx @@ -0,0 +1,189 @@ +import CameraFeatureToggle from "@/components/dynamic/CameraFeatureToggle"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer"; +import { Button } from "@/components/ui/button"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { useResizeObserver } from "@/hooks/resize-observer"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { + isDesktop, + isMobile, + isSafari, + useMobileOrientation, +} from "react-device-detect"; +import { FaCompress, FaExpand } from "react-icons/fa"; +import { IoMdArrowBack } from "react-icons/io"; +import { useNavigate } from "react-router-dom"; +import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; +import useSWR from "swr"; + +export default function LiveBirdseyeView() { + const { data: config } = useSWR("config"); + const navigate = useNavigate(); + const { isPortrait } = useMobileOrientation(); + const mainRef = useRef(null); + const [{ width: windowWidth, height: windowHeight }] = + useResizeObserver(window); + + // fullscreen state + + useEffect(() => { + if (mainRef.current == null) { + return; + } + + const listener = () => { + setFullscreen(document.fullscreenElement != null); + }; + document.addEventListener("fullscreenchange", listener); + + return () => { + document.removeEventListener("fullscreenchange", listener); + }; + }, [mainRef]); + + // playback state + + const [fullscreen, setFullscreen] = useState(false); + + const cameraAspectRatio = useMemo(() => { + if (!config) { + return 16 / 9; + } + + return config.birdseye.width / config.birdseye.height; + }, [config]); + + const growClassName = useMemo(() => { + if (isMobile) { + if (isPortrait) { + return "absolute left-2 right-2 top-[50%] -translate-y-[50%]"; + } else { + if (cameraAspectRatio > 16 / 9) { + return "absolute left-0 top-[50%] -translate-y-[50%]"; + } else { + return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]"; + } + } + } + + if (fullscreen) { + if (cameraAspectRatio > 16 / 9) { + return "absolute inset-x-2 top-[50%] -translate-y-[50%]"; + } else { + return "absolute inset-y-2 left-[50%] -translate-x-[50%]"; + } + } else { + return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]"; + } + }, [cameraAspectRatio, fullscreen, isPortrait]); + + const preferredLiveMode = useMemo(() => { + if (!config || !config.birdseye.restream) { + return "jsmpeg"; + } + + if (isSafari) { + return "webrtc"; + } + + return "mse"; + }, [config]); + + const windowAspectRatio = useMemo(() => { + return windowWidth / windowHeight; + }, [windowWidth, windowHeight]); + + const aspectRatio = useMemo(() => { + if (isMobile || fullscreen) { + return cameraAspectRatio; + } else { + return windowAspectRatio < cameraAspectRatio + ? windowAspectRatio - 0.05 + : cameraAspectRatio - 0.03; + } + }, [cameraAspectRatio, windowAspectRatio, fullscreen]); + + if (!config) { + return ; + } + + return ( + +
+
+ {!fullscreen ? ( + + ) : ( +
+ )} + +
+ { + if (fullscreen) { + document.exitFullscreen(); + } else { + mainRef.current?.requestFullscreen(); + } + }} + /> +
+
+
+ +
+ +
+
+
+ + ); +} diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index 013621350..071a9d71e 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -134,6 +134,7 @@ export default function LiveDashboardView({ onSelectCamera("birdseye")} /> )} {cameras.map((camera) => {