From d08027f2216460e4faa03b4759c2744123e2e42b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 20 May 2026 16:30:24 -0500 Subject: [PATCH] use vod for constructing replay clips --- frigate/api/debug_replay.py | 5 +++ frigate/jobs/debug_replay.py | 39 +++++++++++++---------- frigate/test/test_debug_replay_job.py | 45 +++++++++++++++++++++------ 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/frigate/api/debug_replay.py b/frigate/api/debug_replay.py index 2ba5d2b85f..034da3845d 100644 --- a/frigate/api/debug_replay.py +++ b/frigate/api/debug_replay.py @@ -86,10 +86,15 @@ class DebugReplayStopResponse(BaseModel): async def start_debug_replay(request: Request, body: DebugReplayStartBody): """Start a debug replay session asynchronously.""" replay_manager = request.app.replay_manager + internal_port = request.app.frigate_config.networking.listen.internal + if type(internal_port) is str: + internal_port = int(internal_port.split(":")[-1]) + source = RecordingDebugReplaySource( source_camera=body.camera, start_ts=body.start_time, end_ts=body.end_time, + internal_port=internal_port, ) try: diff --git a/frigate/jobs/debug_replay.py b/frigate/jobs/debug_replay.py index 3dd7a02bf6..3d8b2d6b63 100644 --- a/frigate/jobs/debug_replay.py +++ b/frigate/jobs/debug_replay.py @@ -1,4 +1,4 @@ -"""Debug replay startup job: ffmpeg concat + camera config publish. +"""Debug replay startup job: ffmpeg remux + camera config publish. The runner orchestrates the async portion of starting a debug replay session. The DebugReplayManager (in frigate.debug_replay) owns session @@ -153,15 +153,22 @@ class DebugReplaySource(ABC): class RecordingDebugReplaySource(DebugReplaySource): """Replay source backed by the Recordings table. - Builds a concat playlist of recording files covering the time range - and feeds it to ffmpeg's concat demuxer. + Feeds ffmpeg the internal VOD endpoint so segments with mismatched + SPS/PPS (e.g. across day/night transitions) stitch cleanly via HLS + discontinuities. """ - def __init__(self, source_camera: str, start_ts: float, end_ts: float) -> None: + def __init__( + self, + source_camera: str, + start_ts: float, + end_ts: float, + internal_port: int, + ) -> None: self._camera = source_camera self._start_ts = start_ts self._end_ts = end_ts - self._concat_file: Optional[str] = None + self._internal_port = internal_port @property def source_camera(self) -> str: @@ -185,18 +192,16 @@ class RecordingDebugReplaySource(DebugReplaySource): ) def ffmpeg_input_args(self, working_dir: str) -> list[str]: - replay_name = f"{REPLAY_CAMERA_PREFIX}{self._camera}" - concat_file = os.path.join(working_dir, f"{replay_name}_concat.txt") - recordings = query_recordings(self._camera, self._start_ts, self._end_ts) - with open(concat_file, "w") as f: - for recording in recordings: - f.write(f"file '{recording.path}'\n") - self._concat_file = concat_file - return ["-f", "concat", "-safe", "0", "-i", concat_file] - - def cleanup(self, working_dir: str) -> None: - if self._concat_file: - _remove_silent(self._concat_file) + playlist_url = ( + f"http://127.0.0.1:{self._internal_port}/vod/{self._camera}" + f"/start/{self._start_ts}/end/{self._end_ts}/index.m3u8" + ) + return [ + "-protocol_whitelist", + "pipe,file,http,tcp", + "-i", + playlist_url, + ] class ExportDebugReplaySource(DebugReplaySource): diff --git a/frigate/test/test_debug_replay_job.py b/frigate/test/test_debug_replay_job.py index 5e2da16720..12c4be82b8 100644 --- a/frigate/test/test_debug_replay_job.py +++ b/frigate/test/test_debug_replay_job.py @@ -101,7 +101,10 @@ class TestStartDebugReplayJob(unittest.TestCase): with self.assertRaises(ValueError): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="missing", start_ts=100.0, end_ts=200.0 + source_camera="missing", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -112,7 +115,10 @@ class TestStartDebugReplayJob(unittest.TestCase): with self.assertRaises(ValueError): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=200.0, end_ts=100.0 + source_camera="front", + start_ts=200.0, + end_ts=100.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -126,7 +132,10 @@ class TestStartDebugReplayJob(unittest.TestCase): with self.assertRaises(ValueError): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -156,7 +165,10 @@ class TestStartDebugReplayJob(unittest.TestCase): ): job_id = start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -193,7 +205,10 @@ class TestStartDebugReplayJob(unittest.TestCase): ): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -203,7 +218,10 @@ class TestStartDebugReplayJob(unittest.TestCase): with self.assertRaises(RuntimeError): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -271,7 +289,10 @@ class TestRunnerHappyPath(unittest.TestCase): ): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -342,7 +363,10 @@ class TestRunnerFailurePath(unittest.TestCase): ): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -420,7 +444,10 @@ class TestRunnerCancellation(unittest.TestCase): ): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher,