From acadfb69593d778dde423d33f04614251327b4c2 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 21 Apr 2024 20:03:43 -0500 Subject: [PATCH 1/7] Fix array out of range error in reviews (#11059) --- web/src/views/events/EventView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 6c6bd2dec..015507982 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -589,7 +589,8 @@ function DetectionReview({ ); }) - : Array(itemsToReview) + : (itemsToReview ?? 0) > 0 && + Array(itemsToReview) .fill(0) .map((_, idx) => ( From ba3930ab02913c3d9368efe9b084d0788c3bdf6b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:20:23 -0500 Subject: [PATCH 2/7] Add status bar provider (#11066) --- web/package-lock.json | 11 ++- web/package.json | 2 + web/src/components/Statusbar.tsx | 44 ++++++++-- web/src/components/navigation/Bottombar.tsx | 37 +++++++-- web/src/components/settings/MasksAndZones.tsx | 15 +++- web/src/components/settings/MotionTuner.tsx | 16 +++- web/src/context/providers.tsx | 3 +- web/src/context/statusbar-provider.tsx | 83 +++++++++++++++++++ web/src/hooks/use-deep-memo.ts | 12 +++ web/src/hooks/use-stats.ts | 30 ++++--- web/src/utils/stringUtil.ts | 3 + 11 files changed, 218 insertions(+), 38 deletions(-) create mode 100644 web/src/context/statusbar-provider.tsx create mode 100644 web/src/hooks/use-deep-memo.ts create mode 100644 web/src/utils/stringUtil.ts diff --git a/web/package-lock.json b/web/package-lock.json index d11e354ff..122c26570 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -39,6 +39,7 @@ "idb-keyval": "^6.2.1", "immer": "^10.0.4", "konva": "^9.3.6", + "lodash": "^4.17.21", "lucide-react": "^0.372.0", "monaco-yaml": "^5.1.1", "next-themes": "^0.3.0", @@ -71,6 +72,7 @@ "devDependencies": { "@tailwindcss/forms": "^0.5.7", "@testing-library/jest-dom": "^6.1.5", + "@types/lodash": "^4.17.0", "@types/node": "^20.12.7", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", @@ -2523,6 +2525,12 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "node_modules/@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", + "dev": true + }, "node_modules/@types/mute-stream": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", @@ -5299,8 +5307,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.merge": { "version": "4.6.2", diff --git a/web/package.json b/web/package.json index 9ffeace0a..3e8fc2907 100644 --- a/web/package.json +++ b/web/package.json @@ -44,6 +44,7 @@ "idb-keyval": "^6.2.1", "immer": "^10.0.4", "konva": "^9.3.6", + "lodash": "^4.17.21", "lucide-react": "^0.372.0", "monaco-yaml": "^5.1.1", "next-themes": "^0.3.0", @@ -76,6 +77,7 @@ "devDependencies": { "@tailwindcss/forms": "^0.5.7", "@testing-library/jest-dom": "^6.1.5", + "@types/lodash": "^4.17.0", "@types/node": "^20.12.7", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx index 8599ab9b6..88d00d3b7 100644 --- a/web/src/components/Statusbar.tsx +++ b/web/src/components/Statusbar.tsx @@ -1,7 +1,12 @@ import { useFrigateStats } from "@/api/ws"; +import { + StatusBarMessagesContext, + StatusMessage, +} from "@/context/statusbar-provider"; import useStats from "@/hooks/use-stats"; import { FrigateStats } from "@/types/stats"; -import { useMemo } from "react"; +import { useContext, useEffect, useMemo } from "react"; +import { FaCheck } from "react-icons/fa"; import { IoIosWarning } from "react-icons/io"; import { MdCircle } from "react-icons/md"; import useSWR from "swr"; @@ -11,6 +16,10 @@ export default function Statusbar() { revalidateOnFocus: false, }); const { payload: latestStats } = useFrigateStats(); + const { messages, addMessage, clearMessages } = useContext( + StatusBarMessagesContext, + )!; + const stats = useMemo(() => { if (latestStats) { return latestStats; @@ -31,6 +40,13 @@ export default function Statusbar() { const { potentialProblems } = useStats(stats); + useEffect(() => { + clearMessages("stats"); + potentialProblems.forEach((problem) => { + addMessage("stats", problem.text, problem.color); + }); + }, [potentialProblems, addMessage, clearMessages]); + return (
@@ -86,15 +102,25 @@ export default function Statusbar() { })}
- {potentialProblems.map((prob) => ( -
- - {prob.text} + {Object.entries(messages).length === 0 ? ( +
+ + System is healthy
- ))} + ) : ( + Object.entries(messages).map(([key, messageArray]) => ( +
+ {messageArray.map(({ id, text, color }: StatusMessage) => ( +
+ + {text} +
+ ))} +
+ )) + )}
); diff --git a/web/src/components/navigation/Bottombar.tsx b/web/src/components/navigation/Bottombar.tsx index e21556aaa..38086892e 100644 --- a/web/src/components/navigation/Bottombar.tsx +++ b/web/src/components/navigation/Bottombar.tsx @@ -4,11 +4,15 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import useSWR from "swr"; import { FrigateStats } from "@/types/stats"; import { useFrigateStats } from "@/api/ws"; -import { useMemo } from "react"; +import { useContext, useEffect, useMemo } from "react"; import useStats from "@/hooks/use-stats"; import GeneralSettings from "../menu/GeneralSettings"; import AccountSettings from "../menu/AccountSettings"; import useNavigation from "@/hooks/use-navigation"; +import { + StatusBarMessagesContext, + StatusMessage, +} from "@/context/statusbar-provider"; function Bottombar() { const navItems = useNavigation("secondary"); @@ -30,6 +34,11 @@ function StatusAlertNav() { revalidateOnFocus: false, }); const { payload: latestStats } = useFrigateStats(); + + const { messages, addMessage, clearMessages } = useContext( + StatusBarMessagesContext, + )!; + const stats = useMemo(() => { if (latestStats) { return latestStats; @@ -39,7 +48,14 @@ function StatusAlertNav() { }, [initialStats, latestStats]); const { potentialProblems } = useStats(stats); - if (!potentialProblems || potentialProblems.length == 0) { + useEffect(() => { + clearMessages("stats"); + potentialProblems.forEach((problem) => { + addMessage("stats", problem.text, problem.color); + }); + }, [potentialProblems, addMessage, clearMessages]); + + if (!messages || Object.keys(messages).length === 0) { return; } @@ -50,13 +66,16 @@ function StatusAlertNav() {
- {potentialProblems.map((prob) => ( -
- - {prob.text} + {Object.entries(messages).map(([key, messageArray]) => ( +
+ {messageArray.map(({ id, text, color }: StatusMessage) => ( +
+ + {text} +
+ ))}
))}
diff --git a/web/src/components/settings/MasksAndZones.tsx b/web/src/components/settings/MasksAndZones.tsx index 381b1d952..84999da8f 100644 --- a/web/src/components/settings/MasksAndZones.tsx +++ b/web/src/components/settings/MasksAndZones.tsx @@ -1,7 +1,14 @@ import { FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import ActivityIndicator from "@/components/indicators/activity-indicator"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { PolygonCanvas } from "./PolygonCanvas"; import { Polygon, PolygonType } from "@/types/canvas"; import { interpolatePoints, parseCoordinates } from "@/utils/canvasUtil"; @@ -25,6 +32,7 @@ import ObjectMaskEditPane from "./ObjectMaskEditPane"; import PolygonItem from "./PolygonItem"; import { Link } from "react-router-dom"; import { isDesktop } from "react-device-detect"; +import { StatusBarMessagesContext } from "@/context/statusbar-provider"; type MasksAndZoneProps = { selectedCamera: string; @@ -50,6 +58,8 @@ export default function MasksAndZones({ const containerRef = useRef(null); const [editPane, setEditPane] = useState(undefined); + const { addMessage } = useContext(StatusBarMessagesContext)!; + const cameraConfig = useMemo(() => { if (config && selectedCamera) { return config.cameras[selectedCamera]; @@ -167,7 +177,8 @@ export default function MasksAndZones({ setAllPolygons([...(editingPolygons ?? [])]); setHoveredPolygonIndex(null); setUnsavedChanges(false); - }, [editingPolygons, setUnsavedChanges]); + addMessage("masks_zones", "Restart required (masks/zones changed)"); + }, [editingPolygons, setUnsavedChanges, addMessage]); useEffect(() => { if (isLoading) { diff --git a/web/src/components/settings/MotionTuner.tsx b/web/src/components/settings/MotionTuner.tsx index 584eab83e..7bcd73428 100644 --- a/web/src/components/settings/MotionTuner.tsx +++ b/web/src/components/settings/MotionTuner.tsx @@ -4,7 +4,7 @@ import useSWR from "swr"; import axios from "axios"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { Slider } from "@/components/ui/slider"; import { Label } from "@/components/ui/label"; import { @@ -20,6 +20,7 @@ import { toast } from "sonner"; import { Separator } from "../ui/separator"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; +import { StatusBarMessagesContext } from "@/context/statusbar-provider"; type MotionTunerProps = { selectedCamera: string; @@ -41,6 +42,8 @@ export default function MotionTuner({ const [changedValue, setChangedValue] = useState(false); const [isLoading, setIsLoading] = useState(false); + const { addMessage, clearMessages } = useContext(StatusBarMessagesContext)!; + const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera); const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera); const { send: sendImproveContrast } = useImproveContrast(selectedCamera); @@ -145,7 +148,16 @@ export default function MotionTuner({ const onCancel = useCallback(() => { setMotionSettings(origMotionSettings); setChangedValue(false); - }, [origMotionSettings]); + clearMessages("motion_tuner"); + }, [origMotionSettings, clearMessages]); + + useEffect(() => { + if (changedValue) { + addMessage("motion_tuner", "Unsaved motion tuner changes"); + } else { + clearMessages("motion_tuner"); + } + }, [changedValue, addMessage, clearMessages]); if (!cameraConfig && !selectedCamera) { return ; diff --git a/web/src/context/providers.tsx b/web/src/context/providers.tsx index b239aff47..fe5e931e7 100644 --- a/web/src/context/providers.tsx +++ b/web/src/context/providers.tsx @@ -4,6 +4,7 @@ import { RecoilRoot } from "recoil"; import { ApiProvider } from "@/api"; import { IconContext } from "react-icons"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { StatusBarMessagesProvider } from "@/context/statusbar-provider"; type TProvidersProps = { children: ReactNode; @@ -16,7 +17,7 @@ function providers({ children }: TProvidersProps) { - {children} + {children} diff --git a/web/src/context/statusbar-provider.tsx b/web/src/context/statusbar-provider.tsx new file mode 100644 index 000000000..6d17fa4de --- /dev/null +++ b/web/src/context/statusbar-provider.tsx @@ -0,0 +1,83 @@ +import { + createContext, + useState, + ReactNode, + useCallback, + useMemo, +} from "react"; + +export type StatusMessage = { + id: string; + text: string; + color?: string; +}; + +export type StatusMessagesState = { + [key: string]: StatusMessage[]; +}; + +type StatusBarMessagesProviderProps = { + children: ReactNode; +}; + +type StatusBarMessagesContextValue = { + messages: StatusMessagesState; + addMessage: ( + key: string, + message: string, + color?: string, + messageId?: string, + ) => string; + removeMessage: (key: string, messageId: string) => void; + clearMessages: (key: string) => void; +}; + +export const StatusBarMessagesContext = + createContext(null); + +export function StatusBarMessagesProvider({ + children, +}: StatusBarMessagesProviderProps) { + const [messagesState, setMessagesState] = useState({}); + + const messages = useMemo(() => messagesState, [messagesState]); + + const addMessage = useCallback( + (key: string, message: string, color?: string, messageId?: string) => { + const id = messageId || Date.now().toString(); + const msgColor = color || "text-danger"; + setMessagesState((prevMessages) => ({ + ...prevMessages, + [key]: [ + ...(prevMessages[key] || []), + { id, text: message, color: msgColor }, + ], + })); + return id; + }, + [], + ); + + const removeMessage = useCallback((key: string, messageId: string) => { + setMessagesState((prevMessages) => ({ + ...prevMessages, + [key]: prevMessages[key].filter((msg) => msg.id !== messageId), + })); + }, []); + + const clearMessages = useCallback((key: string) => { + setMessagesState((prevMessages) => { + const updatedMessages = { ...prevMessages }; + delete updatedMessages[key]; + return updatedMessages; + }); + }, []); + + return ( + + {children} + + ); +} diff --git a/web/src/hooks/use-deep-memo.ts b/web/src/hooks/use-deep-memo.ts new file mode 100644 index 000000000..a950f5c7e --- /dev/null +++ b/web/src/hooks/use-deep-memo.ts @@ -0,0 +1,12 @@ +import { useRef } from "react"; +import { isEqual } from "lodash"; + +export default function useDeepMemo(value: T) { + const ref = useRef(undefined); + + if (!isEqual(ref.current, value)) { + ref.current = value; + } + + return ref.current; +} diff --git a/web/src/hooks/use-stats.ts b/web/src/hooks/use-stats.ts index 02db404f1..1cb30dbb7 100644 --- a/web/src/hooks/use-stats.ts +++ b/web/src/hooks/use-stats.ts @@ -7,78 +7,82 @@ import { import { FrigateStats, PotentialProblem } from "@/types/stats"; import { useMemo } from "react"; import useSWR from "swr"; +import useDeepMemo from "./use-deep-memo"; +import { capitalizeFirstLetter } from "@/utils/stringUtil"; export default function useStats(stats: FrigateStats | undefined) { const { data: config } = useSWR("config"); + const memoizedStats = useDeepMemo(stats); + const potentialProblems = useMemo(() => { const problems: PotentialProblem[] = []; - if (!stats) { + if (!memoizedStats) { return problems; } // if frigate has just started // don't look for issues - if (stats.service.uptime < 120) { + if (memoizedStats.service.uptime < 120) { return problems; } // check detectors for high inference speeds - Object.entries(stats["detectors"]).forEach(([key, det]) => { + Object.entries(memoizedStats["detectors"]).forEach(([key, det]) => { if (det["inference_speed"] > InferenceThreshold.error) { problems.push({ - text: `${key} is very slow (${det["inference_speed"]} ms)`, + text: `${capitalizeFirstLetter(key)} is very slow (${det["inference_speed"]} ms)`, color: "text-danger", }); } else if (det["inference_speed"] > InferenceThreshold.warning) { problems.push({ - text: `${key} is slow (${det["inference_speed"]} ms)`, + text: `${capitalizeFirstLetter(key)} is slow (${det["inference_speed"]} ms)`, color: "text-orange-400", }); } }); // check for offline cameras - Object.entries(stats["cameras"]).forEach(([name, cam]) => { + Object.entries(memoizedStats["cameras"]).forEach(([name, cam]) => { if (!config) { return; } if (config.cameras[name].enabled && cam["camera_fps"] == 0) { problems.push({ - text: `${name.replaceAll("_", " ")} is offline`, + text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} is offline`, color: "text-danger", }); } }); // check camera cpu usages - Object.entries(stats["cameras"]).forEach(([name, cam]) => { + Object.entries(memoizedStats["cameras"]).forEach(([name, cam]) => { const ffmpegAvg = parseFloat( - stats["cpu_usages"][cam["ffmpeg_pid"]]?.cpu_average, + memoizedStats["cpu_usages"][cam["ffmpeg_pid"]]?.cpu_average, ); const detectAvg = parseFloat( - stats["cpu_usages"][cam["pid"]]?.cpu_average, + memoizedStats["cpu_usages"][cam["pid"]]?.cpu_average, ); if (!isNaN(ffmpegAvg) && ffmpegAvg >= CameraFfmpegThreshold.error) { problems.push({ - text: `${name.replaceAll("_", " ")} has high FFMPEG CPU usage (${ffmpegAvg}%)`, + text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`, color: "text-danger", }); } if (!isNaN(detectAvg) && detectAvg >= CameraDetectThreshold.error) { problems.push({ - text: `${name.replaceAll("_", " ")} has high detect CPU usage (${detectAvg}%)`, + text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`, color: "text-danger", }); } }); return problems; - }, [config, stats]); + }, [config, memoizedStats]); return { potentialProblems }; } diff --git a/web/src/utils/stringUtil.ts b/web/src/utils/stringUtil.ts new file mode 100644 index 000000000..34556ddb3 --- /dev/null +++ b/web/src/utils/stringUtil.ts @@ -0,0 +1,3 @@ +export const capitalizeFirstLetter = (text: string): string => { + return text.charAt(0).toUpperCase() + text.slice(1); +}; From d6e93d039d94d7db7a46d4db0a2b8d05e4c2845a Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:12:45 -0500 Subject: [PATCH 3/7] Make all corners less rounded on mobile (#11068) * make corners less rounded on mobile * fix live dashboard classes --- web/src/components/camera/CameraImage.tsx | 2 +- .../components/camera/ResizingCameraImage.tsx | 2 +- web/src/components/card/AnimatedEventCard.tsx | 2 +- web/src/components/card/ExportCard.tsx | 12 ++++++------ web/src/components/menu/GeneralSettings.tsx | 8 ++++++-- web/src/components/overlay/ExportDialog.tsx | 4 +++- .../components/player/BirdseyeLivePlayer.tsx | 16 +++++++++++----- web/src/components/player/HlsVideoPlayer.tsx | 2 +- web/src/components/player/LivePlayer.tsx | 12 ++++++------ web/src/components/player/PreviewPlayer.tsx | 8 ++++---- web/src/components/settings/MotionTuner.tsx | 2 +- web/src/components/timeline/EventSegment.tsx | 2 +- web/src/pages/Exports.tsx | 2 +- web/src/pages/SubmitPlus.tsx | 4 ++-- web/src/views/events/EventView.tsx | 6 +++--- web/src/views/live/LiveDashboardView.tsx | 4 ++-- web/src/views/system/CameraMetrics.tsx | 8 ++++---- web/src/views/system/GeneralMetrics.tsx | 18 +++++++++--------- web/src/views/system/StorageMetrics.tsx | 8 ++++---- 19 files changed, 67 insertions(+), 55 deletions(-) diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx index 1f2c28ade..ab0bfdf08 100644 --- a/web/src/components/camera/CameraImage.tsx +++ b/web/src/components/camera/CameraImage.tsx @@ -40,7 +40,7 @@ export default function CameraImage({ {enabled ? ( { setHasLoaded(true); diff --git a/web/src/components/camera/ResizingCameraImage.tsx b/web/src/components/camera/ResizingCameraImage.tsx index b7a9b8369..f19c40ff3 100644 --- a/web/src/components/camera/ResizingCameraImage.tsx +++ b/web/src/components/camera/ResizingCameraImage.tsx @@ -100,7 +100,7 @@ export default function CameraImage({ > {enabled ? (
{previews ? ( diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index 86ec9c7ce..f7dd7f588 100644 --- a/web/src/components/card/ExportCard.tsx +++ b/web/src/components/card/ExportCard.tsx @@ -104,7 +104,7 @@ export default function ExportCard({
setHovered(true) @@ -123,7 +123,7 @@ export default function ExportCard({ > {hovered && ( <> -
+
{exportedRecording.thumb_path.length > 0 ? ( setLoading(false)} /> ) : ( -
+
)} )} {loading && ( - + )} -
+
{exportedRecording.name.replaceAll("_", " ")}
diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index bc45f650f..aed5f2173 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -210,7 +210,9 @@ export default function GeneralSettings({ className }: GeneralSettings) { {colorSchemes.map((scheme) => ( diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index a608528bb..60de90468 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -130,7 +130,9 @@ export default function ExportDialog({ + ); } else if (liveMode == "mse") { if ("MediaSource" in window || "ManagedMediaSource" in window) { player = ( - + ); } else { player = ( @@ -39,7 +45,7 @@ export default function BirdseyeLivePlayer({ } else if (liveMode == "jsmpeg") { player = ( -
-
+
+
{player}
); diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 5f0348d88..54bc8c8c9 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -164,7 +164,7 @@ export default function HlsVideoPlayer({ >
) : ( )}
diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index c8ddd1822..7cd6bbcd6 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -175,7 +175,7 @@ export default function LiveDashboardView({ )}
{includeBirdseye && birdseyeConfig?.enabled && ( Overview
{statsHistory.length != 0 ? ( -
+
DPS
) : ( - + )}
@@ -214,7 +214,7 @@ export default function CameraMetrics({
{Object.keys(cameraCpuSeries).includes(camera.name) ? ( -
+
CPU
)} {Object.keys(cameraFpsSeries).includes(camera.name) ? ( -
+
DPS
{statsHistory.length != 0 ? ( -
+
Detector Inference Speed
{detInferenceTimeSeries.map((series) => ( ) : ( - + )} {statsHistory.length != 0 ? ( <> {detTempSeries && ( -
+
Detector Temperature
{detTempSeries.map((series) => ( )} {statsHistory.length != 0 ? ( -
+
Detector CPU Usage
{detCpuSeries.map((series) => ( )} {statsHistory.length != 0 ? ( -
+
Detector Memory Usage
{detMemSeries.map((series) => (
{statsHistory.length != 0 ? ( -
+
GPU Usage
{gpuSeries.map((series) => ( {gpuMemSeries && ( -
+
GPU Memory
{gpuMemSeries.map((series) => (
{statsHistory.length != 0 ? ( -
+
Process CPU Usage
{otherProcessCpuSeries.map((series) => ( )} {statsHistory.length != 0 ? ( -
+
Process Memory Usage
{otherProcessMemSeries.map((series) => (
Overview
-
+
Recordings
-
+
/tmp/cache
-
+
/dev/shm
{Object.keys(cameraStorage).map((camera) => ( -
+
{camera.replaceAll("_", " ")}
Date: Mon, 22 Apr 2024 20:20:30 -0600 Subject: [PATCH 4/7] Improve review mqtt topic (#11072) * Don't request preview for current hour and fix content type * Send before / after on messages * Send before / after for end * Handle specific cases * remove log * Fix update type --- frigate/api/media.py | 2 +- frigate/review/maintainer.py | 59 +++++++++++++++---- web/src/components/card/AnimatedEventCard.tsx | 7 ++- web/src/types/ws.ts | 3 +- web/src/views/live/LiveDashboardView.tsx | 15 ++++- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/frigate/api/media.py b/frigate/api/media.py index 9770de157..519467643 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -1351,6 +1351,6 @@ def preview_thumbnail(file_name: str): ) response = make_response(jpg_bytes) - response.headers["Content-Type"] = "image/jpeg" + response.headers["Content-Type"] = "image/webp" response.headers["Cache-Control"] = "private, max-age=31536000" return response diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 5e5138083..750ed44ba 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -146,25 +146,61 @@ class ReviewSegmentMaintainer(threading.Thread): self.stop_event = stop_event - def update_segment(self, segment: PendingReviewSegment) -> None: - """Update segment.""" - seg_data = segment.get_data(ended=False) - self.requestor.send_data(UPSERT_REVIEW_SEGMENT, seg_data) + def new_segment( + self, + segment: PendingReviewSegment, + ) -> None: + """New segment.""" + new_data = segment.get_data(ended=False) + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data) + start_data = {k.name: v for k, v in new_data.items()} self.requestor.send_data( "reviews", json.dumps( - {"type": "update", "review": {k.name: v for k, v in seg_data.items()}} + { + "type": "new", + "before": start_data, + "after": start_data, + } + ), + ) + + def update_segment( + self, + segment: PendingReviewSegment, + camera_config: CameraConfig, + frame, + objects: list[TrackedObject], + ) -> None: + """Update segment.""" + prev_data = segment.get_data(ended=False) + segment.update_frame(camera_config, frame, objects) + new_data = segment.get_data(ended=False) + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data) + self.requestor.send_data( + "reviews", + json.dumps( + { + "type": "update", + "before": {k.name: v for k, v in prev_data.items()}, + "after": {k.name: v for k, v in new_data.items()}, + } ), ) def end_segment(self, segment: PendingReviewSegment) -> None: """End segment.""" - seg_data = segment.get_data(ended=True) - self.requestor.send_data(UPSERT_REVIEW_SEGMENT, seg_data) + final_data = segment.get_data(ended=True) + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, final_data) + end_data = {k.name: v for k, v in final_data.items()} self.requestor.send_data( "reviews", json.dumps( - {"type": "end", "review": {k.name: v for k, v in seg_data.items()}} + { + "type": "end", + "before": end_data, + "after": end_data, + } ), ) self.active_review_segments[segment.camera] = None @@ -219,9 +255,10 @@ class ReviewSegmentMaintainer(threading.Thread): yuv_frame = self.frame_manager.get( frame_id, camera_config.frame_shape_yuv ) - segment.update_frame(camera_config, yuv_frame, active_objects) + self.update_segment( + segment, camera_config, yuv_frame, active_objects + ) self.frame_manager.close(frame_id) - self.update_segment(segment) except FileNotFoundError: return else: @@ -317,7 +354,7 @@ class ReviewSegmentMaintainer(threading.Thread): camera_config, yuv_frame, active_objects ) self.frame_manager.close(frame_id) - self.update_segment(self.active_review_segments[camera]) + self.new_segment(self.active_review_segments[camera]) except FileNotFoundError: return diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx index 4e27ad54f..dddbcdb77 100644 --- a/web/src/components/card/AnimatedEventCard.tsx +++ b/web/src/components/card/AnimatedEventCard.tsx @@ -12,6 +12,7 @@ import { InProgressPreview, VideoPreview, } from "../player/PreviewThumbnailPlayer"; +import { isCurrentHour } from "@/utils/dateUtil"; type AnimatedEventCardProps = { event: ReviewSegment; @@ -19,10 +20,14 @@ type AnimatedEventCardProps = { export function AnimatedEventCard({ event }: AnimatedEventCardProps) { const { data: config } = useSWR("config"); + const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]); + // preview const { data: previews } = useSWR( - `/preview/${event.camera}/start/${Math.round(event.start_time)}/end/${Math.round(event.end_time || event.start_time + 20)}`, + currentHour + ? null + : `/preview/${event.camera}/start/${Math.round(event.start_time)}/end/${Math.round(event.end_time || event.start_time + 20)}`, ); // interaction diff --git a/web/src/types/ws.ts b/web/src/types/ws.ts index 8e86604b3..177d3600a 100644 --- a/web/src/types/ws.ts +++ b/web/src/types/ws.ts @@ -31,7 +31,8 @@ type FrigateObjectState = { export interface FrigateReview { type: "new" | "update" | "end"; - review: ReviewSegment; + before: ReviewSegment; + after: ReviewSegment; } export interface FrigateEvent { diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index 7cd6bbcd6..3c186656a 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -47,8 +47,19 @@ export default function LiveDashboardView({ } // if event is ended and was saved, update events list - if (eventUpdate.review.severity == "alert") { - setTimeout(() => updateEvents(), eventUpdate.type == "end" ? 1000 : 6000); + if (eventUpdate.after.severity == "alert") { + if (eventUpdate.type == "end" || eventUpdate.type == "new") { + setTimeout( + () => updateEvents(), + eventUpdate.type == "end" ? 1000 : 6000, + ); + } else if ( + eventUpdate.before.data.objects.length < + eventUpdate.after.data.objects.length + ) { + setTimeout(() => updateEvents(), 5000); + } + return; } }, [eventUpdate, updateEvents]); From 647bcb26183ef872b5c53ce2f1dfc6b15c7b4887 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 23 Apr 2024 08:32:46 -0600 Subject: [PATCH 5/7] Add camera fps to graphs (#11080) * Add camera fps to graphs * Use more generic name --- web/src/views/system/CameraMetrics.tsx | 31 +++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/web/src/views/system/CameraMetrics.tsx b/web/src/views/system/CameraMetrics.tsx index 2b005df8f..a9d8a0d25 100644 --- a/web/src/views/system/CameraMetrics.tsx +++ b/web/src/views/system/CameraMetrics.tsx @@ -66,6 +66,7 @@ export default function CameraMetrics({ [key: string]: { name: string; data: { x: number; y: number }[] }; } = {}; + series["overall_fps"] = { name: "overall frames per second", data: [] }; series["overall_dps"] = { name: "overall detections per second", data: [] }; series["overall_skipped_dps"] = { name: "overall skipped detections per second", @@ -77,6 +78,16 @@ export default function CameraMetrics({ return; } + let frames = 0; + Object.values(stats.cameras).forEach( + (camStat) => (frames += camStat.camera_fps), + ); + + series["overall_fps"].data.push({ + x: statsIdx, + y: Math.round(frames), + }); + series["overall_dps"].data.push({ x: statsIdx, y: stats.detection_fps, @@ -161,6 +172,10 @@ export default function CameraMetrics({ if (!(key in series)) { const camName = key.replaceAll("_", " "); series[key] = {}; + series[key]["fps"] = { + name: `${camName} frames per second`, + data: [], + }; series[key]["det"] = { name: `${camName} detections per second`, data: [], @@ -171,6 +186,10 @@ export default function CameraMetrics({ }; } + series[key]["fps"].data.push({ + x: statsIdx, + y: camStats.camera_fps, + }); series[key]["det"].data.push({ x: statsIdx, y: camStats.detection_fps, @@ -190,11 +209,11 @@ export default function CameraMetrics({
{statsHistory.length != 0 ? (
-
DPS
+
Frames / Detections
@@ -231,11 +250,11 @@ export default function CameraMetrics({ )} {Object.keys(cameraFpsSeries).includes(camera.name) ? (
-
DPS
+
Frames / Detections
Date: Tue, 23 Apr 2024 08:57:29 -0600 Subject: [PATCH 6/7] fix motion updating (#11083) --- frigate/video.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frigate/video.py b/frigate/video.py index 5d3eeeae3..ce01b8dfa 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -413,9 +413,7 @@ def track_camera( object_filters = config.objects.filters motion_detector = ImprovedMotionDetector( - frame_shape, - config.motion, - config.detect.fps, + frame_shape, config.motion, config.detect.fps, name=config.name ) object_detector = RemoteObjectDetector( name, labelmap, detection_queue, result_connection, model_config, stop_event From c035241b39ef97acdfb77cdd70dd1facae762db1 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 24 Apr 2024 07:44:28 -0600 Subject: [PATCH 7/7] Clear ongoing reviews on startup (#11097) * Add topic to clear ongoing review segments * Clear ongoing review items on startup --- frigate/comms/dispatcher.py | 6 ++++++ frigate/const.py | 1 + frigate/review/maintainer.py | 10 +++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 0b466a01c..6fc3885e0 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -1,5 +1,6 @@ """Handle communication between Frigate and other applications.""" +import datetime import logging from abc import ABC, abstractmethod from typing import Any, Callable, Optional @@ -7,6 +8,7 @@ from typing import Any, Callable, Optional from frigate.comms.config_updater import ConfigPublisher from frigate.config import BirdseyeModeEnum, FrigateConfig from frigate.const import ( + CLEAR_ONGOING_REVIEW_SEGMENTS, INSERT_MANY_RECORDINGS, INSERT_PREVIEW, REQUEST_REGION_GRID, @@ -116,6 +118,10 @@ class Dispatcher: ) .execute() ) + elif topic == CLEAR_ONGOING_REVIEW_SEGMENTS: + ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where( + ReviewSegment.end_time == None + ).execute() else: self.publish(topic, payload, retain=False) diff --git a/frigate/const.py b/frigate/const.py index 28ab83c8a..168d880fb 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -79,6 +79,7 @@ INSERT_MANY_RECORDINGS = "insert_many_recordings" INSERT_PREVIEW = "insert_preview" REQUEST_REGION_GRID = "request_region_grid" UPSERT_REVIEW_SEGMENT = "upsert_review_segment" +CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments" # Autotracking diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 750ed44ba..295641656 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -19,7 +19,12 @@ from frigate.comms.config_updater import ConfigSubscriber from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum from frigate.comms.inter_process import InterProcessRequestor from frigate.config import CameraConfig, FrigateConfig -from frigate.const import ALL_ATTRIBUTE_LABELS, CLIPS_DIR, UPSERT_REVIEW_SEGMENT +from frigate.const import ( + ALL_ATTRIBUTE_LABELS, + CLEAR_ONGOING_REVIEW_SEGMENTS, + CLIPS_DIR, + UPSERT_REVIEW_SEGMENT, +) from frigate.events.external import ManualEventState from frigate.models import ReviewSegment from frigate.object_processing import TrackedObject @@ -146,6 +151,9 @@ class ReviewSegmentMaintainer(threading.Thread): self.stop_event = stop_event + # clear ongoing review segments from last instance + self.requestor.send_data(CLEAR_ONGOING_REVIEW_SEGMENTS, "") + def new_segment( self, segment: PendingReviewSegment,