mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 20:18:58 +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
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Best-effort probe for whether this browser can decode a video codec
|
||||
* family.
|
||||
*
|
||||
* Frigate only stores the codec family from ffprobe (no profile or
|
||||
* level), so the probe tests representative MIME samples per family.
|
||||
* The result is a hint, not proof: a 10-bit stream can fail on a
|
||||
* browser that passes the Main-profile sample, so callers must keep a
|
||||
* reactive fallback for fatal codec errors. Unknown or NULL codecs
|
||||
* (legacy rows) always report supported - a wrong "unsupported" answer
|
||||
* silently degrades quality, which is worse than a failed attempt the
|
||||
* reactive path recovers from.
|
||||
*/
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ManagedMediaSource?: typeof MediaSource;
|
||||
}
|
||||
}
|
||||
|
||||
// any one supported sample marks the family playable
|
||||
const CODEC_MIME_SAMPLES: Record<string, string[]> = {
|
||||
h264: ['video/mp4; codecs="avc1.42E01E"', 'video/mp4; codecs="avc1.64001F"'],
|
||||
hevc: [
|
||||
'video/mp4; codecs="hvc1.1.6.L120.90"',
|
||||
'video/mp4; codecs="hev1.1.6.L120.90"',
|
||||
],
|
||||
av1: ['video/mp4; codecs="av01.0.05M.08"'],
|
||||
};
|
||||
|
||||
const FAMILY_ALIASES: Record<string, string> = {
|
||||
avc: "h264",
|
||||
avc1: "h264",
|
||||
h265: "hevc",
|
||||
hev1: "hevc",
|
||||
hvc1: "hevc",
|
||||
av01: "av1",
|
||||
};
|
||||
|
||||
function canPlayMimeType(mimeType: string): boolean {
|
||||
if (window.ManagedMediaSource?.isTypeSupported(mimeType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (window.MediaSource?.isTypeSupported(mimeType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// native playback fallback for browsers without MSE
|
||||
return document.createElement("video").canPlayType(mimeType) !== "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fails open: unknown codecs and NULL (legacy) codecs report supported,
|
||||
* since wrongly downgrading quality is worse than a recoverable failure.
|
||||
*/
|
||||
export function isCodecFamilySupported(
|
||||
codecName: string | null | undefined,
|
||||
): boolean {
|
||||
if (!codecName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalized = codecName.toLowerCase().trim();
|
||||
const family = FAMILY_ALIASES[normalized] ?? normalized;
|
||||
const samples = CODEC_MIME_SAMPLES[family];
|
||||
|
||||
if (!samples) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return samples.some(canPlayMimeType);
|
||||
}
|
||||
@@ -61,15 +61,18 @@ export function calculateSeekPosition(
|
||||
return false;
|
||||
}
|
||||
|
||||
// playlist duration exceeds wall length when the clip carries
|
||||
// keyframe back-snap lead-in
|
||||
const wallLength = segment.end_time - segment.start_time;
|
||||
const leadIn = Math.max(0, segment.duration - wallLength);
|
||||
|
||||
if (segment.end_time < timestamp) {
|
||||
// Add the full duration of this segment
|
||||
seekSeconds += segment.end_time - segment.start_time;
|
||||
seekSeconds += segment.duration;
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're in this segment - calculate position within it
|
||||
seekSeconds +=
|
||||
segment.end_time - segment.start_time - (segment.end_time - timestamp);
|
||||
seekSeconds += leadIn + (timestamp - segment.start_time);
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user