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

WIP: Storing the relationship between recordings and events in the RecordingsToEvents table #7451

Draft
wants to merge 4 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
35 changes: 35 additions & 0 deletions migrations/019_recordings_to_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# migrations/019_recordings_to_events.py
from peewee import CharField, CompositeKey, Model


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")