From f95ce913b163222b93e1ab9af6795cc71f5a8552 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 27 Feb 2024 06:37:39 -0700 Subject: [PATCH] Publish finished reviews to mqtt / ws and use that for source of update banner (#10072) * Add reviews to frontend * Update ready when new review is saved * fix * Formatting --- frigate/review/maintainer.py | 10 +++++++- web/src/api/ws.tsx | 11 +++++++-- web/src/pages/Events.tsx | 30 +++++++++++++++-------- web/src/types/ws.ts | 7 ++++++ web/src/views/events/DesktopEventView.tsx | 5 +++- web/src/views/events/MobileEventView.tsx | 5 +++- 6 files changed, 53 insertions(+), 15 deletions(-) diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index f459b2f35..9e0a38210 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -1,5 +1,6 @@ """Maintain review segments in db.""" +import json import logging import os import random @@ -138,7 +139,14 @@ class ReviewSegmentMaintainer(threading.Thread): def end_segment(self, segment: PendingReviewSegment) -> None: """End segment.""" - self.requestor.send_data(UPSERT_REVIEW_SEGMENT, segment.end()) + seg_data = segment.end() + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, seg_data) + self.requestor.send_data( + "reviews", + json.dumps( + {"type": "end", "review": {k.name: v for k, v in seg_data.items()}} + ), + ) self.active_review_segments[segment.camera] = None def update_existing_segment( diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index a21ba4857..88091a915 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -10,7 +10,7 @@ import { import { produce, Draft } from "immer"; import useWebSocket, { ReadyState } from "react-use-websocket"; import { FrigateConfig } from "@/types/frigateConfig"; -import { FrigateEvent, ToggleableSetting } from "@/types/ws"; +import { FrigateEvent, FrigateReview, ToggleableSetting } from "@/types/ws"; import { FrigateStats } from "@/types/stats"; type ReducerState = { @@ -218,7 +218,14 @@ export function useRestart(): { export function useFrigateEvents(): { payload: FrigateEvent } { const { value: { payload }, - } = useWs(`events`, ""); + } = useWs("events", ""); + return { payload }; +} + +export function useFrigateReviews(): { payload: FrigateReview } { + const { + value: { payload }, + } = useWs("reviews", ""); return { payload }; } diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index 62a18c504..40d71adb1 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -1,7 +1,7 @@ -import { useFrigateEvents } from "@/api/ws"; +import { useFrigateReviews } from "@/api/ws"; import useApiFilter from "@/hooks/use-api-filter"; import useOverlayState from "@/hooks/use-overlay-state"; -import { ReviewFilter, ReviewSegment } from "@/types/review"; +import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; import DesktopEventView from "@/views/events/DesktopEventView"; import DesktopRecordingView from "@/views/events/DesktopRecordingView"; import MobileEventView from "@/views/events/MobileEventView"; @@ -15,6 +15,8 @@ const API_LIMIT = 250; export default function Events() { // recordings viewer + + const [severity, setSeverity] = useState("alert"); const [selectedReviewId, setSelectedReviewId] = useOverlayState("review"); // review filter @@ -97,6 +99,11 @@ export default function Events() { setSize(size + 1); }, [size]); + const reloadData = useCallback(() => { + setSize(1); + updateSegments(); + }, []) + // preview videos const previewTimes = useMemo(() => { @@ -196,22 +203,21 @@ export default function Events() { // review updates - const { payload: eventUpdate } = useFrigateEvents(); + const { payload: reviewUpdate } = useFrigateReviews(); const [hasUpdate, setHasUpdate] = useState(false); useEffect(() => { - if (!eventUpdate) { + if (!reviewUpdate || hasUpdate) { return; } - // if event is ended and was saved, update events list if ( - eventUpdate.type == "end" && - (eventUpdate.after.has_clip || eventUpdate.after.has_snapshot) + reviewUpdate.type == "end" && + reviewUpdate.review.severity == severity ) { setHasUpdate(true); return; } - }, [eventUpdate]); + }, [reviewUpdate]); if (selectedData) { return ( @@ -229,11 +235,13 @@ export default function Events() { relevantPreviews={allPreviews} reachedEnd={isDone} isValidating={isValidating} + severity={severity} hasUpdate={hasUpdate} + setSeverity={setSeverity} setHasUpdate={setHasUpdate} loadNextPage={onLoadNextPage} markItemAsReviewed={markItemAsReviewed} - pullLatestData={updateSegments} + pullLatestData={reloadData} /> ); } @@ -246,12 +254,14 @@ export default function Events() { reachedEnd={isDone} isValidating={isValidating} filter={reviewFilter} + severity={severity} hasUpdate={hasUpdate} + setSeverity={setSeverity} setHasUpdate={setHasUpdate} loadNextPage={onLoadNextPage} markItemAsReviewed={markItemAsReviewed} onSelectReview={setSelectedReviewId} - pullLatestData={updateSegments} + pullLatestData={reloadData} updateFilter={onUpdateFilter} /> ); diff --git a/web/src/types/ws.ts b/web/src/types/ws.ts index fb041b0c2..a8e454c93 100644 --- a/web/src/types/ws.ts +++ b/web/src/types/ws.ts @@ -1,3 +1,5 @@ +import { ReviewSegment } from "./review"; + type FrigateObjectState = { id: string; camera: string; @@ -27,6 +29,11 @@ type FrigateObjectState = { }; }; +export interface FrigateReview { + type: "new" | "update" | "end"; + review: ReviewSegment; +} + export interface FrigateEvent { type: "new" | "update" | "end"; before: FrigateObjectState; diff --git a/web/src/views/events/DesktopEventView.tsx b/web/src/views/events/DesktopEventView.tsx index 666cce103..c198d2eb3 100644 --- a/web/src/views/events/DesktopEventView.tsx +++ b/web/src/views/events/DesktopEventView.tsx @@ -18,7 +18,9 @@ type DesktopEventViewProps = { reachedEnd: boolean; isValidating: boolean; filter?: ReviewFilter; + severity: ReviewSeverity; hasUpdate: boolean; + setSeverity: (severity: ReviewSeverity) => void; setHasUpdate: (hasUpdated: boolean) => void; loadNextPage: () => void; markItemAsReviewed: (reviewId: string) => void; @@ -33,7 +35,9 @@ export default function DesktopEventView({ reachedEnd, isValidating, filter, + severity, hasUpdate, + setSeverity, setHasUpdate, loadNextPage, markItemAsReviewed, @@ -42,7 +46,6 @@ export default function DesktopEventView({ updateFilter, }: DesktopEventViewProps) { const { data: config } = useSWR("config"); - const [severity, setSeverity] = useState("alert"); const contentRef = useRef(null); // review paging diff --git a/web/src/views/events/MobileEventView.tsx b/web/src/views/events/MobileEventView.tsx index b476808d7..cfdce9bba 100644 --- a/web/src/views/events/MobileEventView.tsx +++ b/web/src/views/events/MobileEventView.tsx @@ -14,7 +14,9 @@ type MobileEventViewProps = { relevantPreviews?: Preview[]; reachedEnd: boolean; isValidating: boolean; + severity: ReviewSeverity; hasUpdate: boolean; + setSeverity: (severity: ReviewSeverity) => void; setHasUpdate: (hasUpdated: boolean) => void; loadNextPage: () => void; markItemAsReviewed: (reviewId: string) => void; @@ -25,14 +27,15 @@ export default function MobileEventView({ relevantPreviews, reachedEnd, isValidating, + severity, hasUpdate, + setSeverity, setHasUpdate, loadNextPage, markItemAsReviewed, pullLatestData, }: MobileEventViewProps) { const { data: config } = useSWR("config"); - const [severity, setSeverity] = useState("alert"); const contentRef = useRef(null); // review paging