Save initial camera state to update when websocket connects (#11174)

* Send camera state to dispatcher

* Fix logic

* Cleanup

* Send camera activitiy in on connect

* Support reading initial camera state

* Fix key

* Formatting

* Sorting
This commit is contained in:
Nicolas Mowen 2024-04-30 07:09:50 -06:00 committed by GitHub
parent 0080c28c4d
commit 461442b399
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 147 additions and 43 deletions

View File

@ -1,6 +1,7 @@
"""Handle communication between Frigate and other applications.""" """Handle communication between Frigate and other applications."""
import datetime import datetime
import json
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Callable, Optional from typing import Any, Callable, Optional
@ -12,6 +13,7 @@ from frigate.const import (
INSERT_MANY_RECORDINGS, INSERT_MANY_RECORDINGS,
INSERT_PREVIEW, INSERT_PREVIEW,
REQUEST_REGION_GRID, REQUEST_REGION_GRID,
UPDATE_CAMERA_ACTIVITY,
UPSERT_REVIEW_SEGMENT, UPSERT_REVIEW_SEGMENT,
) )
from frigate.models import Previews, Recordings, ReviewSegment from frigate.models import Previews, Recordings, ReviewSegment
@ -76,6 +78,8 @@ class Dispatcher:
for comm in self.comms: for comm in self.comms:
comm.subscribe(self._receive) comm.subscribe(self._receive)
self.camera_activity = {}
def _receive(self, topic: str, payload: str) -> Optional[Any]: def _receive(self, topic: str, payload: str) -> Optional[Any]:
"""Handle receiving of payload from communicators.""" """Handle receiving of payload from communicators."""
if topic.endswith("set"): if topic.endswith("set"):
@ -122,6 +126,10 @@ class Dispatcher:
ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where( ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where(
ReviewSegment.end_time == None ReviewSegment.end_time == None
).execute() ).execute()
elif topic == UPDATE_CAMERA_ACTIVITY:
self.camera_activity = payload
elif topic == "onConnect":
self.publish("camera_activity", json.dumps(self.camera_activity))
else: else:
self.publish(topic, payload, retain=False) self.publish(topic, payload, retain=False)

View File

@ -50,6 +50,10 @@ class WebSocketClient(Communicator): # type: ignore[misc]
class _WebSocketHandler(WebSocket): # type: ignore[misc] class _WebSocketHandler(WebSocket): # type: ignore[misc]
receiver = self._dispatcher receiver = self._dispatcher
def opened(self) -> None:
"""A new websocket is opened, we need to send an update message"""
threading.Timer(1.0, self.receiver, ("onConnect", "")).start()
def received_message(self, message: WebSocket.received_message) -> None: def received_message(self, message: WebSocket.received_message) -> None:
try: try:
json_message = json.loads(message.data.decode("utf-8")) json_message = json.loads(message.data.decode("utf-8"))

View File

@ -80,6 +80,7 @@ INSERT_PREVIEW = "insert_preview"
REQUEST_REGION_GRID = "request_region_grid" REQUEST_REGION_GRID = "request_region_grid"
UPSERT_REVIEW_SEGMENT = "upsert_review_segment" UPSERT_REVIEW_SEGMENT = "upsert_review_segment"
CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments" CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments"
UPDATE_CAMERA_ACTIVITY = "update_camera_activity"
# Autotracking # Autotracking

View File

@ -16,6 +16,7 @@ import numpy as np
from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum
from frigate.comms.dispatcher import Dispatcher from frigate.comms.dispatcher import Dispatcher
from frigate.comms.events_updater import EventEndSubscriber, EventUpdatePublisher from frigate.comms.events_updater import EventEndSubscriber, EventUpdatePublisher
from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import ( from frigate.config import (
CameraConfig, CameraConfig,
FrigateConfig, FrigateConfig,
@ -24,7 +25,7 @@ from frigate.config import (
SnapshotsConfig, SnapshotsConfig,
ZoomingModeEnum, ZoomingModeEnum,
) )
from frigate.const import CLIPS_DIR from frigate.const import ALL_ATTRIBUTE_LABELS, CLIPS_DIR, UPDATE_CAMERA_ACTIVITY
from frigate.events.types import EventStateEnum, EventTypeEnum from frigate.events.types import EventStateEnum, EventTypeEnum
from frigate.ptz.autotrack import PtzAutoTrackerThread from frigate.ptz.autotrack import PtzAutoTrackerThread
from frigate.util.image import ( from frigate.util.image import (
@ -724,8 +725,31 @@ class CameraState:
# TODO: can i switch to looking this up and only changing when an event ends? # TODO: can i switch to looking this up and only changing when an event ends?
# maintain best objects # maintain best objects
camera_activity: dict[str, list[any]] = {
"motion": len(motion_boxes) > 0,
"objects": [],
}
for obj in tracked_objects.values(): for obj in tracked_objects.values():
object_type = obj.obj_data["label"] object_type = obj.obj_data["label"]
active = (
obj.obj_data["motionless_count"]
< self.camera_config.detect.stationary.threshold
)
if not obj.false_positive:
label = object_type
if (
obj.obj_data.get("sub_label")
and obj.obj_data.get("sub_label")[0] in ALL_ATTRIBUTE_LABELS
):
label = obj.obj_data["sub_label"]
camera_activity["objects"].append(
{"id": obj.obj_data["id"], "label": label, "stationary": not active}
)
# if the object's thumbnail is not from the current frame # if the object's thumbnail is not from the current frame
if obj.false_positive or obj.thumbnail_data["frame_time"] != frame_time: if obj.false_positive or obj.thumbnail_data["frame_time"] != frame_time:
continue continue
@ -752,6 +776,9 @@ class CameraState:
for c in self.callbacks["snapshot"]: for c in self.callbacks["snapshot"]:
c(self.name, self.best_objects[object_type], frame_time) c(self.name, self.best_objects[object_type], frame_time)
for c in self.callbacks["camera_activity"]:
c(self.name, camera_activity)
# update overall camera state for each object type # update overall camera state for each object type
obj_counter = Counter( obj_counter = Counter(
obj.obj_data["label"] obj.obj_data["label"]
@ -841,10 +868,14 @@ class TrackedObjectProcessor(threading.Thread):
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager()
self.last_motion_detected: dict[str, float] = {} self.last_motion_detected: dict[str, float] = {}
self.ptz_autotracker_thread = ptz_autotracker_thread self.ptz_autotracker_thread = ptz_autotracker_thread
self.requestor = InterProcessRequestor()
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.video) self.detection_publisher = DetectionPublisher(DetectionTypeEnum.video)
self.event_sender = EventUpdatePublisher() self.event_sender = EventUpdatePublisher()
self.event_end_subscriber = EventEndSubscriber() self.event_end_subscriber = EventEndSubscriber()
self.camera_activity: dict[str, dict[str, any]] = {}
def start(camera, obj: TrackedObject, current_frame_time): def start(camera, obj: TrackedObject, current_frame_time):
self.event_sender.publish( self.event_sender.publish(
( (
@ -962,6 +993,13 @@ class TrackedObjectProcessor(threading.Thread):
def object_status(camera, object_name, status): def object_status(camera, object_name, status):
self.dispatcher.publish(f"{camera}/{object_name}", status, retain=False) self.dispatcher.publish(f"{camera}/{object_name}", status, retain=False)
def camera_activity(camera, activity):
last_activity = self.camera_activity.get(camera)
if not last_activity or activity != last_activity:
self.camera_activity[camera] = activity
self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity)
for camera in self.config.cameras.keys(): for camera in self.config.cameras.keys():
camera_state = CameraState( camera_state = CameraState(
camera, self.config, self.frame_manager, self.ptz_autotracker_thread camera, self.config, self.frame_manager, self.ptz_autotracker_thread
@ -972,6 +1010,7 @@ class TrackedObjectProcessor(threading.Thread):
camera_state.on("end", end) camera_state.on("end", end)
camera_state.on("snapshot", snapshot) camera_state.on("snapshot", snapshot)
camera_state.on("object_status", object_status) camera_state.on("object_status", object_status)
camera_state.on("camera_activity", camera_activity)
self.camera_states[camera] = camera_state self.camera_states[camera] = camera_state
# { # {
@ -1228,6 +1267,7 @@ class TrackedObjectProcessor(threading.Thread):
event_id, camera = update event_id, camera = update
self.camera_states[camera].finished(event_id) self.camera_states[camera].finished(event_id)
self.requestor.stop()
self.detection_publisher.stop() self.detection_publisher.stop()
self.event_sender.stop() self.event_sender.stop()
self.event_end_subscriber.stop() self.event_end_subscriber.stop()

View File

@ -2,7 +2,12 @@ import { baseUrl } from "./baseUrl";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import useWebSocket, { ReadyState } from "react-use-websocket"; import useWebSocket, { ReadyState } from "react-use-websocket";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { FrigateEvent, FrigateReview, ToggleableSetting } from "@/types/ws"; import {
FrigateCameraState,
FrigateEvent,
FrigateReview,
ToggleableSetting,
} from "@/types/ws";
import { FrigateStats } from "@/types/stats"; import { FrigateStats } from "@/types/stats";
import useSWR from "swr"; import useSWR from "swr";
import { createContainer } from "react-tracked"; import { createContainer } from "react-tracked";
@ -193,6 +198,16 @@ export function useFrigateStats(): { payload: FrigateStats } {
return { payload: JSON.parse(payload as string) }; return { payload: JSON.parse(payload as string) };
} }
export function useInitialCameraState(camera: string): {
payload: FrigateCameraState;
} {
const {
value: { payload },
} = useWs("camera_activity", "");
const data = JSON.parse(payload as string);
return { payload: data ? data[camera] : undefined };
}
export function useMotionActivity(camera: string): { payload: string } { export function useMotionActivity(camera: string): { payload: string } {
const { const {
value: { payload }, value: { payload },

View File

@ -131,7 +131,11 @@ export default function Statusbar() {
); );
if (link) { if (link) {
return <Link to={link}>{message}</Link>; return (
<Link key={id} to={link}>
{message}
</Link>
);
} else { } else {
return message; return message;
} }

View File

@ -48,7 +48,7 @@ export default function LivePlayer({
// camera activity // camera activity
const { activeMotion, activeTracking, activeObjects } = const { activeMotion, activeTracking, objects } =
useCameraActivity(cameraConfig); useCameraActivity(cameraConfig);
const cameraActive = useMemo( const cameraActive = useMemo(
@ -166,7 +166,7 @@ export default function LivePlayer({
<div className="absolute bottom-0 inset-x-0 rounded-lg md:rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div> <div className="absolute bottom-0 inset-x-0 rounded-lg md:rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div>
{player} {player}
{activeObjects.length > 0 && ( {objects.length > 0 && (
<div className="absolute left-0 top-2 z-40"> <div className="absolute left-0 top-2 z-40">
<Tooltip> <Tooltip>
<div className="flex"> <div className="flex">
@ -177,7 +177,7 @@ export default function LivePlayer({
> >
{[ {[
...new Set([ ...new Set([
...(activeObjects || []).map(({ label }) => label), ...(objects || []).map(({ label }) => label),
]), ]),
] ]
.map((label) => { .map((label) => {
@ -189,11 +189,7 @@ export default function LivePlayer({
</TooltipTrigger> </TooltipTrigger>
</div> </div>
<TooltipContent className="capitalize"> <TooltipContent className="capitalize">
{[ {[...new Set([...(objects || []).map(({ label }) => label)])]
...new Set([
...(activeObjects || []).map(({ label }) => label),
]),
]
.filter( .filter(
(label) => (label) =>
label !== undefined && !label.includes("-verified"), label !== undefined && !label.includes("-verified"),

View File

@ -1,72 +1,97 @@
import { useFrigateEvents, useMotionActivity } from "@/api/ws"; import {
useFrigateEvents,
useInitialCameraState,
useMotionActivity,
} from "@/api/ws";
import { CameraConfig } from "@/types/frigateConfig"; import { CameraConfig } from "@/types/frigateConfig";
import { MotionData, ReviewSegment } from "@/types/review"; import { MotionData, ReviewSegment } from "@/types/review";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTimelineUtils } from "./use-timeline-utils"; import { useTimelineUtils } from "./use-timeline-utils";
import { ObjectType } from "@/types/ws";
type ActiveObjectType = { import useDeepMemo from "./use-deep-memo";
id: string;
label: string;
stationary: boolean;
};
type useCameraActivityReturn = { type useCameraActivityReturn = {
activeTracking: boolean; activeTracking: boolean;
activeMotion: boolean; activeMotion: boolean;
activeObjects: ActiveObjectType[]; objects: ObjectType[];
}; };
export function useCameraActivity( export function useCameraActivity(
camera: CameraConfig, camera: CameraConfig,
): useCameraActivityReturn { ): useCameraActivityReturn {
const [activeObjects, setActiveObjects] = useState<ActiveObjectType[]>([]); const [objects, setObjects] = useState<ObjectType[]>([]);
// init camera activity
const { payload: initialCameraState } = useInitialCameraState(camera.name);
const updatedCameraState = useDeepMemo(initialCameraState);
useEffect(() => {
if (updatedCameraState) {
setObjects(updatedCameraState.objects);
}
}, [updatedCameraState]);
// handle camera activity
const hasActiveObjects = useMemo( const hasActiveObjects = useMemo(
() => activeObjects.filter((obj) => !obj.stationary).length > 0, () => objects.filter((obj) => !obj.stationary).length > 0,
[activeObjects], [objects],
); );
const { payload: detectingMotion } = useMotionActivity(camera.name); const { payload: detectingMotion } = useMotionActivity(camera.name);
const { payload: event } = useFrigateEvents(); const { payload: event } = useFrigateEvents();
const updatedEvent = useDeepMemo(event);
useEffect(() => { useEffect(() => {
if (!event) { if (!updatedEvent) {
return; return;
} }
if (event.after.camera != camera.name) { if (updatedEvent.after.camera != camera.name) {
return; return;
} }
const eventIndex = activeObjects.findIndex( const updatedEventIndex = objects.findIndex(
(obj) => obj.id === event.after.id, (obj) => obj.id === updatedEvent.after.id,
); );
if (event.type == "end") { if (updatedEvent.type == "end") {
if (eventIndex != -1) { if (updatedEventIndex != -1) {
const newActiveObjects = [...activeObjects]; const newActiveObjects = [...objects];
newActiveObjects.splice(eventIndex, 1); newActiveObjects.splice(updatedEventIndex, 1);
setActiveObjects(newActiveObjects); setObjects(newActiveObjects);
} }
} else { } else {
if (eventIndex == -1) { if (updatedEventIndex == -1) {
// add unknown event to list if not stationary // add unknown updatedEvent to list if not stationary
if (!event.after.stationary) { if (!updatedEvent.after.stationary) {
const newActiveObject: ActiveObjectType = { const newActiveObject: ObjectType = {
id: event.after.id, id: updatedEvent.after.id,
label: event.after.label, label: updatedEvent.after.label,
stationary: event.after.stationary, stationary: updatedEvent.after.stationary,
}; };
const newActiveObjects = [...activeObjects, newActiveObject]; const newActiveObjects = [...objects, newActiveObject];
setActiveObjects(newActiveObjects); setObjects(newActiveObjects);
} }
} else {
const newObjects = [...objects];
newObjects[updatedEventIndex].label =
updatedEvent.after.sub_label ?? updatedEvent.after.label;
newObjects[updatedEventIndex].stationary =
updatedEvent.after.stationary;
setObjects(newObjects);
} }
} }
}, [camera, event, activeObjects]); }, [camera, updatedEvent, objects]);
return { return {
activeTracking: hasActiveObjects, activeTracking: hasActiveObjects,
activeMotion: detectingMotion == "ON", activeMotion: detectingMotion
activeObjects, ? detectingMotion == "ON"
: initialCameraState?.motion == true,
objects,
}; };
} }

View File

@ -41,4 +41,15 @@ export interface FrigateEvent {
after: FrigateObjectState; after: FrigateObjectState;
} }
export type ObjectType = {
id: string;
label: string;
stationary: boolean;
};
export interface FrigateCameraState {
motion: boolean;
objects: ObjectType[];
}
export type ToggleableSetting = "ON" | "OFF"; export type ToggleableSetting = "ON" | "OFF";