Support reading initial camera state

This commit is contained in:
Nicolas Mowen 2024-04-30 06:50:23 -06:00
parent 839f35f0a1
commit 5c7d6556c5
6 changed files with 94 additions and 50 deletions

View File

@ -52,7 +52,7 @@ class WebSocketClient(Communicator): # type: ignore[misc]
def opened(self) -> None: def opened(self) -> None:
"""A new websocket is opened, we need to send an update message""" """A new websocket is opened, we need to send an update message"""
self.receiver("onConnect", "") 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:

View File

@ -727,8 +727,9 @@ 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 = { camera_activity: dict[str, list[any]] = {
"motion": len(motion_boxes) > 0, "motion": len(motion_boxes) > 0,
"objects": [],
} }
for obj in tracked_objects.values(): for obj in tracked_objects.values():
@ -747,13 +748,9 @@ class CameraState:
): ):
label = obj.obj_data["sub_label"] label = obj.obj_data["sub_label"]
if label not in camera_activity: camera_activity["objects"].append(
camera_activity[label] = { {"id": obj.obj_data["id"], "label": label, "stationary": not active}
"active": 1 if active else 0, )
"stationary": 1 if not active else 0,
}
else:
camera_activity[label]["active" if active else "stationary"] += 1
# 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:

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

@ -46,7 +46,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(
@ -163,7 +163,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">
@ -174,7 +174,7 @@ export default function LivePlayer({
> >
{[ {[
...new Set([ ...new Set([
...(activeObjects || []).map(({ label }) => label), ...(objects || []).map(({ label }) => label),
]), ]),
] ]
.map((label) => { .map((label) => {
@ -186,11 +186,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, updatedEvent, objects]);
}, [camera, event, activeObjects]);
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";