Add sub stream recording with adaptive quality playback (#24009)

* add sub stream recording with adaptive quality playback

Optionally record a second, lower bitrate stream alongside the main
recording stream via a `record_sub` input role and `record.sub` config block, with its own retention windows.
Recordings rows now carry the stream type plus the media details needed to serve both streams from one manifest: video codec, audio presence, audio codec and rate, and a record-time keyframe index.

Playback resolves coverage across both streams and merges them into a single VOD sequence, falling back to a discontinuity manifest with per-clip init segments when the media signatures differ. The player exposes a quality selector, and an auto governor picks the stream from stall time, bandwidth, codec support, and the save-data hint.

* fix tests and i18n
This commit is contained in:
Josh Hawkins
2026-09-12 07:30:04 -06:00
committed by Nicolas Mowen
parent f7c5500ea8
commit 1498231eb9
68 changed files with 6794 additions and 602 deletions
+31 -8
View File
@@ -255,6 +255,15 @@ def verify_config_roles(camera_config: CameraConfig) -> None:
f"Camera {camera_config.name} has record enabled, but record is not assigned to an input."
)
if (
camera_config.record.enabled
and camera_config.record.sub.enabled
and "record_sub" not in assigned_roles
):
raise ValueError(
f"Camera {camera_config.name} has sub stream recording enabled, but record_sub is not assigned to an input."
)
if camera_config.audio.enabled and "audio" not in assigned_roles:
raise ValueError(
f"Camera {camera_config.name} has audio events enabled, but audio is not assigned to an input."
@@ -275,13 +284,11 @@ def verify_valid_live_stream_names(
)
def verify_recording_segments_setup_with_reasonable_time(
camera_config: CameraConfig,
def verify_record_output_args_segment_time(
camera_config: CameraConfig, output_args: str | list[str], role: str
) -> None:
"""Verify that recording segments are setup and segment time is not greater than 60."""
record_args: list[str] = get_ffmpeg_arg_list(
camera_config.ffmpeg.output_args.record
)
"""Verify that a recording role's output args segment at a reasonable time."""
record_args: list[str] = get_ffmpeg_arg_list(output_args)
if record_args[0].startswith("preset"):
return
@@ -291,16 +298,32 @@ def verify_recording_segments_setup_with_reasonable_time(
except ValueError:
raise ValueError(
f"Camera {camera_config.name} has no segment_time in \
recording output args, segment args are required for record."
{role} output args, segment args are required for record."
) from None
if int(record_args[seg_arg_index + 1]) > 60:
raise ValueError(
f"Camera {camera_config.name} has invalid segment_time output arg, \
f"Camera {camera_config.name} has invalid segment_time in {role} output args, \
segment_time must be 60 or less."
)
def verify_recording_segments_setup_with_reasonable_time(
camera_config: CameraConfig,
) -> None:
"""Verify that recording segments are setup and segment time is not greater than 60."""
verify_record_output_args_segment_time(
camera_config, camera_config.ffmpeg.output_args.record, "recording"
)
if camera_config.record.sub.enabled:
verify_record_output_args_segment_time(
camera_config,
camera_config.ffmpeg.output_args.effective_record_sub,
"sub stream recording",
)
def verify_zone_objects_are_tracked(camera_config: CameraConfig) -> None:
"""Verify that user has not entered zone objects that are not in the tracking config."""
for zone_name, zone in camera_config.zones.items():