From fbaab71d78048e41dc9b3d40a714d092184cf7aa Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 23 Apr 2023 09:45:19 -0600 Subject: [PATCH] Metadata Timeline (#6194) * Create timeline table * Fix indexes * Add other fields * Adjust schema to be less descriptive * Handle timeline queue from tracked object data * Setup timeline queue in events * Add source id for index * Add other fields * Fixes * Formatting * Store better data * Add api with filtering * Setup basic UI for timeline in events * Cleanups * Add recordings snapshot url * Start working on timeline ui * Add tooltip with info * Improve icons * Fix start time with clip * Move player logic back to clips * Make box in timeline relative coordinates * Make region relative * Get box overlay working * Remove overlay when playing again * Add disclaimer when selecting overlay points * Add docs for new apis * Fix mobile * Fix docs * Change color of bottom center box * Fix vscode formatting --- docs/docs/integrations/api.md | 14 +++ frigate/app.py | 16 ++- frigate/events.py | 16 ++- frigate/http.py | 85 +++++++++++++- frigate/models.py | 9 ++ frigate/timeline.py | 140 ++++++++++++++++++++++++ migrations/013_create_timeline_table.py | 48 ++++++++ web/src/components/TimelineSummary.jsx | 95 ++++++++++++++++ web/src/components/Tooltip.jsx | 2 +- web/src/icons/Exit.jsx | 12 ++ web/src/icons/Play.jsx | 4 +- web/src/routes/Events.jsx | 76 ++++++++++--- 12 files changed, 494 insertions(+), 23 deletions(-) create mode 100644 frigate/timeline.py create mode 100644 migrations/013_create_timeline_table.py create mode 100644 web/src/components/TimelineSummary.jsx create mode 100644 web/src/icons/Exit.jsx diff --git a/docs/docs/integrations/api.md b/docs/docs/integrations/api.md index 51887b14b..2b89288db 100644 --- a/docs/docs/integrations/api.md +++ b/docs/docs/integrations/api.md @@ -168,6 +168,16 @@ Events from the database. Accepts the following query string parameters: | `include_thumbnails` | int | Include thumbnails in the response (0 or 1) | | `in_progress` | int | Limit to events in progress (0 or 1) | +### `GET /api/timeline` + +Timeline of key moments of an event(s) from the database. Accepts the following query string parameters: + +| param | Type | Description | +| -------------------- | ---- | --------------------------------------------- | +| `camera` | int | Name of camera | +| `source_id` | str | ID of tracked object | +| `limit` | int | Limit the number of events returned | + ### `GET /api/events/summary` Returns summary data for events in the database. Used by the Home Assistant integration. @@ -233,6 +243,10 @@ Accepts the following query string parameters, but they are only applied when an Returns the snapshot image from the latest event for the given camera and label combo. Using `any` as the label will return the latest thumbnail regardless of type. +### `GET /api//recording//snapshot.png` + +Returns the snapshot image from the specific point in that cameras recordings. + ### `GET /clips/-.jpg` JPG snapshot for the given camera and event id. diff --git a/frigate/app.py b/frigate/app.py index ef77cd1dd..05e6c6d9c 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -23,13 +23,14 @@ from frigate.object_detection import ObjectDetectProcess from frigate.events import EventCleanup, EventProcessor from frigate.http import create_app from frigate.log import log_process, root_configurer -from frigate.models import Event, Recordings +from frigate.models import Event, Recordings, Timeline from frigate.object_processing import TrackedObjectProcessor from frigate.output import output_frames from frigate.plus import PlusApi from frigate.record import RecordingCleanup, RecordingMaintainer from frigate.stats import StatsEmitter, stats_init from frigate.storage import StorageMaintainer +from frigate.timeline import TimelineProcessor from frigate.version import VERSION from frigate.video import capture_camera, track_camera from frigate.watchdog import FrigateWatchdog @@ -135,6 +136,9 @@ class FrigateApp: # Queue for recordings info self.recordings_info_queue: Queue = mp.Queue() + # Queue for timeline events + self.timeline_queue: Queue = mp.Queue() + def init_database(self) -> None: # Migrate DB location old_db_path = os.path.join(CLIPS_DIR, "frigate.db") @@ -154,7 +158,7 @@ class FrigateApp: migrate_db.close() self.db = SqliteQueueDatabase(self.config.database.path) - models = [Event, Recordings] + models = [Event, Recordings, Timeline] self.db.bind(models) def init_stats(self) -> None: @@ -286,12 +290,19 @@ class FrigateApp: capture_process.start() logger.info(f"Capture process started for {name}: {capture_process.pid}") + def start_timeline_processor(self) -> None: + self.timeline_processor = TimelineProcessor( + self.config, self.timeline_queue, self.stop_event + ) + self.timeline_processor.start() + def start_event_processor(self) -> None: self.event_processor = EventProcessor( self.config, self.camera_metrics, self.event_queue, self.event_processed_queue, + self.timeline_queue, self.stop_event, ) self.event_processor.start() @@ -384,6 +395,7 @@ class FrigateApp: self.start_storage_maintainer() self.init_stats() self.init_web_server() + self.start_timeline_processor() self.start_event_processor() self.start_event_cleanup() self.start_recording_maintainer() diff --git a/frigate/events.py b/frigate/events.py index f502c4ded..416b28fd7 100644 --- a/frigate/events.py +++ b/frigate/events.py @@ -3,14 +3,14 @@ import logging import os import queue import threading -import time from pathlib import Path from peewee import fn -from frigate.config import EventsConfig, FrigateConfig, RecordConfig +from frigate.config import EventsConfig, FrigateConfig from frigate.const import CLIPS_DIR from frigate.models import Event +from frigate.timeline import TimelineSourceEnum from frigate.types import CameraMetricsTypes from multiprocessing.queues import Queue @@ -48,6 +48,7 @@ class EventProcessor(threading.Thread): camera_processes: dict[str, CameraMetricsTypes], event_queue: Queue, event_processed_queue: Queue, + timeline_queue: Queue, stop_event: MpEvent, ): threading.Thread.__init__(self) @@ -56,6 +57,7 @@ class EventProcessor(threading.Thread): self.camera_processes = camera_processes self.event_queue = event_queue self.event_processed_queue = event_processed_queue + self.timeline_queue = timeline_queue self.events_in_process: Dict[str, Event] = {} self.stop_event = stop_event @@ -73,6 +75,16 @@ class EventProcessor(threading.Thread): logger.debug(f"Event received: {event_type} {camera} {event_data['id']}") + self.timeline_queue.put( + ( + camera, + TimelineSourceEnum.tracked_object, + event_type, + self.events_in_process.get(event_data["id"]), + event_data, + ) + ) + event_config: EventsConfig = self.config.cameras[camera].record.events if event_type == "start": diff --git a/frigate/http.py b/frigate/http.py index f59e341df..cb8c1cbe8 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -33,7 +33,7 @@ from playhouse.shortcuts import model_to_dict from frigate.config import FrigateConfig from frigate.const import CLIPS_DIR, MAX_SEGMENT_DURATION, RECORD_DIR -from frigate.models import Event, Recordings +from frigate.models import Event, Recordings, Timeline from frigate.object_processing import TrackedObject from frigate.stats import stats_snapshot from frigate.util import ( @@ -414,6 +414,42 @@ def event_thumbnail(id, max_cache_age=2592000): return response +@bp.route("/timeline") +def timeline(): + camera = request.args.get("camera", "all") + source_id = request.args.get("source_id", type=str) + limit = request.args.get("limit", 100) + + clauses = [] + + selected_columns = [ + Timeline.timestamp, + Timeline.camera, + Timeline.source, + Timeline.source_id, + Timeline.class_type, + Timeline.data, + ] + + if camera != "all": + clauses.append((Timeline.camera == camera)) + + if source_id: + clauses.append((Timeline.source_id == source_id)) + + if len(clauses) == 0: + clauses.append((True)) + + timeline = ( + Timeline.select(*selected_columns) + .where(reduce(operator.and_, clauses)) + .order_by(Timeline.timestamp.asc()) + .limit(limit) + ) + + return jsonify([model_to_dict(t) for t in timeline]) + + @bp.route("//