From 5177c983c844d4511386a6690b6c0261cc3feebf Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Thu, 28 Dec 2023 09:24:00 -0700 Subject: [PATCH] Automatically update camera image when detecting objects and show activity indicators --- web/src/api/ws.tsx | 21 ++++ .../components/camera/DynamicCameraImage.tsx | 109 ++++++++++++++++++ web/src/pages/Dashboard.tsx | 8 +- web/src/types/ws.ts | 34 ++++++ 4 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 web/src/components/camera/DynamicCameraImage.tsx create mode 100644 web/src/types/ws.ts diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index 3f1cd87e6..7d621f2c2 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -194,3 +194,24 @@ export function useRestart() { } = useWs("restart", "restart"); return { payload, send }; } + +export function useFrigateEvents() { + const { + value: { payload }, + } = useWs(`events`, ""); + return { payload }; +} + +export function useMotionActivity(camera: string) { + const { + value: { payload }, + } = useWs(`${camera}/motion`, ""); + return { payload }; +} + +export function useAudioActivity(camera: string) { + const { + value: { payload }, + } = useWs(`${camera}/audio/rms`, ""); + return { payload }; +} diff --git a/web/src/components/camera/DynamicCameraImage.tsx b/web/src/components/camera/DynamicCameraImage.tsx new file mode 100644 index 000000000..9cb441f7d --- /dev/null +++ b/web/src/components/camera/DynamicCameraImage.tsx @@ -0,0 +1,109 @@ +import { useCallback, useEffect, useState } from "react"; +import { AspectRatio } from "../ui/aspect-ratio"; +import CameraImage from "./CameraImage"; +import { LuEar } from "react-icons/lu"; +import { CameraConfig } from "@/types/frigateConfig"; +import { TbUserScan } from "react-icons/tb"; +import { MdLeakAdd } from "react-icons/md"; +import { useFrigateEvents, useMotionActivity } from "@/api/ws"; +import { FrigateEvent } from "@/types/ws"; + +type DynamicCameraImageProps = { + camera: CameraConfig; + aspect: number; +}; + +const INTERVAL_INACTIVE_MS = 60000; // refresh once a minute +const INTERVAL_ACTIVE_MS = 1000; // refresh once a second + +export default function DynamicCameraImage({ + camera, + aspect, +}: DynamicCameraImageProps) { + const [key, setKey] = useState(Date.now()); + const [activeObjects, setActiveObjects] = useState([]); + + const { payload: detectingMotion } = useMotionActivity(camera.name); + const { payload: event } = useFrigateEvents(); + const { payload: audioRms } = useMotionActivity(camera.name); + + useEffect(() => { + if (!event) { + return; + } + + const frigateEvent = event as unknown as FrigateEvent; + + if (frigateEvent.after.camera != camera.name) { + return; + } + + if (frigateEvent.type == "end") { + const eventIndex = activeObjects.indexOf(frigateEvent.after.id); + + if (eventIndex != -1) { + const newActiveObjects = [...activeObjects]; + newActiveObjects.splice(eventIndex, 1); + setActiveObjects(newActiveObjects); + } + } else { + if (!frigateEvent.after.stationary) { + const eventIndex = activeObjects.indexOf(frigateEvent.after.id); + + if (eventIndex == -1) { + const newActiveObjects = [...activeObjects, frigateEvent.after.id]; + setActiveObjects(newActiveObjects); + setKey(Date.now()); + } + } + } + }, [event, activeObjects]); + + const handleLoad = useCallback(() => { + const loadTime = Date.now() - key; + const loadInterval = + activeObjects.length > 0 ? INTERVAL_ACTIVE_MS : INTERVAL_INACTIVE_MS; + + setTimeout( + () => { + setKey(Date.now()); + }, + loadTime > loadInterval ? 1 : loadInterval + ); + }, [activeObjects, key]); + + return ( + + +
+ + 0 ? "text-cyan-500" : "text-gray-600" + }`} + /> + {camera.audio.enabled && ( + = camera.audio.min_volume + ? "text-orange-500" + : "text-gray-600" + }`} + /> + )} +
+
+ ); +} diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index 7ad79bb94..c8c9a4942 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -20,6 +20,7 @@ import { TbMovie } from "react-icons/tb"; import MiniEventCard from "@/components/card/MiniEventCard"; import { Event as FrigateEvent } from "@/types/event"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import DynamicCameraImage from "@/components/camera/DynamicCameraImage"; export function Dashboard() { const { data: config } = useSWR("config"); @@ -96,12 +97,7 @@ function Camera({ camera }: { camera: CameraConfig }) { <> - - - +
{camera.name.replaceAll("_", " ")} diff --git a/web/src/types/ws.ts b/web/src/types/ws.ts new file mode 100644 index 000000000..74682e86c --- /dev/null +++ b/web/src/types/ws.ts @@ -0,0 +1,34 @@ +type FrigateObjectState = { + id: string; + camera: string; + frame_time: number; + snapshot_time: number; + label: string; + sub_label: string | null; + top_score: number; + false_positive: boolean; + start_time: number; + end_time: number | null; + score: number; + box: [number, number, number, number]; + area: number; + ratio: number; + region: [number, number, number, number]; + current_zones: string[]; + entered_zones: string[]; + thumbnail: string | null; + has_snapshot: boolean; + has_clip: boolean; + stationary: boolean; + motionless_count: number; + position_changes: number; + attributes: { + [key: string]: number; + }; +}; + +export interface FrigateEvent { + type: "new" | "update" | "end"; + before: FrigateObjectState; + after: FrigateObjectState; +}