mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-06 14:52:47 +03:00
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:
committed by
Nicolas Mowen
parent
f7c5500ea8
commit
1498231eb9
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user