From 750502c629da3e7460197aed3be194044643d76f Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 1 Mar 2024 10:06:01 -0700 Subject: [PATCH] Get live camera view working --- web/src/components/player/LivePlayer.tsx | 11 +- web/src/pages/Live.tsx | 129 +++----------------- web/src/views/live/LiveCameraView.tsx | 49 ++++++++ web/src/views/live/LiveDashboardView.tsx | 143 +++++++++++++++++++++++ 4 files changed, 215 insertions(+), 117 deletions(-) create mode 100644 web/src/views/live/LiveCameraView.tsx create mode 100644 web/src/views/live/LiveDashboardView.tsx diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index 769028788..b890bd05f 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -20,6 +20,7 @@ type LivePlayerProps = { preferredLiveMode?: LivePlayerMode; showStillWithoutActivity?: boolean; windowVisible?: boolean; + onClick?: () => void; }; export default function LivePlayer({ @@ -28,6 +29,7 @@ export default function LivePlayer({ preferredLiveMode, showStillWithoutActivity = true, windowVisible = true, + onClick, }: LivePlayerProps) { // camera activity @@ -35,8 +37,10 @@ export default function LivePlayer({ useCameraActivity(cameraConfig); const cameraActive = useMemo( - () => windowVisible && (activeMotion || activeTracking), - [activeMotion, activeTracking, windowVisible], + () => + !showStillWithoutActivity || + (windowVisible && (activeMotion || activeTracking)), + [activeMotion, activeTracking, showStillWithoutActivity, windowVisible], ); // camera live state @@ -127,11 +131,12 @@ export default function LivePlayer({ return (
diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index 8e3d58193..ba0811ecc 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -5,9 +5,12 @@ import LivePlayer from "@/components/player/LivePlayer"; import { Button } from "@/components/ui/button"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { TooltipProvider } from "@/components/ui/tooltip"; +import useOverlayState from "@/hooks/use-overlay-state"; import { usePersistence } from "@/hooks/use-persistence"; import { FrigateConfig } from "@/types/frigateConfig"; import { ReviewSegment } from "@/types/review"; +import LiveCameraView from "@/views/live/LiveCameraView"; +import LiveDashboardView from "@/views/live/LiveDashboardView"; import { useCallback, useEffect, useMemo, useState } from "react"; import { isDesktop, isMobile, isSafari } from "react-device-detect"; import { CiGrid2H, CiGrid31 } from "react-icons/ci"; @@ -15,45 +18,7 @@ import useSWR from "swr"; function Live() { const { data: config } = useSWR("config"); - - // layout - - const [layout, setLayout] = usePersistence<"grid" | "list">( - "live-layout", - isDesktop ? "grid" : "list", - ); - - // recent events - const { payload: eventUpdate } = useFrigateReviews(); - const { data: allEvents, mutate: updateEvents } = useSWR([ - "review", - { limit: 10, severity: "alert" }, - ]); - - useEffect(() => { - if (!eventUpdate) { - return; - } - - // if event is ended and was saved, update events list - if (eventUpdate.type == "end" && eventUpdate.review.severity == "alert") { - updateEvents(); - return; - } - }, [eventUpdate, updateEvents]); - - const events = useMemo(() => { - if (!allEvents) { - return []; - } - - const date = new Date(); - date.setHours(date.getHours() - 1); - const cutoff = date.getTime() / 1000; - return allEvents.filter((event) => event.start_time > cutoff); - }, [allEvents]); - - // camera live views + const [selectedCameraName, setSelectedCameraName] = useOverlayState("camera"); const cameras = useMemo(() => { if (!config) { @@ -65,84 +30,20 @@ function Live() { .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); }, [config]); - const [windowVisible, setWindowVisible] = useState(true); - const visibilityListener = useCallback(() => { - setWindowVisible(document.visibilityState == "visible"); - }, []); + const selectedCamera = useMemo( + () => cameras.find((cam) => cam.name == selectedCameraName), + [cameras, selectedCameraName], + ); - useEffect(() => { - addEventListener("visibilitychange", visibilityListener); - - return () => { - removeEventListener("visibilitychange", visibilityListener); - }; - }, [visibilityListener]); + if (selectedCamera) { + return ; + } return ( -
- {isMobile && ( -
- -
-
- - -
-
- )} - - {events && events.length > 0 && ( - - -
- {events.map((event) => { - return ; - })} -
-
- -
- )} - -
- {cameras.map((camera) => { - let grow; - const aspectRatio = camera.detect.width / camera.detect.height; - if (aspectRatio > 2) { - grow = `${layout == "grid" ? "col-span-2" : ""} aspect-wide`; - } else if (aspectRatio < 1) { - grow = `${layout == "grid" ? "row-span-2 aspect-tall md:h-full" : ""} aspect-tall`; - } else { - grow = "aspect-video"; - } - return ( - - ); - })} -
-
+ ); } diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx new file mode 100644 index 000000000..e5055b93d --- /dev/null +++ b/web/src/views/live/LiveCameraView.tsx @@ -0,0 +1,49 @@ +import LivePlayer from "@/components/player/LivePlayer"; +import { Button } from "@/components/ui/button"; +import { CameraConfig } from "@/types/frigateConfig"; +import { useMemo } from "react"; +import { isSafari } from "react-device-detect"; +import { IoMdArrowBack } from "react-icons/io"; +import { useNavigate } from "react-router-dom"; + +type LiveCameraViewProps = { + camera: CameraConfig; +}; +export default function LiveCameraView({ camera }: LiveCameraViewProps) { + const navigate = useNavigate(); + + const growClassName = useMemo(() => { + if (camera.detect.width / camera.detect.height > 2) { + return "absolute left-2 right-2 top-[50%] -translate-y-[50%]"; + } else { + return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]"; + } + }, [camera]); + + return ( +
+
+ +
+ +
+
+ +
+
+
+ ); +} diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx new file mode 100644 index 000000000..ba60fadcf --- /dev/null +++ b/web/src/views/live/LiveDashboardView.tsx @@ -0,0 +1,143 @@ +import { useFrigateReviews } from "@/api/ws"; +import Logo from "@/components/Logo"; +import { AnimatedEventThumbnail } from "@/components/image/AnimatedEventThumbnail"; +import LivePlayer from "@/components/player/LivePlayer"; +import { Button } from "@/components/ui/button"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { usePersistence } from "@/hooks/use-persistence"; +import { CameraConfig } from "@/types/frigateConfig"; +import { ReviewSegment } from "@/types/review"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { isDesktop, isMobile, isSafari } from "react-device-detect"; +import { CiGrid2H, CiGrid31 } from "react-icons/ci"; +import useSWR from "swr"; + +type LiveDashboardViewProps = { + cameras: CameraConfig[]; + onSelectCamera: (camera: string) => void; +}; +export default function LiveDashboardView({ + cameras, + onSelectCamera, +}: LiveDashboardViewProps) { + // layout + + const [layout, setLayout] = usePersistence<"grid" | "list">( + "live-layout", + isDesktop ? "grid" : "list", + ); + + // recent events + const { payload: eventUpdate } = useFrigateReviews(); + const { data: allEvents, mutate: updateEvents } = useSWR([ + "review", + { limit: 10, severity: "alert" }, + ]); + + useEffect(() => { + if (!eventUpdate) { + return; + } + + // if event is ended and was saved, update events list + if (eventUpdate.type == "end" && eventUpdate.review.severity == "alert") { + updateEvents(); + return; + } + }, [eventUpdate, updateEvents]); + + const events = useMemo(() => { + if (!allEvents) { + return []; + } + + const date = new Date(); + date.setHours(date.getHours() - 1); + const cutoff = date.getTime() / 1000; + return allEvents.filter((event) => event.start_time > cutoff); + }, [allEvents]); + + // camera live views + + const [windowVisible, setWindowVisible] = useState(true); + const visibilityListener = useCallback(() => { + setWindowVisible(document.visibilityState == "visible"); + }, []); + + useEffect(() => { + addEventListener("visibilitychange", visibilityListener); + + return () => { + removeEventListener("visibilitychange", visibilityListener); + }; + }, [visibilityListener]); + + return ( +
+ {isMobile && ( +
+ +
+
+ + +
+
+ )} + + {events && events.length > 0 && ( + + +
+ {events.map((event) => { + return ; + })} +
+
+ +
+ )} + +
+ {cameras.map((camera) => { + let grow; + const aspectRatio = camera.detect.width / camera.detect.height; + if (aspectRatio > 2) { + grow = `${layout == "grid" ? "col-span-2" : ""} aspect-wide`; + } else if (aspectRatio < 1) { + grow = `${layout == "grid" ? "row-span-2 aspect-tall md:h-full" : ""} aspect-tall`; + } else { + grow = "aspect-video"; + } + return ( + onSelectCamera(camera.name)} + /> + ); + })} +
+
+ ); +}