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 ? (