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
+8 -8
View File
@@ -197,9 +197,11 @@ class EventCleanup(threading.Thread):
def expire_clips(self) -> list[str]:
## Expire events from unlisted cameras based on the global config
# effective days cover the sub window, keeping tracked objects in
# Explore while sub recordings and review items still exist
expire_days = max(
self.config.record.alerts.retain.days,
self.config.record.detections.retain.days,
self.config.record.effective_alert_days,
self.config.record.effective_detection_days,
)
file_extension = None # mp4 clips are no longer stored in /clips
update_params = {"has_clip": False}
@@ -278,15 +280,13 @@ class EventCleanup(threading.Thread):
## Expire events from cameras based on the camera config
for name, camera in self.config.cameras.items():
expire_days = max(
camera.record.alerts.retain.days,
camera.record.detections.retain.days,
)
# effective days cover the sub window, keeping tracked objects
# in Explore while sub recordings and review items still exist
alert_expire_date = (
now - datetime.timedelta(days=camera.record.alerts.retain.days)
now - datetime.timedelta(days=camera.record.effective_alert_days)
).timestamp()
detection_expire_date = (
now - datetime.timedelta(days=camera.record.detections.retain.days)
now - datetime.timedelta(days=camera.record.effective_detection_days)
).timestamp()
# grab all events after specific time
expired_events = (