Files
frigate/frigate/timeline.py
T

198 lines
7.0 KiB
Python
Raw Normal View History

2023-04-23 09:45:19 -06:00
"""Record events for object, audio, etc. detections."""
import logging
import queue
2023-05-29 12:31:17 +02:00
import threading
2023-07-16 06:42:56 -06:00
from multiprocessing import Queue
2023-05-29 12:31:17 +02:00
from multiprocessing.synchronize import Event as MpEvent
2025-05-13 16:27:20 +02:00
from typing import Any
2023-04-23 09:45:19 -06:00
from frigate.config import FrigateConfig
2024-10-01 10:07:13 -06:00
from frigate.events.maintainer import EventStateEnum, EventTypeEnum
2023-04-23 09:45:19 -06:00
from frigate.models import Timeline
2023-07-06 08:28:50 -06:00
from frigate.util.builtin import to_relative_box
2023-04-24 07:24:28 -05:00
2023-04-23 09:45:19 -06:00
logger = logging.getLogger(__name__)
class TimelineProcessor(threading.Thread):
"""Handle timeline queue and update DB."""
def __init__(
self,
config: FrigateConfig,
queue: Queue,
stop_event: MpEvent,
) -> None:
super().__init__(name="timeline_processor")
2023-04-23 09:45:19 -06:00
self.config = config
self.queue = queue
self.stop_event = stop_event
2025-05-13 16:27:20 +02:00
self.pre_event_cache: dict[str, list[dict[str, Any]]] = {}
2023-04-23 09:45:19 -06:00
def run(self) -> None:
while not self.stop_event.is_set():
try:
(
camera,
input_type,
event_type,
prev_event_data,
event_data,
) = self.queue.get(timeout=1)
except queue.Empty:
continue
2023-04-30 11:07:14 -06:00
if input_type == EventTypeEnum.tracked_object:
2024-10-01 10:07:13 -06:00
# None prev_event_data is only allowed for the start of an event
if event_type != EventStateEnum.start and prev_event_data is None:
continue
self.handle_object_detection(
camera, event_type, prev_event_data, event_data
)
elif input_type == EventTypeEnum.api:
self.handle_api_entry(camera, event_type, event_data)
2023-04-23 09:45:19 -06:00
def insert_or_save(
self,
2025-05-13 16:27:20 +02:00
entry: dict[str, Any],
prev_event_data: dict[Any, Any],
event_data: dict[Any, Any],
) -> None:
"""Insert into db or cache."""
id = entry[Timeline.source_id]
if not event_data["has_clip"] and not event_data["has_snapshot"]:
# the related event has not been saved yet, should be added to cache
if id in self.pre_event_cache.keys():
self.pre_event_cache[id].append(entry)
else:
self.pre_event_cache[id] = [entry]
else:
# the event is saved, insert to db and insert cached into db
if id in self.pre_event_cache.keys():
for e in self.pre_event_cache[id]:
Timeline.insert(e).execute()
self.pre_event_cache.pop(id)
Timeline.insert(entry).execute()
2023-04-23 09:45:19 -06:00
def handle_object_detection(
self,
camera: str,
event_type: str,
2025-05-13 16:27:20 +02:00
prev_event_data: dict[Any, Any],
event_data: dict[Any, Any],
) -> bool:
2023-04-23 09:45:19 -06:00
"""Handle object detection."""
save = False
2023-04-23 09:45:19 -06:00
camera_config = self.config.cameras[camera]
2024-01-31 05:29:18 -07:00
event_id = event_data["id"]
2023-04-23 09:45:19 -06:00
2023-04-24 07:24:28 -05:00
timeline_entry = {
Timeline.timestamp: event_data["frame_time"],
Timeline.camera: camera,
Timeline.source: "tracked_object",
2024-01-31 05:29:18 -07:00
Timeline.source_id: event_id,
2023-04-24 07:24:28 -05:00
Timeline.data: {
"box": to_relative_box(
camera_config.detect.width,
camera_config.detect.height,
event_data["box"],
),
"label": event_data["label"],
"sub_label": event_data.get("sub_label"),
2023-04-24 07:24:28 -05:00
"region": to_relative_box(
camera_config.detect.width,
camera_config.detect.height,
event_data["region"],
),
"attribute": "",
2025-11-24 07:34:56 -06:00
"score": event_data["score"],
2023-04-24 07:24:28 -05:00
},
}
2024-01-31 05:29:18 -07:00
# update sub labels for existing entries that haven't been added yet
if (
prev_event_data != None
and prev_event_data["sub_label"] != event_data["sub_label"]
and event_id in self.pre_event_cache.keys()
):
for e in self.pre_event_cache[event_id]:
e[Timeline.data]["sub_label"] = event_data["sub_label"]
2024-10-01 10:07:13 -06:00
if event_type == EventStateEnum.start:
2023-04-24 07:24:28 -05:00
timeline_entry[Timeline.class_type] = "visible"
save = True
2024-10-01 10:07:13 -06:00
elif event_type == EventStateEnum.update:
if (
len(prev_event_data["current_zones"]) < len(event_data["current_zones"])
2023-10-24 17:24:59 -06:00
and not event_data["stationary"]
):
timeline_entry[Timeline.class_type] = "entered_zone"
timeline_entry[Timeline.data]["zones"] = event_data["current_zones"]
save = True
elif prev_event_data["stationary"] != event_data["stationary"]:
timeline_entry[Timeline.class_type] = (
"stationary" if event_data["stationary"] else "active"
)
save = True
elif prev_event_data["attributes"] == {} and event_data["attributes"] != {}:
timeline_entry[Timeline.class_type] = "attribute"
timeline_entry[Timeline.data]["attribute"] = list(
event_data["attributes"].keys()
)[0]
2025-10-18 21:31:55 -06:00
if len(event_data["current_attributes"]) > 0:
timeline_entry[Timeline.data]["attribute_box"] = to_relative_box(
camera_config.detect.width,
camera_config.detect.height,
event_data["current_attributes"][0]["box"],
)
save = True
2024-10-01 10:07:13 -06:00
elif event_type == EventStateEnum.end:
timeline_entry[Timeline.class_type] = "gone"
save = True
if save:
self.insert_or_save(timeline_entry, prev_event_data, event_data)
def handle_api_entry(
self,
camera: str,
event_type: str,
2025-05-13 16:27:20 +02:00
event_data: dict[Any, Any],
) -> bool:
2025-10-08 18:06:03 -05:00
if event_type != "start":
return False
if event_data.get("type", "api") == "audio":
timeline_entry = {
Timeline.class_type: "heard",
Timeline.timestamp: event_data["start_time"],
Timeline.camera: camera,
Timeline.source: "audio",
Timeline.source_id: event_data["id"],
Timeline.data: {
"label": event_data["label"],
"sub_label": event_data.get("sub_label"),
},
}
else:
timeline_entry = {
Timeline.class_type: "external",
Timeline.timestamp: event_data["start_time"],
Timeline.camera: camera,
Timeline.source: "api",
Timeline.source_id: event_data["id"],
Timeline.data: {
"label": event_data["label"],
"sub_label": event_data.get("sub_label"),
},
}
Timeline.insert(timeline_entry).execute()
return True