Skip to content

Commit

Permalink
Add support for storing the relationship between recordings and event…
Browse files Browse the repository at this point in the history
…s in the `RecordingsToEvents` table
  • Loading branch information
skrashevich committed Aug 10, 2023
1 parent 3921a7f commit 7adea8f
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 3 deletions.
66 changes: 65 additions & 1 deletion frigate/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import logging
import os
import subprocess as sp
import tempfile
import time
import traceback
from datetime import datetime, timedelta, timezone
Expand All @@ -23,6 +24,7 @@
jsonify,
make_response,
request,
send_from_directory,
)
from peewee import DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict
Expand All @@ -38,7 +40,7 @@
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions frigate/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from peewee import (
BooleanField,
CharField,
CompositeKey,
DateTimeField,
FloatField,
IntegerField,
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions frigate/record/cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ def expire_recordings(self) -> None:
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.")
Expand Down Expand Up @@ -154,6 +162,14 @@ def expire_recordings(self) -> None:
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}.")

Expand Down
31 changes: 29 additions & 2 deletions frigate/record/maintainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
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
Expand Down Expand Up @@ -173,6 +173,13 @@ async def move_files(self) -> None:
(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:
Expand Down Expand Up @@ -221,6 +228,7 @@ async def validate_and_move_segment(
):
# 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
Expand All @@ -234,19 +242,38 @@ async def validate_and_move_segment(
# 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,
duration,
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:
Expand Down
8 changes: 8 additions & 0 deletions frigate/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ def reduce_storage_consumption(self) -> None:
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."""
Expand Down
37 changes: 37 additions & 0 deletions migrations/019_recordings_to_events.py
Original file line number Diff line number Diff line change
@@ -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")

0 comments on commit 7adea8f

Please sign in to comment.