From 1b0d2f8c548a5ba0ed8a8e0acd8822a772085eea Mon Sep 17 00:00:00 2001 From: Fabien Del Olmo Date: Wed, 29 Jul 2026 18:58:57 +0200 Subject: [PATCH] Improve recording timeline and VOD query performance --- frigate/api/media.py | 8 ++-- frigate/api/record.py | 3 +- frigate/test/http_api/test_http_media.py | 51 ++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/frigate/api/media.py b/frigate/api/media.py index 8d56475c59..dd52a6d5e6 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -574,11 +574,11 @@ async def vod_ts( Recordings.start_time, ) .where( - Recordings.start_time.between(start_ts, end_ts) - | Recordings.end_time.between(start_ts, end_ts) - | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + Recordings.camera == camera_name, + Recordings.start_time >= start_ts - MAX_SEGMENT_DURATION, + Recordings.start_time <= end_ts, + Recordings.end_time >= start_ts, ) - .where(Recordings.camera == camera_name) .order_by(Recordings.start_time.asc()) .iterator() ) diff --git a/frigate/api/record.py b/frigate/api/record.py index 5db257f482..4913223bf3 100644 --- a/frigate/api/record.py +++ b/frigate/api/record.py @@ -25,7 +25,7 @@ from frigate.api.defs.query.recordings_query_parameters import ( ) from frigate.api.defs.response.generic_response import GenericResponse from frigate.api.defs.tags import Tags -from frigate.const import RECORD_DIR +from frigate.const import MAX_SEGMENT_DURATION, RECORD_DIR from frigate.models import Event, Recordings from frigate.util.time import get_dst_transitions @@ -243,6 +243,7 @@ async def recordings( ) .where( Recordings.camera == camera_name, + Recordings.start_time >= after - MAX_SEGMENT_DURATION, Recordings.end_time >= after, Recordings.start_time <= before, ) diff --git a/frigate/test/http_api/test_http_media.py b/frigate/test/http_api/test_http_media.py index b2d83cbc8a..6a05efd136 100644 --- a/frigate/test/http_api/test_http_media.py +++ b/frigate/test/http_api/test_http_media.py @@ -1,11 +1,13 @@ """Unit tests for recordings/media API endpoints.""" from datetime import UTC, datetime +from unittest.mock import patch import pytz from fastapi import Request from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user +from frigate.const import MAX_SEGMENT_DURATION from frigate.models import Recordings from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp @@ -404,6 +406,55 @@ class TestHttpMedia(BaseTestHttp): assert "2024-03-10" in summary assert summary["2024-03-10"] is True + def test_recordings_returns_overlapping_segment(self): + """Recordings include a segment that overlaps the requested range.""" + after = 1000 + before = 1100 + + with AuthTestClient(self.app) as client: + super().insert_mock_recording("too_old", after - 10, after - 1) + super().insert_mock_recording( + "overlap", + after - MAX_SEGMENT_DURATION + 1, + after, + ) + super().insert_mock_recording("too_new", before + 1, before + 10) + + response = client.get( + "/front_door/recordings", + params={"after": after, "before": before}, + ) + + assert response.status_code == 200 + assert [recording["id"] for recording in response.json()] == ["overlap"] + + def test_vod_returns_overlapping_segment(self): + """VOD mapping includes a segment that overlaps the requested range.""" + start = 1000 + end = 1100 + + with ( + AuthTestClient(self.app) as client, + patch( + "frigate.api.media.get_keyframe_before", + return_value=None, + ), + ): + super().insert_mock_recording("too_old", start - 10, start - 1) + super().insert_mock_recording( + "overlap", + start - MAX_SEGMENT_DURATION + 2, + start + 1, + ) + super().insert_mock_recording("too_new", end + 1, end + 10) + + response = client.get(f"/vod/front_door/start/{start}/end/{end}") + + assert response.status_code == 200 + assert [ + clip["path"] for clip in response.json()["sequences"][0]["clips"] + ] == ["overlap"] + def test_recordings_unavailable_reports_gap_between_recordings(self): """A gap between two recordings is reported as an unavailable segment.""" with AuthTestClient(self.app) as client: