diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index eab7206d1..9245afbb2 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -228,7 +228,7 @@ export function useMotionActivity(camera: string): { payload: string } { return { payload }; } -export function useAudioActivity(camera: string): { payload: string } { +export function useAudioActivity(camera: string): { payload: number } { const { value: { payload }, } = useWs(`${camera}/audio/rms`, ""); diff --git a/web/src/components/camera/DynamicCameraImage.tsx b/web/src/components/camera/DynamicCameraImage.tsx index 0ef249964..1f15f595b 100644 --- a/web/src/components/camera/DynamicCameraImage.tsx +++ b/web/src/components/camera/DynamicCameraImage.tsx @@ -108,7 +108,7 @@ export default function DynamicCameraImage({ {camera.audio.enabled_in_config && ( = camera.audio.min_volume + audioRms >= camera.audio.min_volume ? "text-audio" : "text-gray-600" }`} diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index e8687ec7c..f29baa7ba 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -11,8 +11,10 @@ import { Label } from "../ui/label"; import { usePersistence } from "@/hooks/use-persistence"; import MSEPlayer from "./MsePlayer"; import JSMpegPlayer from "./JSMpegPlayer"; -import { MdCircle } from "react-icons/md"; +import { MdCircle, MdLeakAdd, MdSelectAll } from "react-icons/md"; +import { BsSoundwave } from "react-icons/bs"; import Chip from "../Chip"; +import useCameraActivity from "@/hooks/use-camera-activity"; const emptyObject = Object.freeze({}); @@ -20,6 +22,7 @@ type LivePlayerProps = { className?: string; cameraConfig: CameraConfig; liveMode?: "webrtc" | "mse" | "jsmpeg" | "debug"; + liveChips?: boolean; }; type Options = { [key: string]: boolean }; @@ -28,14 +31,19 @@ export default function LivePlayer({ className, cameraConfig, liveMode = "mse", + liveChips = false, }: LivePlayerProps) { - const [showSettings, setShowSettings] = useState(false); + // camera activity + const { activeMotion, activeAudio, activeTracking } = + useCameraActivity(cameraConfig); + // debug view settings + + const [showSettings, setShowSettings] = useState(false); const [options, setOptions] = usePersistence( `${cameraConfig?.name}-feed`, emptyObject ); - const handleSetOption = useCallback( (id: string, value: boolean) => { const newOptions = { ...options, [id]: value }; @@ -43,7 +51,6 @@ export default function LivePlayer({ }, [options, setOptions] ); - const searchParams = useMemo( () => new URLSearchParams( @@ -55,7 +62,6 @@ export default function LivePlayer({ ), [options] ); - const handleToggleSettings = useCallback(() => { setShowSettings(!showSettings); }, [showSettings, setShowSettings]); @@ -126,6 +132,30 @@ export default function LivePlayer({ return (
{player} +
+ + +
Motion
+
+ {cameraConfig.audio.enabled_in_config && ( + + +
Sound
+
+ )} + + +
Tracking
+
+
diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts new file mode 100644 index 000000000..1493d6518 --- /dev/null +++ b/web/src/hooks/use-camera-activity.ts @@ -0,0 +1,64 @@ +import { + useAudioActivity, + useFrigateEvents, + useMotionActivity, +} from "@/api/ws"; +import { CameraConfig } from "@/types/frigateConfig"; +import { useEffect, useMemo, useState } from "react"; + +type useCameraActivityReturn = { + activeTracking: boolean; + activeMotion: boolean; + activeAudio: boolean; +}; + +export default function useCameraActivity( + camera: CameraConfig +): useCameraActivityReturn { + const [activeObjects, setActiveObjects] = useState([]); + const hasActiveObjects = useMemo( + () => activeObjects.length > 0, + [activeObjects] + ); + + const { payload: detectingMotion } = useMotionActivity(camera.name); + const { payload: event } = useFrigateEvents(); + const { payload: audioRms } = useAudioActivity(camera.name); + + useEffect(() => { + if (!event) { + return; + } + + if (event.after.camera != camera.name) { + return; + } + + if (event.type == "end") { + const eventIndex = activeObjects.indexOf(event.after.id); + + if (eventIndex != -1) { + const newActiveObjects = [...activeObjects]; + newActiveObjects.splice(eventIndex, 1); + setActiveObjects(newActiveObjects); + } + } else { + if (!event.after.stationary) { + const eventIndex = activeObjects.indexOf(event.after.id); + + if (eventIndex == -1) { + const newActiveObjects = [...activeObjects, event.after.id]; + setActiveObjects(newActiveObjects); + } + } + } + }, [event, activeObjects]); + + return { + activeTracking: hasActiveObjects, + activeMotion: detectingMotion == "ON", + activeAudio: camera.audio.enabled_in_config + ? audioRms >= camera.audio.min_volume + : false, + }; +} diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index 24899fb5e..0a3aa1cdd 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -87,6 +87,7 @@ function Live() { key={camera.name} className={`rounded-2xl bg-black ${grow}`} cameraConfig={camera} + liveChips /> ); })}