From 71445231f6db2c5dd2a140ea1055b27c19408a3e Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 9 Oct 2026 06:37:21 -0500 Subject: [PATCH] don't treat unknown audio as an audio-bearing stream A recording row with NULL has_audio (ffprobe failed and the cv2 fallback can't report audio) marked the whole stream as audio-bearing, so every confirmed video-only row on it was dropped as a glitch. A stream now counts as audio-bearing only when a row is known to carry audio. --- frigate/test/test_recording_coverage.py | 17 +++++++++++++++++ frigate/util/recording_coverage.py | 7 ++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/frigate/test/test_recording_coverage.py b/frigate/test/test_recording_coverage.py index 00ce79865b..0695a0250d 100644 --- a/frigate/test/test_recording_coverage.py +++ b/frigate/test/test_recording_coverage.py @@ -13,6 +13,7 @@ from frigate.models import Recordings from frigate.util.recording_coverage import ( _rows_query, coverage_spans, + null_audio_glitches, plan_clip, realized_timeline, resolve_coverage, @@ -166,6 +167,22 @@ class TestRecordingCoverage(CoverageDbTestCase): expected = int(5 * 1024 * 1024 * 8 / 10) assert summary["main"]["bitrate"] == expected + def test_unknown_audio_row_keeps_video_only_stream(self): + self._insert("s1", 1000.0, 1010.0, "sub", has_audio=False) + self._insert("s2", 1010.0, 1020.0, "sub", has_audio=None) + self._insert("s3", 1020.0, 1030.0, "sub", has_audio=False) + kept = null_audio_glitches(resolve_coverage("front_door", 1000.0, 1030.0)) + self.assertEqual( + [i.sub.path for i in kept], [f"/tmp/s{n}.mp4" for n in (1, 2, 3)] + ) + + def test_video_only_glitch_dropped_on_audio_stream(self): + self._insert("s1", 1000.0, 1010.0, "sub", has_audio=True) + self._insert("s2", 1010.0, 1020.0, "sub", has_audio=False) + self._insert("s3", 1020.0, 1030.0, "sub", has_audio=None) + kept = null_audio_glitches(resolve_coverage("front_door", 1000.0, 1030.0)) + self.assertEqual([i.sub.path for i in kept], ["/tmp/s1.mp4", "/tmp/s3.mp4"]) + def test_other_camera_rows_excluded(self): self._insert("m1", 1000.0, 1010.0, "main") self._insert("o1", 1000.0, 1010.0, "main", camera="back_yard") diff --git a/frigate/util/recording_coverage.py b/frigate/util/recording_coverage.py index 96a3ec5292..55ca2449a9 100644 --- a/frigate/util/recording_coverage.py +++ b/frigate/util/recording_coverage.py @@ -204,11 +204,12 @@ def coverage_spans(intervals: list[CoverageInterval]) -> list[dict[str, Any]]: def stream_has_audio(intervals: list[CoverageInterval], main: bool) -> bool: """Whether a stream is audio-bearing over a coverage window. - A stream counts as audio-bearing unless EVERY one of its rows reports - has_audio False; NULL (legacy or undetermined) counts as audio. + A stream counts as audio-bearing only when one of its rows is known + to carry audio. A NULL row (legacy, or a segment ffprobe could not + read) proves nothing either way. """ return any( - row is not None and row.has_audio is not False + row is not None and row.has_audio is True for row in ((interval.main if main else interval.sub) for interval in intervals) )