From 60ef4c2fd58e1fc7f4ce0f0fdb102b50d938f0f8 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 7 Feb 2024 09:29:13 -0700 Subject: [PATCH 1/9] Don't force heights --- web/src/pages/Live.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index f8896c021..f2776e70b 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -77,15 +77,15 @@ function Live() { )} -
+
{cameras.map((camera) => { let grow; if (camera.detect.width / camera.detect.height > 2) { - grow = "h-[424px] col-span-2"; + grow = "md:col-span-2"; } else if (camera.detect.width / camera.detect.height < 1) { - grow = "h-[840px] row-span-2"; + grow = "md:row-span-2"; } else { - grow = "h-[425px]"; + grow = "aspect-video"; } return ( Date: Wed, 7 Feb 2024 09:34:56 -0700 Subject: [PATCH 2/9] Adjust scaling --- web/src/pages/Live.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index f2776e70b..8c0fb0a3b 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -77,7 +77,7 @@ function Live() { )} -
+
{cameras.map((camera) => { let grow; if (camera.detect.width / camera.detect.height > 2) { From da95d351926a979d0b66bc6c27a39778911f940c Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 7 Feb 2024 09:42:34 -0700 Subject: [PATCH 3/9] Cleanup --- web/src/pages/Live.tsx | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index 8c0fb0a3b..8612bb61f 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -22,29 +22,26 @@ function Live() { const date = new Date(); date.setHours(date.getHours() - 4); setRecentCutoff(date.getTime() / 1000); - }, 30000); + }, 60000); return () => clearInterval(intervalId); - }, [30000]); + }, [60000]); const { data: events, mutate: updateEvents } = useSWR([ "events", { limit: 10, after: recentCutoff }, ]); - const onFavorite = useCallback( - async (e: Event, event: FrigateEvent) => { - e.stopPropagation(); - let response; - if (!event.retain_indefinitely) { - response = await axios.post(`events/${event.id}/retain`); - } else { - response = await axios.delete(`events/${event.id}/retain`); - } - if (response.status === 200) { - updateEvents(); - } - }, - [event] - ); + const onFavorite = useCallback(async (e: Event, event: FrigateEvent) => { + e.stopPropagation(); + let response; + if (!event.retain_indefinitely) { + response = await axios.post(`events/${event.id}/retain`); + } else { + response = await axios.delete(`events/${event.id}/retain`); + } + if (response.status === 200) { + updateEvents(); + } + }, []); // camera live views From dad0e1b39a59935dad353a4a386dc3a6de3e6af1 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 7 Feb 2024 11:30:54 -0600 Subject: [PATCH 4/9] remove sidebar (#9731) * remove sidebar * keep sidebar on mobile for now and add icons --- web/src/components/Header.tsx | 65 +++++++++++++++++++++++++++++++--- web/src/components/Sidebar.tsx | 2 +- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 6fff81e44..eb5df6f25 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -8,7 +8,6 @@ import { LuList, LuMenu, LuMoon, - LuMoreVertical, LuPenSquare, LuRotateCw, LuSettings, @@ -18,7 +17,7 @@ import { import { IoColorPalette } from "react-icons/io5"; import { CgDarkMode } from "react-icons/cg"; import { Button } from "@/components/ui/button"; -import Heading from "./ui/heading"; +import { VscAccount } from "react-icons/vsc"; import { DropdownMenu, DropdownMenuContent, @@ -57,11 +56,66 @@ import { import ActivityIndicator from "./ui/activity-indicator"; import { useRestart } from "@/api/ws"; import { ENV } from "@/env"; +import { NavLink } from "react-router-dom"; type HeaderProps = { onToggleNavbar: () => void; }; +function HeaderNavigation() { + const navbarLinks = [ + { + id: 1, + title: "Dashboard", + url: "/", + }, + { + id: 2, + title: "Live", + url: "/live", + }, + { + id: 3, + title: "History", + url: "/history", + }, + { + id: 4, + title: "Export", + url: "/export", + }, + { + id: 5, + title: "UI Playground", + url: "/playground", + dev: true, + }, + ]; + + return ( +
+ {navbarLinks.map((item) => { + let shouldRender = item.dev ? ENV !== "production" : true; + return ( + shouldRender && ( + + `py-4 px-2 flex flex-row items-center text-center rounded-lg gap-2 hover:bg-border ${ + isActive ? "font-bold bg-popover text-popover-foreground" : "" + }` + } + > +
{item.title}
+
+ ) + ); + })} +
+ ); +} + function Header({ onToggleNavbar }: HeaderProps) { const { theme, colorScheme, setTheme, setColorScheme } = useTheme(); const [restartDialogOpen, setRestartDialogOpen] = useState(false); @@ -110,7 +164,6 @@ function Header({ onToggleNavbar }: HeaderProps) {
- Frigate
{ENV == "production" && (
@@ -118,12 +171,13 @@ function Header({ onToggleNavbar }: HeaderProps) {
)} +
@@ -257,6 +311,9 @@ function Header({ onToggleNavbar }: HeaderProps) { +
{restartDialogOpen && ( -
{sidebar}
+
{sidebar}
Date: Wed, 7 Feb 2024 10:31:19 -0700 Subject: [PATCH 5/9] Fix revalidation --- web/src/pages/Live.tsx | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index 8612bb61f..97f141e61 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -12,23 +12,21 @@ function Live() { // recent events - const [recentCutoff, setRecentCutoff] = useState(0); - useEffect(() => { + const { data: allEvents, mutate: updateEvents } = useSWR( + ["events", { limit: 10 }], + { revalidateOnFocus: false, refreshInterval: 60000 } + ); + + const events = useMemo(() => { + if (!allEvents) { + return []; + } + const date = new Date(); date.setHours(date.getHours() - 4); - setRecentCutoff(date.getTime() / 1000); - - const intervalId: NodeJS.Timeout = setInterval(() => { - const date = new Date(); - date.setHours(date.getHours() - 4); - setRecentCutoff(date.getTime() / 1000); - }, 60000); - return () => clearInterval(intervalId); - }, [60000]); - const { data: events, mutate: updateEvents } = useSWR([ - "events", - { limit: 10, after: recentCutoff }, - ]); + const cutoff = date.getTime() / 1000; + return allEvents.filter((event) => event.start_time > cutoff); + }, [allEvents]); const onFavorite = useCallback(async (e: Event, event: FrigateEvent) => { e.stopPropagation(); From 321eec77bceb0d48b1fb60432fd4e3753d7907f7 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 7 Feb 2024 10:34:57 -0700 Subject: [PATCH 6/9] Cleanup --- web/src/pages/Live.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index 97f141e61..24899fb5e 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -4,7 +4,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { Event as FrigateEvent } from "@/types/event"; import { FrigateConfig } from "@/types/frigateConfig"; import axios from "axios"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo } from "react"; import useSWR from "swr"; function Live() { @@ -23,7 +23,7 @@ function Live() { } const date = new Date(); - date.setHours(date.getHours() - 4); + date.setHours(date.getHours() - 1); const cutoff = date.getTime() / 1000; return allEvents.filter((event) => event.start_time > cutoff); }, [allEvents]); From ae51db8ffe17f433ce30c6fe5252950d21e0ec16 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 7 Feb 2024 10:37:01 -0700 Subject: [PATCH 7/9] Cleanup width --- web/src/App.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 15ad1e2be..231c2f285 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -30,10 +30,7 @@ function App() {
-
+
} /> } /> From 5976de8324871bee6aa042c81f0126a13c52232f Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 7 Feb 2024 12:13:36 -0700 Subject: [PATCH 8/9] Add chips for activity on cameras --- web/src/api/ws.tsx | 2 +- .../components/camera/DynamicCameraImage.tsx | 2 +- web/src/components/player/LivePlayer.tsx | 40 ++++++++++-- web/src/hooks/use-camera-activity.ts | 64 +++++++++++++++++++ web/src/pages/Live.tsx | 1 + 5 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 web/src/hooks/use-camera-activity.ts 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 /> ); })} From 474bdbca65621b7c83be2160778524bbd11af886 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 7 Feb 2024 12:23:37 -0700 Subject: [PATCH 9/9] Remove dashboard from header --- web/src/App.tsx | 4 +--- web/src/components/Header.tsx | 11 +++-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 231c2f285..377d22493 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -4,7 +4,6 @@ import { useState } from "react"; import Wrapper from "@/components/Wrapper"; import Sidebar from "@/components/Sidebar"; import Header from "@/components/Header"; -import Dashboard from "@/pages/Dashboard"; import Live from "@/pages/Live"; import History from "@/pages/History"; import Export from "@/pages/Export"; @@ -32,8 +31,7 @@ function App() {
- } /> - } /> + } /> } /> } /> } /> diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index eb5df6f25..c84e6afd6 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -66,26 +66,21 @@ function HeaderNavigation() { const navbarLinks = [ { id: 1, - title: "Dashboard", + title: "Live", url: "/", }, { id: 2, - title: "Live", - url: "/live", - }, - { - id: 3, title: "History", url: "/history", }, { - id: 4, + id: 3, title: "Export", url: "/export", }, { - id: 5, + id: 4, title: "UI Playground", url: "/playground", dev: true,