mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 10:38: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
@@ -28,7 +28,6 @@ export class DynamicVideoController {
|
||||
private timeRange: TimeRange = { after: 0, before: 0 };
|
||||
private inpointOffset: number = 0;
|
||||
private annotationOffset: number;
|
||||
private timeToStart: number | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
camera: string,
|
||||
@@ -55,11 +54,6 @@ export class DynamicVideoController {
|
||||
this.timeRange.after,
|
||||
this.recordings[0],
|
||||
);
|
||||
|
||||
if (this.timeToStart) {
|
||||
this.seekToTimestamp(this.timeToStart);
|
||||
this.timeToStart = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
@@ -75,8 +69,11 @@ export class DynamicVideoController {
|
||||
}
|
||||
|
||||
seekToTimestamp(time: number, play: boolean = false) {
|
||||
// a seek outside the current playback window is a no-op: the view
|
||||
// moves its anchor and chunk on such seeks, and the rebuilt source
|
||||
// resumes at the anchor (startPosition plus the post-load seek).
|
||||
// Seeking here would only reposition the outgoing source's media
|
||||
if (time < this.timeRange.after || time > this.timeRange.before) {
|
||||
this.timeToStart = time;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,16 +92,24 @@ export class DynamicVideoController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (seekSeconds != 0) {
|
||||
this.playerController.currentTime = seekSeconds;
|
||||
|
||||
if (this.playerController.currentTime === seekSeconds) {
|
||||
// seeking to the current position fires no seeked event, so apply
|
||||
// the play intent directly (this includes position 0, which the
|
||||
// player sits at before its first seek)
|
||||
if (play) {
|
||||
this.waitAndPlay();
|
||||
playWithTemporaryMuteFallback(this.playerController);
|
||||
} else {
|
||||
this.playerController.pause();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.playerController.currentTime = seekSeconds;
|
||||
|
||||
if (play) {
|
||||
this.waitAndPlay();
|
||||
} else {
|
||||
// no op
|
||||
this.playerController.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,20 +144,26 @@ export class DynamicVideoController {
|
||||
|
||||
getProgress(playerTime: number): number {
|
||||
// take a player time in seconds and convert to timestamp in timeline
|
||||
let timestamp = 0;
|
||||
const recordings = this.recordings || [];
|
||||
let totalTime = 0;
|
||||
(this.recordings || []).every((segment) => {
|
||||
for (const segment of recordings) {
|
||||
if (totalTime + segment.duration > playerTime) {
|
||||
// segment is here
|
||||
timestamp = segment.start_time + (playerTime - totalTime);
|
||||
return false;
|
||||
} else {
|
||||
totalTime += segment.duration;
|
||||
return true;
|
||||
// playlist media from before the span's wall start (keyframe
|
||||
// back-snap lead-in) clamps to the span start
|
||||
const wallLength = segment.end_time - segment.start_time;
|
||||
const leadIn = Math.max(0, segment.duration - wallLength);
|
||||
return (
|
||||
segment.start_time + Math.max(0, playerTime - totalTime - leadIn)
|
||||
);
|
||||
}
|
||||
});
|
||||
totalTime += segment.duration;
|
||||
}
|
||||
|
||||
return timestamp;
|
||||
// past the modeled total: clamp to the covered end rather than
|
||||
// reporting wall-clock zero
|
||||
return recordings.length > 0
|
||||
? recordings[recordings.length - 1].end_time
|
||||
: 0;
|
||||
}
|
||||
|
||||
scrubToTimestamp(time: number, saveIfNotReady: boolean = false) {
|
||||
@@ -162,7 +173,10 @@ export class DynamicVideoController {
|
||||
this.previewController.setNewPreviewStartTime(time);
|
||||
}
|
||||
|
||||
if (scrubResult && this.playerMode != "scrubbing") {
|
||||
// pause even when no preview can render this range: a hidden player
|
||||
// left running reports stale times once the drag releases, bouncing
|
||||
// the handlebar back and sometimes swallowing the release seek
|
||||
if (this.playerMode != "scrubbing") {
|
||||
this.playerMode = "scrubbing";
|
||||
this.playerController.pause();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user