mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-26 08:24:42 +03:00
keep audio skip consistent with playback
Clips were skipped even when the range would be served video-only, and seeking into a skipped span landed on later footage. The skip now only applies when every row in the range carries audio, and seeks inside a zero-duration span land on the next clip.
This commit is contained in:
@@ -64,6 +64,7 @@ from frigate.util.recording_coverage import (
|
|||||||
plan_clip,
|
plan_clip,
|
||||||
recording_overlap,
|
recording_overlap,
|
||||||
resolve_coverage,
|
resolve_coverage,
|
||||||
|
sequence_needs_audio,
|
||||||
)
|
)
|
||||||
from frigate.util.time import get_normalized_tz_name
|
from frigate.util.time import get_normalized_tz_name
|
||||||
|
|
||||||
@@ -624,14 +625,14 @@ async def recording_clip(
|
|||||||
|
|
||||||
|
|
||||||
def _build_vod_clip(
|
def _build_vod_clip(
|
||||||
row: Any, start: float, end: float, shared: bool
|
row: Any, start: float, end: float, needs_audio: bool
|
||||||
) -> tuple[dict[str, Any], int] | None:
|
) -> tuple[dict[str, Any], int] | None:
|
||||||
"""Build one nginx-vod clip dict + duration (ms) for a recording row trimmed to [start, end).
|
"""Build one nginx-vod clip dict + duration (ms) for a recording row trimmed to [start, end).
|
||||||
|
|
||||||
Realization comes entirely from the shared plan_clip, so the coverage
|
Realization comes entirely from the shared plan_clip, so the coverage
|
||||||
endpoint's realized timelines match this manifest by construction.
|
endpoint's realized timelines match this manifest by construction.
|
||||||
"""
|
"""
|
||||||
plan = plan_clip(row, start, end, shared)
|
plan = plan_clip(row, start, end, needs_audio)
|
||||||
|
|
||||||
if plan.skipped:
|
if plan.skipped:
|
||||||
return None
|
return None
|
||||||
@@ -690,6 +691,7 @@ async def _vod_response(
|
|||||||
null_audio_glitches(resolve_coverage(camera_name, start_ts, end_ts)),
|
null_audio_glitches(resolve_coverage(camera_name, start_ts, end_ts)),
|
||||||
stream_preference,
|
stream_preference,
|
||||||
)
|
)
|
||||||
|
needs_audio = sequence_needs_audio(spans)
|
||||||
|
|
||||||
durations: list[int] = []
|
durations: list[int] = []
|
||||||
clips: list[dict[str, Any]] = []
|
clips: list[dict[str, Any]] = []
|
||||||
@@ -707,7 +709,7 @@ async def _vod_response(
|
|||||||
row.end_time,
|
row.end_time,
|
||||||
row.duration,
|
row.duration,
|
||||||
)
|
)
|
||||||
built = _build_vod_clip(row, span_start, span_end, len(spans) > 1)
|
built = _build_vod_clip(row, span_start, span_end, needs_audio)
|
||||||
|
|
||||||
if built is None:
|
if built is None:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -477,6 +477,31 @@ class TestVodAudioCutShort(CoverageDbTestCase):
|
|||||||
clips = self._clips(1005.0, 1020.0)
|
clips = self._clips(1005.0, 1020.0)
|
||||||
assert len(clips) == 2
|
assert len(clips) == 2
|
||||||
|
|
||||||
|
def test_video_only_range_keeps_clip(self):
|
||||||
|
# a video-only sub row serves the whole range without audio, so
|
||||||
|
# nginx-vod never compares audio tracks
|
||||||
|
self._insert(
|
||||||
|
"m1",
|
||||||
|
1000.0,
|
||||||
|
1010.0,
|
||||||
|
"main",
|
||||||
|
has_audio=True,
|
||||||
|
audio_codec="aac",
|
||||||
|
audio_rate=16000,
|
||||||
|
keyframes=self.KEYFRAMES,
|
||||||
|
audio_duration=2.112,
|
||||||
|
)
|
||||||
|
self._insert(
|
||||||
|
"s1", 1010.0, 1020.0, "sub", has_audio=False, keyframes=self.KEYFRAMES
|
||||||
|
)
|
||||||
|
clips = self._clips(1005.0, 1020.0)
|
||||||
|
assert [c["path"] for c in clips] == ["/tmp/m1.mp4", "/tmp/s1.mp4"]
|
||||||
|
assert all(c["tracks"] == "v" for c in clips)
|
||||||
|
timeline = realized_timeline(
|
||||||
|
resolve_coverage("front_door", 1005.0, 1020.0), None
|
||||||
|
)
|
||||||
|
assert timeline[0]["duration"] > 0
|
||||||
|
|
||||||
def test_sole_clip_is_kept(self):
|
def test_sole_clip_is_kept(self):
|
||||||
# alone in its sequence the clip plays video-only, so keep it
|
# alone in its sequence the clip plays video-only, so keep it
|
||||||
self._insert_pair(2.112)
|
self._insert_pair(2.112)
|
||||||
|
|||||||
@@ -341,7 +341,18 @@ class ClipPlan:
|
|||||||
first_key_frame_offset_ms: int = 0
|
first_key_frame_offset_ms: int = 0
|
||||||
|
|
||||||
|
|
||||||
def plan_clip(row: Any, start: float, end: float, shared: bool = False) -> ClipPlan:
|
def sequence_needs_audio(spans: list[list[Any]]) -> bool:
|
||||||
|
"""Whether nginx-vod will require every clip in these spans to carry audio.
|
||||||
|
|
||||||
|
A range mixing audio-bearing and audio-less rows is served video-only,
|
||||||
|
and a single clip has nothing to mismatch against.
|
||||||
|
"""
|
||||||
|
return len(spans) > 1 and all(row.has_audio is not False for row, *_ in spans)
|
||||||
|
|
||||||
|
|
||||||
|
def plan_clip(
|
||||||
|
row: Any, start: float, end: float, needs_audio: bool = False
|
||||||
|
) -> ClipPlan:
|
||||||
"""Plan one nginx-vod clip for a recording row trimmed to [start, end).
|
"""Plan one nginx-vod clip for a recording row trimmed to [start, end).
|
||||||
|
|
||||||
The single source of truth for clip realization: the vod mapping
|
The single source of truth for clip realization: the vod mapping
|
||||||
@@ -349,7 +360,7 @@ def plan_clip(row: Any, start: float, end: float, shared: bool = False) -> ClipP
|
|||||||
to the frontend, so the playhead model matches the playlist by
|
to the frontend, so the playhead model matches the playlist by
|
||||||
construction rather than accumulating drift at each hand-off.
|
construction rather than accumulating drift at each hand-off.
|
||||||
|
|
||||||
shared marks a clip that sits in a sequence with other clips.
|
needs_audio comes from sequence_needs_audio for the clip's range.
|
||||||
"""
|
"""
|
||||||
min_duration_ms = 100 # Minimum 100ms to ensure at least one video frame
|
min_duration_ms = 100 # Minimum 100ms to ensure at least one video frame
|
||||||
max_duration_ms = MAX_SEGMENT_DURATION * 1000
|
max_duration_ms = MAX_SEGMENT_DURATION * 1000
|
||||||
@@ -404,7 +415,7 @@ def plan_clip(row: Any, start: float, end: float, shared: bool = False) -> ClipP
|
|||||||
# nginx-vod then counts no audio track in this clip and fails the whole
|
# nginx-vod then counts no audio track in this clip and fails the whole
|
||||||
# sequence on the track count mismatch
|
# sequence on the track count mismatch
|
||||||
if (
|
if (
|
||||||
shared
|
needs_audio
|
||||||
and row.has_audio
|
and row.has_audio
|
||||||
and row.audio_duration is not None
|
and row.audio_duration is not None
|
||||||
and (clip_from or 0) >= int(row.audio_duration * 1000)
|
and (clip_from or 0) >= int(row.audio_duration * 1000)
|
||||||
@@ -465,7 +476,7 @@ def realized_timeline(
|
|||||||
clip is skipped reports duration 0.
|
clip is skipped reports duration 0.
|
||||||
"""
|
"""
|
||||||
spans = build_spans(intervals, stream)
|
spans = build_spans(intervals, stream)
|
||||||
shared = len(spans) > 1
|
needs_audio = sequence_needs_audio(spans)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -474,7 +485,7 @@ def realized_timeline(
|
|||||||
"duration": plan.duration_ms,
|
"duration": plan.duration_ms,
|
||||||
}
|
}
|
||||||
for row, span_start, span_end, _ in spans
|
for row, span_start, span_end, _ in spans
|
||||||
for plan in (plan_clip(row, span_start, span_end, shared),)
|
for plan in (plan_clip(row, span_start, span_end, needs_audio),)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ export function calculateSeekPosition(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skipped clips have no playlist media, so a seek inside one lands on
|
||||||
|
// the next clip that plays
|
||||||
|
if (segment.duration === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// playlist duration exceeds wall length when the clip carries
|
// playlist duration exceeds wall length when the clip carries
|
||||||
// keyframe back-snap lead-in
|
// keyframe back-snap lead-in
|
||||||
const wallLength = segment.end_time - segment.start_time;
|
const wallLength = segment.end_time - segment.start_time;
|
||||||
|
|||||||
Reference in New Issue
Block a user