Add support for storing the relationship between recordings and events in the RecordingsToEvents table

This commit is contained in:
Sergey Krashevich 2023-08-11 00:54:18 +03:00
parent 3921a7faa2
commit 7adea8f3c9
No known key found for this signature in database
GPG Key ID: 625171324E7D3856
6 changed files with 165 additions and 3 deletions

View File

@ -5,6 +5,7 @@ import json
import logging
import os
import subprocess as sp
import tempfile
import time
import traceback
from datetime import datetime, timedelta, timezone
@ -23,6 +24,7 @@ from flask import (
jsonify,
make_response,
request,
send_from_directory,
)
from peewee import DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict
@ -38,7 +40,7 @@ from frigate.const import (
RECORD_DIR,
)
from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, Recordings, Timeline
from frigate.models import Event, Recordings, RecordingsToEvents, Timeline
from frigate.object_processing import TrackedObject
from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController
@ -699,6 +701,66 @@ def label_snapshot(camera_name, label):
return response
@bp.route("/events/<id>/record.mp3")
def event_audio(id):
download = request.args.get("download", type=bool)
try:
event: Event = Event.get(Event.id == id)
except DoesNotExist:
return "Event not found.", 404
recordings = (
Recordings.select(Recordings.path)
.join(RecordingsToEvents, on=(Recordings.id == RecordingsToEvents.recording_id))
.where(RecordingsToEvents.event_id == event.id)
)
# Extract file paths from the query
file_paths = [rec.path for rec in recordings]
# Generate a temporary output file name for the combined MP3
output_file = tempfile.NamedTemporaryFile(
prefix=id, suffix=".mp3", delete=False
).name
os.unlink(output_file) # fucking python
# Create a list of inputs for FFmpeg
ffmpeg_inputs = sum([["-i", path] for path in file_paths], [])
# Use FFmpeg to extract audio from each mp4 and combine into a single MP3
cmd = [
"ffmpeg",
"-y", # fucking python #2
*ffmpeg_inputs,
"-filter_complex",
"concat=n={}:v=0:a=1[aout]".format(len(file_paths)),
"-map",
"[aout]",
"-vn",
output_file,
]
logger.debug(f"ffmpeg command for {id}/record.mp3: {cmd}")
sp.run(cmd)
if not os.path.exists(output_file):
return "Error processing audio files.", 500
# Trigger download if requested
if download:
return send_from_directory(
os.path.dirname(output_file),
os.path.basename(output_file),
as_attachment=True,
attachment_filename=f"event-{id}.mp3",
)
# Otherwise, just return the combined file's path or content
# Depending on your needs, you can directly stream the audio or just provide the path
return send_from_directory(
os.path.dirname(output_file), os.path.basename(output_file)
)
@bp.route("/events/<id>/clip.mp4")
def event_clip(id):
download = request.args.get("download", type=bool)
@ -1167,6 +1229,8 @@ def latest_frame(camera_name):
"motion_boxes": request.args.get("motion", type=int),
"regions": request.args.get("regions", type=int),
}
# TODO: debug print draw_options
logger.debug(f"Drawing options for {camera_name}: {draw_options}")
resize_quality = request.args.get("quality", default=70, type=int)
if camera_name in current_app.frigate_config.cameras:

View File

