Files
frigate/frigate/timeline.py
T

233 lines
8.6 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
2026-03-25 18:30:59 -06:00
from frigate.events.types 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
2026-03-25 18:30:59 -06:00
self.pre_event_cache: dict[str, list[dict[Any, 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,
2026-03-25 18:30:59 -06:00
entry: dict[Any, Any],
2025-05-13 16:27:20 +02:00
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],
2026-03-25 18:30:59 -06:00
) -> None:
2023-04-23 09:45:19 -06:00
"""Handle object detection."""
2026-03-04 10:07:34 -06:00
camera_config = self.config.cameras.get(camera)
2026-03-25 18:30:59 -06:00
if (
camera_config is None
or camera_config.detect.width is None
or camera_config.detect.height is None
):
return
2024-01-31 05:29:18 -07:00
event_id = event_data["id"]
2023-04-23 09:45:19 -06:00
2025-12-19 18:59:26 -06:00
# Base timeline entry data that all entries will share
base_entry = {
2023-04-24 07:24:28 -05:00
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"],
"computed_score": event_data.get("computed_score"),
"top_score": event_data.get("top_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:
2025-12-19 18:59:26 -06:00
timeline_entry = base_entry.copy()
2023-04-24 07:24:28 -05:00
timeline_entry[Timeline.class_type] = "visible"
2025-12-19 18:59:26 -06:00
self.insert_or_save(timeline_entry, prev_event_data, event_data)
2024-10-01 10:07:13 -06:00
elif event_type == EventStateEnum.update:
2025-12-19 18:59:26 -06:00
# Check all conditions and create timeline entries for each change
entries_to_save = []
# Check for zone changes
prev_zones = set(prev_event_data["current_zones"])
current_zones = set(event_data["current_zones"])
zones_changed = prev_zones != current_zones
# Only save "entered_zone" events when the object is actually IN zones
if (
2025-12-19 18:59:26 -06:00
zones_changed
2023-10-24 17:24:59 -06:00
and not event_data["stationary"]
2025-12-19 18:59:26 -06:00
and len(current_zones) > 0
):
2025-12-19 18:59:26 -06:00
zone_entry = base_entry.copy()
zone_entry[Timeline.class_type] = "entered_zone"
zone_entry[Timeline.data] = base_entry[Timeline.data].copy()
zone_entry[Timeline.data]["zones"] = event_data["current_zones"]
entries_to_save.append(zone_entry)
# Check for stationary status change
if prev_event_data["stationary"] != event_data["stationary"]:
stationary_entry = base_entry.copy()
stationary_entry[Timeline.class_type] = (
"stationary" if event_data["stationary"] else "active"
)
2025-12-19 18:59:26 -06:00
stationary_entry[Timeline.data] = base_entry[Timeline.data].copy()
entries_to_save.append(stationary_entry)
# Check for new attributes
if prev_event_data["attributes"] == {} and event_data["attributes"] != {}:
attribute_entry = base_entry.copy()
attribute_entry[Timeline.class_type] = "attribute"
attribute_entry[Timeline.data] = base_entry[Timeline.data].copy()
attribute_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:
2025-12-19 18:59:26 -06:00
attribute_entry[Timeline.data]["attribute_box"] = to_relative_box(
2025-10-18 21:31:55 -06:00
camera_config.detect.width,
camera_config.detect.height,
event_data["current_attributes"][0]["box"],
)
2025-12-19 18:59:26 -06:00
entries_to_save.append(attribute_entry)
2025-12-19 18:59:26 -06:00
# Save all entries
for entry in entries_to_save:
self.insert_or_save(entry, prev_event_data, event_data)
elif event_type == EventStateEnum.end:
timeline_entry = base_entry.copy()
timeline_entry[Timeline.class_type] = "gone"
self.insert_or_save(timeline_entry, prev_event_data, event_data)
# drop entries for events that ended without being saved
self.pre_event_cache.pop(event_id, None)
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