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
@@ -20,6 +20,13 @@ const ffmpegArgsWidget = (
},
});
const recordSubArgsWidget = () =>
ffmpegArgsWidget("output_args.record_sub", {
allowInherit: true,
forceSplitLayout: true,
unsetLabelKey: "configForm.ffmpegArgs.sameAsRecord",
});
const ffmpeg: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/ffmpeg_presets",
@@ -75,6 +82,8 @@ const ffmpeg: SectionConfigOverrides = {
output_args: "/configuration/ffmpeg_presets#output-args-presets",
"inputs.output_args": "/configuration/ffmpeg_presets#output-args-presets",
"output_args.record": "/configuration/ffmpeg_presets#output-args-presets",
"output_args.record_sub":
"/configuration/ffmpeg_presets#output-args-presets",
"inputs.roles": "/configuration/cameras/#setting-up-camera-inputs",
apple_compatibility:
"/configuration/camera_specific#h265-cameras-via-safari",
@@ -112,9 +121,11 @@ const ffmpeg: SectionConfigOverrides = {
output_args: {
detect: arrayAsTextWidget,
record: ffmpegArgsWidget("output_args.record"),
record_sub: recordSubArgsWidget(),
items: {
detect: arrayAsTextWidget,
record: ffmpegArgsWidget("output_args.record"),
record_sub: recordSubArgsWidget(),
},
},
inputs: {
@@ -148,6 +159,7 @@ const ffmpeg: SectionConfigOverrides = {
items: {
detect: arrayAsTextWidget,
record: ffmpegArgsWidget("output_args.record"),
record_sub: recordSubArgsWidget(),
},
},
},
@@ -176,6 +188,7 @@ const ffmpeg: SectionConfigOverrides = {
output_args: {
detect: arrayAsTextWidget,
record: ffmpegArgsWidget("output_args.record"),
record_sub: recordSubArgsWidget(),
},
},
},
@@ -15,6 +15,19 @@ const record: SectionConfigOverrides = {
);
},
},
{
key: "no-record-sub-role",
messageKey: "configMessages.record.noRecordSubRole",
severity: "warning",
condition: (ctx) => {
if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
const sub = ctx.formData?.sub as Record<string, unknown> | undefined;
if (!sub?.enabled) return false;
return !ctx.fullCameraConfig.ffmpeg?.inputs?.some((i) =>
i.roles?.includes("record_sub"),
);
},
},
],
fieldDocs: {
"alerts.pre_capture":
@@ -34,6 +47,7 @@ const record: SectionConfigOverrides = {
"motion",
"alerts",
"detections",
"sub",
"preview",
"export",
],
@@ -67,6 +81,12 @@ const record: SectionConfigOverrides = {
"detections.retain.mode": {
"ui:options": { enumI18nPrefix: "retainMode" },
},
"sub.alerts.mode": {
"ui:options": { enumI18nPrefix: "retainMode" },
},
"sub.detections.mode": {
"ui:options": { enumI18nPrefix: "retainMode" },
},
preview: {
"ui:options": { defaultOpen: true, disableCollapsible: true },
quality: {