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
+28 -1
View File
@@ -3,7 +3,12 @@ from enum import Enum
from pydantic import Field, PrivateAttr, model_validator
from frigate.const import CACHE_DIR, CACHE_SEGMENT_FORMAT, REGEX_CAMERA_NAME
from frigate.const import (
CACHE_DIR,
CACHE_SEGMENT_FORMAT,
REGEX_CAMERA_NAME,
SUB_CACHE_TAG,
)
from frigate.ffmpeg_presets import (
parse_preset_hardware_acceleration_decode,
parse_preset_hardware_acceleration_scale,
@@ -294,6 +299,28 @@ class CameraConfig(FrigateBaseModel):
+ ffmpeg_output_args
)
if (
"record_sub" in ffmpeg_input.roles
and self.record.enabled
and self.record.sub.enabled
):
sub_output_args = self.ffmpeg.output_args.effective_record_sub
record_args = get_ffmpeg_arg_list(
parse_preset_output_record(
sub_output_args,
self.ffmpeg.apple_compatibility,
)
or sub_output_args
)
ffmpeg_output_args = (
record_args
+ [
f"{os.path.join(CACHE_DIR, self.name)}{SUB_CACHE_TAG}@{CACHE_SEGMENT_FORMAT}.mp4"
]
+ ffmpeg_output_args
)
# if there aren't any outputs enabled for this input
if len(ffmpeg_output_args) == 0:
return None
+15
View File
@@ -42,6 +42,20 @@ class FfmpegOutputArgsConfig(FrigateBaseModel):
title="Record output arguments",
description="Default output arguments for record role streams.",
)
record_sub: str | list[str] = Field(
default_factory=list,
title="Sub stream record output arguments",
description="Output arguments for record_sub role streams. The record output arguments are used when this is not set.",
)
@property
def effective_record_sub(self) -> str | list[str]:
"""Output arguments used for the record_sub role.
Falls back to the record arguments rather than to the stock preset so
that a customized record value keeps applying to both recorded streams.
"""
return self.record_sub or self.record
class FfmpegConfig(FrigateBaseModel):
@@ -99,6 +113,7 @@ class FfmpegConfig(FrigateBaseModel):
class CameraRoleEnum(str, Enum):
audio = "audio"
record = "record"
record_sub = "record_sub"
detect = "detect"
+52
View File
@@ -13,6 +13,7 @@ __all__ = [
"RecordExportConfig",
"RecordPreviewConfig",
"RecordQualityEnum",
"RecordSubConfig",
"EventsConfig",
"ReviewRetainConfig",
"RecordRetainConfig",
@@ -110,6 +111,34 @@ class RecordExportConfig(FrigateBaseModel):
)
class RecordSubConfig(FrigateBaseModel):
enabled: bool = Field(
default=False,
title="Enable sub stream recording",
description="Enable recording of a second, lower quality stream for adaptive quality playback and extended retention.",
)
continuous: RecordRetainConfig = Field(
default_factory=RecordRetainConfig,
title="Sub stream continuous retention",
description="Number of days to retain sub stream recordings regardless of tracked objects or motion.",
)
motion: RecordRetainConfig = Field(
default_factory=RecordRetainConfig,
title="Sub stream motion retention",
description="Number of days to retain sub stream recordings triggered by motion.",
)
alerts: ReviewRetainConfig = Field(
default_factory=ReviewRetainConfig,
title="Sub stream alert retention",
description="Retention settings for sub stream recordings of alerts.",
)
detections: ReviewRetainConfig = Field(
default_factory=ReviewRetainConfig,
title="Sub stream detection retention",
description="Retention settings for sub stream recordings of detections.",
)
class RecordConfig(FrigateBaseModel):
enabled: bool = Field(
default=False,
@@ -151,12 +180,35 @@ class RecordConfig(FrigateBaseModel):
title="Preview config",
description="Settings controlling the quality of recording previews shown in the UI.",
)
sub: RecordSubConfig = Field(
default_factory=RecordSubConfig,
title="Sub stream recording",
description="Settings for recording a second, lower quality stream.",
)
enabled_in_config: bool | None = Field(
default=None,
title="Original recording state",
description="Indicates whether recording was enabled in the original static configuration.",
)
@property
def effective_alert_days(self) -> float:
"""Alert retention extended to the sub stream window when sub is enabled.
Review items and tracked objects must stay visible for as long as
either stream still has recordings.
"""
if self.sub.enabled:
return max(self.alerts.retain.days, self.sub.alerts.days)
return self.alerts.retain.days
@property
def effective_detection_days(self) -> float:
"""Detection retention extended to the sub window when sub is enabled."""
if self.sub.enabled:
return max(self.detections.retain.days, self.sub.detections.days)
return self.detections.retain.days
@property
def event_pre_capture(self) -> int:
return max(
+6 -1
View File
@@ -129,8 +129,13 @@ class CameraConfigUpdateSubscriber:
config.objects = updated_config
elif update_type == CameraConfigUpdateEnum.record:
old_enabled_in_config = config.record.enabled_in_config
old_sub_enabled = config.record.sub.enabled
config.record = updated_config
if old_enabled_in_config != updated_config.enabled_in_config:
# the record and record_sub ffmpeg outputs are gated on these
if (
old_enabled_in_config != updated_config.enabled_in_config
or old_sub_enabled != updated_config.sub.enabled
):
config.recreate_ffmpeg_cmds()
elif update_type == CameraConfigUpdateEnum.review:
config.review = updated_config