mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-07 03:11:15 +03:00
Refactor manual event api to use ZMQ (#17105)
* Don't wait for topic * Refactor object processing and camera state * Move manual event handling to camera state / tracked object * Cleanup * Refactor audio to use internal zmq * Cleanup * Clenaup * Cleanup * Quick label fix * Fix tests * Cleanup
This commit is contained in:
+39
-36
@@ -2,17 +2,22 @@
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import threading
|
||||
import time
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
import frigate.util as util
|
||||
from frigate.camera import CameraMetrics
|
||||
from frigate.comms.config_updater import ConfigSubscriber
|
||||
from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum
|
||||
from frigate.comms.event_metadata_updater import (
|
||||
EventMetadataPublisher,
|
||||
EventMetadataTypeEnum,
|
||||
)
|
||||
from frigate.comms.inter_process import InterProcessRequestor
|
||||
from frigate.config import CameraConfig, CameraInput, FfmpegConfig
|
||||
from frigate.const import (
|
||||
@@ -21,7 +26,6 @@ from frigate.const import (
|
||||
AUDIO_MAX_BIT_RANGE,
|
||||
AUDIO_MIN_CONFIDENCE,
|
||||
AUDIO_SAMPLE_RATE,
|
||||
FRIGATE_LOCALHOST,
|
||||
)
|
||||
from frigate.ffmpeg_presets import parse_preset_input
|
||||
from frigate.log import LogPipe
|
||||
@@ -139,6 +143,7 @@ class AudioEventMaintainer(threading.Thread):
|
||||
f"config/enabled/{camera.name}", True
|
||||
)
|
||||
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio)
|
||||
self.event_metadata_publisher = EventMetadataPublisher()
|
||||
|
||||
self.was_enabled = camera.enabled
|
||||
|
||||
@@ -207,24 +212,33 @@ class AudioEventMaintainer(threading.Thread):
|
||||
datetime.datetime.now().timestamp()
|
||||
)
|
||||
else:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
rand_id = "".join(
|
||||
random.choices(string.ascii_lowercase + string.digits, k=6)
|
||||
)
|
||||
event_id = f"{now}-{rand_id}"
|
||||
self.requestor.send_data(f"{self.config.name}/audio/{label}", "ON")
|
||||
|
||||
resp = requests.post(
|
||||
f"{FRIGATE_LOCALHOST}/api/events/{self.config.name}/{label}/create",
|
||||
json={"duration": None, "score": score, "source_type": "audio"},
|
||||
self.event_metadata_publisher.publish(
|
||||
EventMetadataTypeEnum.manual_event_create,
|
||||
(
|
||||
now,
|
||||
self.config.name,
|
||||
label,
|
||||
event_id,
|
||||
True,
|
||||
score,
|
||||
None,
|
||||
None,
|
||||
"audio",
|
||||
{},
|
||||
),
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
event_id = resp.json()["event_id"]
|
||||
self.detections[label] = {
|
||||
"id": event_id,
|
||||
"label": label,
|
||||
"last_detection": datetime.datetime.now().timestamp(),
|
||||
}
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"Failed to create audio event with status code {resp.status_code}"
|
||||
)
|
||||
self.detections[label] = {
|
||||
"id": event_id,
|
||||
"label": label,
|
||||
"last_detection": now,
|
||||
}
|
||||
|
||||
def expire_detections(self) -> None:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
@@ -241,17 +255,11 @@ class AudioEventMaintainer(threading.Thread):
|
||||
f"{self.config.name}/audio/{detection['label']}", "OFF"
|
||||
)
|
||||
|
||||
resp = requests.put(
|
||||
f"{FRIGATE_LOCALHOST}/api/events/{detection['id']}/end",
|
||||
json={"end_time": detection["last_detection"]},
|
||||
self.event_metadata_publisher.publish(
|
||||
EventMetadataTypeEnum.manual_event_end,
|
||||
(detection["id"], detection["last_detection"]),
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
self.detections[detection["label"]] = None
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"Failed to end audio event {detection['id']} with status code {resp.status_code}"
|
||||
)
|
||||
self.detections[detection["label"]] = None
|
||||
|
||||
def expire_all_detections(self) -> None:
|
||||
"""Immediately end all current detections"""
|
||||
@@ -259,16 +267,11 @@ class AudioEventMaintainer(threading.Thread):
|
||||
for label, detection in list(self.detections.items()):
|
||||
if detection:
|
||||
self.requestor.send_data(f"{self.config.name}/audio/{label}", "OFF")
|
||||
resp = requests.put(
|
||||
f"{FRIGATE_LOCALHOST}/api/events/{detection['id']}/end",
|
||||
json={"end_time": now},
|
||||
self.event_metadata_publisher.publish(
|
||||
EventMetadataTypeEnum.manual_event_end,
|
||||
(detection["id"], now),
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
self.detections[label] = None
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"Failed to end audio event {detection['id']} with status code {resp.status_code}"
|
||||
)
|
||||
self.detections[label] = None
|
||||
|
||||
def start_or_restart_ffmpeg(self) -> None:
|
||||
self.audio_listener = start_or_restart_ffmpeg(
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
"""Handle external events created by the user."""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
from numpy import ndarray
|
||||
|
||||
from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum
|
||||
from frigate.comms.events_updater import EventUpdatePublisher
|
||||
from frigate.config import CameraConfig, FrigateConfig
|
||||
from frigate.const import CLIPS_DIR, THUMB_DIR
|
||||
from frigate.events.types import EventStateEnum, EventTypeEnum
|
||||
from frigate.util.image import draw_box_with_label
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ManualEventState(str, Enum):
|
||||
complete = "complete"
|
||||
start = "start"
|
||||
end = "end"
|
||||
|
||||
|
||||
class ExternalEventProcessor:
|
||||
def __init__(self, config: FrigateConfig) -> None:
|
||||
self.config = config
|
||||
self.default_thumbnail = None
|
||||
self.event_sender = EventUpdatePublisher()
|
||||
self.detection_updater = DetectionPublisher(DetectionTypeEnum.api)
|
||||
self.event_camera = {}
|
||||
|
||||
def create_manual_event(
|
||||
self,
|
||||
camera: str,
|
||||
label: str,
|
||||
source_type: str,
|
||||
sub_label: Optional[str],
|
||||
score: int,
|
||||
duration: Optional[int],
|
||||
include_recording: bool,
|
||||
draw: dict[str, any],
|
||||
snapshot_frame: Optional[ndarray],
|
||||
) -> str:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
camera_config = self.config.cameras.get(camera)
|
||||
|
||||
# create event id and start frame time
|
||||
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
event_id = f"{now}-{rand_id}"
|
||||
|
||||
self._write_images(camera_config, label, event_id, draw, snapshot_frame)
|
||||
end = now + duration if duration is not None else None
|
||||
|
||||
self.event_sender.publish(
|
||||
(
|
||||
EventTypeEnum.api,
|
||||
EventStateEnum.start,
|
||||
camera,
|
||||
"",
|
||||
{
|
||||
"id": event_id,
|
||||
"label": label,
|
||||
"sub_label": sub_label,
|
||||
"score": score,
|
||||
"camera": camera,
|
||||
"start_time": now - camera_config.record.event_pre_capture,
|
||||
"end_time": end,
|
||||
"has_clip": camera_config.record.enabled and include_recording,
|
||||
"has_snapshot": True,
|
||||
"type": source_type,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
if source_type == "api":
|
||||
self.event_camera[event_id] = camera
|
||||
self.detection_updater.publish(
|
||||
(
|
||||
camera,
|
||||
now,
|
||||
{
|
||||
"state": (
|
||||
ManualEventState.complete if end else ManualEventState.start
|
||||
),
|
||||
"label": f"{label}: {sub_label}" if sub_label else label,
|
||||
"event_id": event_id,
|
||||
"end_time": end,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return event_id
|
||||
|
||||
def finish_manual_event(self, event_id: str, end_time: float) -> None:
|
||||
"""Finish external event with indeterminate duration."""
|
||||
self.event_sender.publish(
|
||||
(
|
||||
EventTypeEnum.api,
|
||||
EventStateEnum.end,
|
||||
None,
|
||||
"",
|
||||
{"id": event_id, "end_time": end_time},
|
||||
)
|
||||
)
|
||||
|
||||
if event_id in self.event_camera:
|
||||
self.detection_updater.publish(
|
||||
(
|
||||
self.event_camera[event_id],
|
||||
end_time,
|
||||
{
|
||||
"state": ManualEventState.end,
|
||||
"event_id": event_id,
|
||||
"end_time": end_time,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.event_camera.pop(event_id)
|
||||
|
||||
def _write_images(
|
||||
self,
|
||||
camera_config: CameraConfig,
|
||||
label: str,
|
||||
event_id: str,
|
||||
draw: dict[str, any],
|
||||
img_frame: Optional[ndarray],
|
||||
) -> None:
|
||||
if img_frame is None:
|
||||
return
|
||||
|
||||
# write clean snapshot if enabled
|
||||
if camera_config.snapshots.clean_copy:
|
||||
ret, png = cv2.imencode(".png", img_frame)
|
||||
|
||||
if ret:
|
||||
with open(
|
||||
os.path.join(
|
||||
CLIPS_DIR,
|
||||
f"{camera_config.name}-{event_id}-clean.png",
|
||||
),
|
||||
"wb",
|
||||
) as p:
|
||||
p.write(png.tobytes())
|
||||
|
||||
# write jpg snapshot with optional annotations
|
||||
if draw.get("boxes") and isinstance(draw.get("boxes"), list):
|
||||
for box in draw.get("boxes"):
|
||||
x = int(box["box"][0] * camera_config.detect.width)
|
||||
y = int(box["box"][1] * camera_config.detect.height)
|
||||
width = int(box["box"][2] * camera_config.detect.width)
|
||||
height = int(box["box"][3] * camera_config.detect.height)
|
||||
|
||||
draw_box_with_label(
|
||||
img_frame,
|
||||
x,
|
||||
y,
|
||||
x + width,
|
||||
y + height,
|
||||
label,
|
||||
f"{box.get('score', '-')}% {int(width * height)}",
|
||||
thickness=2,
|
||||
color=box.get("color", (255, 0, 0)),
|
||||
)
|
||||
|
||||
ret, jpg = cv2.imencode(".jpg", img_frame)
|
||||
with open(
|
||||
os.path.join(CLIPS_DIR, f"{camera_config.name}-{event_id}.jpg"),
|
||||
"wb",
|
||||
) as j:
|
||||
j.write(jpg.tobytes())
|
||||
|
||||
# create thumbnail with max height of 175 and save
|
||||
width = int(175 * img_frame.shape[1] / img_frame.shape[0])
|
||||
thumb = cv2.resize(img_frame, dsize=(width, 175), interpolation=cv2.INTER_AREA)
|
||||
cv2.imwrite(
|
||||
os.path.join(THUMB_DIR, camera_config.name, f"{event_id}.webp"), thumb
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
self.event_sender.stop()
|
||||
self.detection_updater.stop()
|
||||
Reference in New Issue
Block a user