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/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 5e5138083..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,25 +151,64 @@ 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) + # clear ongoing review segments from last instance + self.requestor.send_data(CLEAR_ONGOING_REVIEW_SEGMENTS, "") + + 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 +263,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 +362,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/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 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/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 ? ( ("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 @@ -63,7 +68,7 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) { }} >
{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/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/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({ >
); }) - : Array(itemsToReview) + : (itemsToReview ?? 0) > 0 && + Array(itemsToReview) .fill(0) .map((_, idx) => ( @@ -890,7 +891,7 @@ function MotionReview({ {motionData ? ( <> ) : ( )}
diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index c8ddd1822..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]); @@ -175,7 +186,7 @@ export default function LiveDashboardView({ )}
{includeBirdseye && birdseyeConfig?.enabled && ( (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, @@ -189,18 +208,18 @@ export default function CameraMetrics({
Overview
{statsHistory.length != 0 ? ( -
-
DPS
+
+
Frames / Detections
) : ( - + )}
@@ -214,7 +233,7 @@ export default function CameraMetrics({
{Object.keys(cameraCpuSeries).includes(camera.name) ? ( -
+
CPU
)} {Object.keys(cameraFpsSeries).includes(camera.name) ? ( -
-
DPS
+
+
Frames / Detections
{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("_", " ")}