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-08-22 11:40:42 -05:00
parent 8de6216c61
commit d67304a84d
68 changed files with 6813 additions and 602 deletions
+26 -1
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { isIOS } from "react-device-detect";
import { useTimelineUtils } from "./use-timeline-utils";
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
@@ -10,6 +11,11 @@ import useUserInteraction from "./use-user-interaction";
const DRAG_STATE_COMMIT_MS = 100;
// iOS Safari synthesizes a click shortly after a drag's touchend even
// though the touchend handler calls preventDefault; clicks observed in
// traces arrive ~50ms after release
const GHOST_CLICK_WINDOW_MS = 400;
type DraggableElementProps = {
contentRef: React.RefObject<HTMLElement | null>;
timelineRef: React.RefObject<HTMLDivElement | null>;
@@ -164,9 +170,28 @@ function useDraggableElement({
setDraggableElementTime(pendingDragTimeRef.current);
pendingDragTimeRef.current = null;
}
// iOS Safari synthesizes a click after touchend despite the
// preventDefault, hit-tested at the drag origin where a segment
// now sits; its onClick would yank the handlebar back
if (isIOS && "TouchEvent" in window && e instanceof TouchEvent) {
const swallow = (clickEvent: MouseEvent) => {
cleanup();
if (timelineRef.current?.contains(clickEvent.target as Node)) {
clickEvent.preventDefault();
clickEvent.stopPropagation();
}
};
const cleanup = () => {
document.removeEventListener("click", swallow, true);
window.clearTimeout(timer);
};
const timer = window.setTimeout(cleanup, GHOST_CLICK_WINDOW_MS);
document.addEventListener("click", swallow, true);
}
}
},
[isDragging, setIsDragging, setDraggableElementTime],
[isDragging, setIsDragging, setDraggableElementTime, timelineRef],
);
const timestampToPixels = useCallback(