Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metadata Timeline #6194

Merged
merged 30 commits into from Apr 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8542cdf
Create timeline table
NickM-27 Apr 20, 2023
616566a
Fix indexes
NickM-27 Apr 20, 2023
cd19b61
Add other fields
NickM-27 Apr 20, 2023
6b277c7
Adjust schema to be less descriptive
NickM-27 Apr 20, 2023
229d134
Handle timeline queue from tracked object data
NickM-27 Apr 20, 2023
cca6676
Setup timeline queue in events
NickM-27 Apr 20, 2023
98b154f
Add source id for index
NickM-27 Apr 20, 2023
b2a8f83
Add other fields
NickM-27 Apr 20, 2023
922d327
Fixes
NickM-27 Apr 20, 2023
f2d8298
Formatting
NickM-27 Apr 20, 2023
ea3e700
Store better data
NickM-27 Apr 20, 2023
2da8581
Add api with filtering
NickM-27 Apr 20, 2023
04ea6dc
Setup basic UI for timeline in events
NickM-27 Apr 20, 2023
3d37604
Cleanups
NickM-27 Apr 20, 2023
dfdf2b6
Add recordings snapshot url
NickM-27 Apr 21, 2023
5994860
Start working on timeline ui
NickM-27 Apr 21, 2023
1c826f3
Add tooltip with info
NickM-27 Apr 21, 2023
e234810
Improve icons
NickM-27 Apr 21, 2023
5d0e4f7
Fix start time with clip
NickM-27 Apr 21, 2023
50e4b82
Move player logic back to clips
NickM-27 Apr 21, 2023
a6cde7d
Make box in timeline relative coordinates
NickM-27 Apr 21, 2023
b3c4504
Make region relative
NickM-27 Apr 21, 2023
614c950
Get box overlay working
NickM-27 Apr 21, 2023
2ef1f1d
Remove overlay when playing again
NickM-27 Apr 21, 2023
fb306ed
Add disclaimer when selecting overlay points
NickM-27 Apr 21, 2023
0142aef
Add docs for new apis
NickM-27 Apr 21, 2023
b2359d1
Fix mobile
NickM-27 Apr 21, 2023
08ce46b
Fix docs
NickM-27 Apr 21, 2023
e089bae
Change color of bottom center box
NickM-27 Apr 23, 2023
be3627e
Fix vscode formatting
NickM-27 Apr 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/docs/integrations/api.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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/<camera_name>/recording/<frame_time>/snapshot.png`

Returns the snapshot image from the specific point in that cameras recordings.

### `GET /clips/<camera>-<id>.jpg`

JPG snapshot for the given camera and event id.
Expand Down
16 changes: 14 additions & 2 deletions frigate/app.py
Expand Up @@ -23,13 +23,14 @@
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
Expand Down Expand Up @@ -135,6 +136,9 @@ def init_queues(self) -> None:
# 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")
Expand All @@ -154,7 +158,7 @@ def init_database(self) -> None:
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:
Expand Down Expand Up @@ -286,12 +290,19 @@ def start_camera_capture_processes(self) -> None:
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()
Expand Down Expand Up @@ -384,6 +395,7 @@ def start(self) -> None:
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()
Expand Down
16 changes: 14 additions & 2 deletions frigate/events.py
Expand Up @@ -3,14 +3,14 @@
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
Expand Down Expand Up @@ -48,6 +48,7 @@ def __init__(
camera_processes: dict[str, CameraMetricsTypes],
event_queue: Queue,
event_processed_queue: Queue,
timeline_queue: Queue,
stop_event: MpEvent,
):
threading.Thread.__init__(self)
Expand All @@ -56,6 +57,7 @@ def __init__(
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

Expand All @@ -73,6 +75,16 @@ def run(self) -> None:

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":
Expand Down
85 changes: 84 additions & 1 deletion frigate/http.py
Expand Up @@ -33,7 +33,7 @@

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 (
Expand Down Expand Up @@ -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("/<camera_name>/<label>/best.jpg")
@bp.route("/<camera_name>/<label>/thumbnail.jpg")
def label_thumbnail(camera_name, label):
Expand Down Expand Up @@ -924,6 +960,53 @@ def latest_frame(camera_name):
return "Camera named {} not found".format(camera_name), 404


@bp.route("/<camera_name>/recordings/<frame_time>/snapshot.png")
def get_snapshot_from_recording(camera_name: str, frame_time: str):
if camera_name not in current_app.frigate_config.cameras:
return "Camera named {} not found".format(camera_name), 404

frame_time = float(frame_time)
recording_query = (
Recordings.select()
.where(
((frame_time > Recordings.start_time) & (frame_time < Recordings.end_time))
)
.where(Recordings.camera == camera_name)
)

try:
recording: Recordings = recording_query.get()
time_in_segment = frame_time - recording.start_time

ffmpeg_cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel",
"warning",
"-ss",
f"00:00:{time_in_segment}",
"-i",
recording.path,
"-frames:v",
"1",
"-c:v",
"png",
"-f",
"image2pipe",
"-",
]

process = sp.run(
ffmpeg_cmd,
capture_output=True,
)
response = make_response(process.stdout)
response.headers["Content-Type"] = "image/png"
return response
except DoesNotExist:
return "Recording not found for {} at {}".format(camera_name, frame_time), 404


@bp.route("/recordings/storage", methods=["GET"])
def get_recordings_storage_usage():
recording_stats = stats_snapshot(
Expand Down
9 changes: 9 additions & 0 deletions frigate/models.py
Expand Up @@ -32,6 +32,15 @@ class Event(Model): # type: ignore[misc]
plus_id = CharField(max_length=30)


class Timeline(Model): # type: ignore[misc]
timestamp = DateTimeField()
camera = CharField(index=True, max_length=20)
source = CharField(index=True, max_length=20) # ex: tracked object, audio, external
source_id = CharField(index=True, max_length=30)
class_type = CharField(max_length=50) # ex: entered_zone, audio_heard
data = JSONField() # ex: tracked object id, region, box, etc.


class Recordings(Model): # type: ignore[misc]
id = CharField(null=False, primary_key=True, max_length=30)
camera = CharField(index=True, max_length=20)
Expand Down