@ -1,6 +1,7 @@
from peewee import (
BooleanField,
CharField,
CompositeKey,
DateTimeField,
FloatField,
IntegerField,
@ -70,6 +71,15 @@ class Recordings(Model): # type: ignore[misc]
segment_size = FloatField(default=0) # this should be stored as MB
class RecordingsToEvents(Model): # type: ignore[misc]
event_id = CharField(null=False, index=True, max_length=30)
recording_id = CharField(null=False, index=True, max_length=30)
class Meta:
db_table = "recordingstoevents"
primary_key = CompositeKey("event", "recording")
# Used for temporary table in record/cleanup.py
class RecordingsToDelete(Model): # type: ignore[misc]
id = CharField(null=False, primary_key=False, max_length=30)

View File

@ -66,6 +66,14 @@ class RecordingCleanup(threading.Thread):
Recordings.delete().where(
Recordings.id << deleted_recordings_list[i : i + max_deletes]
).execute()
"""
TODO: right way
RecordingsToEvents.update(is_deleted=True).where(
RecordingsToEvents.recording_id
<< deleted_recordings_list[i : i + max_deletes]
).execute()
"""
logger.debug("End deleted cameras.")
logger.debug("Start all cameras.")
@ -154,6 +162,14 @@ class RecordingCleanup(threading.Thread):
Recordings.delete().where(
Recordings.id << deleted_recordings_list[i : i + max_deletes]
).execute()
"""
TODO: right way
RecordingsToEvents.update(is_deleted=True).where(
RecordingsToEvents.recording_id
<< deleted_recordings_list[i : i + max_deletes]
).execute()
"""
logger.debug(f"End camera: {camera}.")

View File

@ -24,7 +24,7 @@ from frigate.const import (
MAX_SEGMENT_DURATION,
RECORD_DIR,
)
from frigate.models import Event, Recordings
from frigate.models import Event, Recordings, RecordingsToEvents
from frigate.types import FeatureMetricsTypes
from frigate.util.image import area
from frigate.util.services import get_video_properties
@ -173,6 +173,13 @@ class RecordingMaintainer(threading.Thread):
(INSERT_MANY_RECORDINGS, [r for r in recordings_to_insert if r is not None])
)
def store_recording_to_event_relation(
self, recording_id: str, event_id: str
) -> None:
"""Store the relationship between a recording and an event in the RecordingsToEvents table."""
relation = RecordingsToEvents(recording=recording_id, event=event_id)
relation.save()
async def validate_and_move_segment(
self, camera: str, events: Event, recording: dict[str, any]
) -> None:
@ -221,6 +228,7 @@ class RecordingMaintainer(threading.Thread):
):
# if the cached segment overlaps with the events:
overlaps = False
overlapping_event_id = None
for event in events:
# if the event starts in the future, stop checking events
# and remove this segment
@ -234,12 +242,13 @@ class RecordingMaintainer(threading.Thread):
# and stop looking at events
if event.end_time is None or event.end_time >= start_time.timestamp():
overlaps = True
overlapping_event_id = event.id
break
if overlaps:
record_mode = self.config.cameras[camera].record.events.retain.mode
# move from cache to recordings immediately
return await self.move_segment(
recording_result = await self.move_segment(
camera,
start_time,
end_time,
@ -247,6 +256,24 @@ class RecordingMaintainer(threading.Thread):
cache_path,
record_mode,
)
if recording_result:
try:
# Store the relation in the RecordingsToEvents table
self.store_recording_to_event_relation(
recording_result.id, overlapping_event_id
)
logging.debug(
f"Successfully stored relation for recording_id: {recording_result}, event_id: {overlapping_event_id} in RecordingsToEvents table"
)
except Exception as e:
logging.error(
f"Failed to store relation r:{recording_result},e:{overlapping_event_id} in RecordingsToEvents table: {str(e)}"
)
else:
logging.debug(
f"No recording result available for overlapping event_id {overlapping_event_id}"
)
return recording_result
# if it doesn't overlap with an event, go ahead and drop the segment
# if it ends more than the configured pre_capture for the camera
else:

View File

@ -173,6 +173,14 @@ class StorageMaintainer(threading.Thread):
Recordings.delete().where(
Recordings.id << deleted_recordings_list[i : i + max_deletes]
).execute()
"""
TODO: right way
RecordingsToEvents.update(is_deleted=True).where(
RecordingsToEvents.recording_id
<< deleted_recordings_list[i : i + max_deletes]
).execute()
"""
def run(self):
"""Check every 5 minutes if storage needs to be cleaned up."""

View File

@ -0,0 +1,37 @@
# migrations/019_recordings_to_events.py
from peewee import CharField, CompositeKey, Model
from frigate.models import Recordings, Event
def migrate(migrator, database, fake=False, **kwargs):
"""Write your migrations here."""
@migrator.create_model
class RecordingsToEvents(Model): # type: ignore[misc]
event_id = CharField(null=False, index=True, max_length=30)
recording_id = CharField(null=False, index=True, max_length=30)
class Meta:
db_table = "recordingstoevents"
primary_key = CompositeKey("event_id", "recording_id")
sql = """
INSERT INTO recordingstoevents (recording_id, event_id)
SELECT
r.id AS recording,
e.id AS event
FROM
event e
JOIN
recordings r ON e.camera = r.camera
WHERE
r.start_time <= e.end_time
AND r.end_time >= e.start_time;
"""
migrator.sql(sql)
def rollback(migrator, database, fake=False, **kwargs):
"""This function is used to undo the migration, i.e., to drop the RecordingToEvent table."""
migrator.drop_table("recordingstoevents")