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
@@ -615,16 +615,16 @@ export default function MotionSearchView({
}, [selectedRangeIdx, chunkedTimeRange]);
const updateSelectedSegment = useCallback(
(nextTime: number, updateStartTime: boolean) => {
(nextTime: number) => {
const index = chunkedTimeRange.findIndex(
(segment) => segment.after <= nextTime && segment.before >= nextTime,
);
if (index != -1) {
if (updateStartTime) {
setPlaybackStart(nextTime);
}
setPlaybackStart(nextTime);
// the outgoing chunk's player runs until the new source replaces
// it, reporting old positions while the new chunk loads
mainControllerRef.current?.pause();
setSelectedRangeIdx(index);
}
},
@@ -638,7 +638,10 @@ export default function MotionSearchView({
currentTime > currentTimeRange.before + 60 ||
currentTime < currentTimeRange.after - 60
) {
updateSelectedSegment(currentTime, false);
// the player rebuilds its source against playbackStart, and a
// stale anchor resolves to no startPosition, dropping playback
// at the start of the hour instead of the drag target
updateSelectedSegment(currentTime);
return;
}
@@ -678,9 +681,12 @@ export default function MotionSearchView({
currentTimeRange.after <= currentTime &&
currentTimeRange.before >= currentTime
) {
// a source reload mid-seek resumes from playbackStart, so the
// anchor has to follow explicit seeks
setPlaybackStart(currentTime);
mainControllerRef.current?.seekToTimestamp(currentTime, true);
} else {
updateSelectedSegment(currentTime, true);
updateSelectedSegment(currentTime);
}
} else if (playerTime != currentTime) {
mainControllerRef.current?.play();
@@ -700,9 +706,12 @@ export default function MotionSearchView({
setCurrentTime(time);
if (currentTimeRange.after <= time && currentTimeRange.before >= time) {
// a source reload mid-seek resumes from playbackStart, so the
// anchor has to follow explicit seeks
setPlaybackStart(time);
mainControllerRef.current?.seekToTimestamp(time, play);
} else {
updateSelectedSegment(time, true);
updateSelectedSegment(time);
}
},
[currentTimeRange, updateSelectedSegment],