frigate/web/src/components/player/dynamic/DynamicVideoPlayer.tsx

440 lines
12 KiB
TypeScript
Raw Normal View History

2026-03-17 20:46:42 +03:00
import {
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useApiHost } from "@/api";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import {
Recording,
RecordingPlaybackPreference,
} from "@/types/record";
import { Preview } from "@/types/preview";
import PreviewPlayer, { PreviewController } from "../PreviewPlayer";
import { DynamicVideoController } from "./DynamicVideoController";
import HlsVideoPlayer, { HlsSource } from "../HlsVideoPlayer";
import { useDetailStream } from "@/context/detail-stream-context";
import { TimeRange } from "@/types/timeline";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { VideoResolutionType } from "@/types/live";
import axios from "axios";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { useUserPersistence } from "@/hooks/use-user-persistence";
import usePlaybackCapabilities from "@/hooks/use-playback-capabilities";
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
import {
chooseRecordingPlayback,
getRecordingsForPlaybackVariant,
} from "@/utils/recordingPlayback";
2026-03-17 20:46:42 +03:00
import {
calculateInpointOffset,
calculateSeekPosition,
} from "@/utils/videoUtil";
import { isFirefox } from "react-device-detect";
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
import RecordingPlaybackPreferenceSelect from "../RecordingPlaybackPreferenceSelect";
2026-03-17 20:46:42 +03:00
/**
* Dynamically switches between video playback and scrubbing preview player.
*/
type DynamicVideoPlayerProps = {
className?: string;
camera: string;
timeRange: TimeRange;
cameraPreviews: Preview[];
startTimestamp?: number;
isScrubbing: boolean;
hotKeys: boolean;
supportsFullscreen: boolean;
fullscreen: boolean;
onControllerReady: (controller: DynamicVideoController) => void;
onTimestampUpdate?: (timestamp: number) => void;
onClipEnded?: () => void;
onSeekToTime?: (timestamp: number, play?: boolean) => void;
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
toggleFullscreen: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
transformedOverlay?: ReactNode;
};
export default function DynamicVideoPlayer({
className,
camera,
timeRange,
cameraPreviews,
startTimestamp,
isScrubbing,
hotKeys,
supportsFullscreen,
fullscreen,
onControllerReady,
onTimestampUpdate,
onClipEnded,
onSeekToTime,
setFullResolution,
toggleFullscreen,
containerRef,
transformedOverlay,
}: DynamicVideoPlayerProps) {
const { t } = useTranslation(["components/player"]);
const apiHost = useApiHost();
const { data: config } = useSWR<FrigateConfig>("config");
// for detail stream context in History
const {
isDetailMode,
camera: contextCamera,
currentTime,
} = useDetailStream();
// controlling playback
const playerRef = useRef<HTMLVideoElement | null>(null);
const [previewController, setPreviewController] =
useState<PreviewController | null>(null);
const [noRecording, setNoRecording] = useState(false);
const controller = useMemo(() => {
if (!config || !playerRef.current || !previewController) {
return undefined;
}
return new DynamicVideoController(
camera,
playerRef.current,
previewController,
(config.cameras[camera]?.detect?.annotation_offset || 0) / 1000,
isScrubbing ? "scrubbing" : "playback",
setNoRecording,
() => {},
);
// we only want to fire once when players are ready
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [camera, config, playerRef.current, previewController]);
useEffect(() => {
if (!controller) {
return;
}
if (controller) {
onControllerReady(controller);
}
// we only want to fire once when players are ready
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [controller]);
// initial state
const [isLoading, setIsLoading] = useState(false);
const [isBuffering, setIsBuffering] = useState(false);
const [loadingTimeout, setLoadingTimeout] = useState<NodeJS.Timeout>();
const [playbackPreference, setPlaybackPreference] =
useUserPersistence<RecordingPlaybackPreference>(
`${camera}-recording-playback-v2`,
"sub",
);
// Don't set source until recordings load - we need accurate startPosition
// to avoid hls.js clamping to video end when startPosition exceeds duration
const [source, setSource] = useState<HlsSource | undefined>(undefined);
// start at correct time
useEffect(() => {
if (!isScrubbing) {
setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000));
}
return () => {
if (loadingTimeout) {
clearTimeout(loadingTimeout);
}
};
// we only want trigger when scrubbing state changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [camera, isScrubbing]);
const onPlayerLoaded = useCallback(() => {
if (!controller || !startTimestamp) {
return;
}
controller.seekToTimestamp(startTimestamp, true);
}, [startTimestamp, controller]);
const onTimeUpdate = useCallback(
(time: number) => {
if (isScrubbing || !controller || !onTimestampUpdate || time == 0) {
return;
}
if (isLoading) {
setIsLoading(false);
}
if (isBuffering) {
setIsBuffering(false);
}
onTimestampUpdate(controller.getProgress(time));
},
[controller, onTimestampUpdate, isBuffering, isLoading, isScrubbing],
);
const onUploadFrameToPlus = useCallback(
(playTime: number) => {
if (!controller) {
return;
}
const time = controller.getProgress(playTime);
return axios.post(`/${camera}/plus/${time}`);
},
[camera, controller],
);
// state of playback player
const recordingParams = useMemo(
() => ({
before: timeRange.before,
after: timeRange.after,
}),
[timeRange],
);
const { data: allRecordings } = useSWR<Recording[]>(
[`${camera}/recordings`, { ...recordingParams, variant: "all" }],
{ revalidateOnFocus: false },
);
const codecNames = useMemo(
() =>
Array.from(
new Set((allRecordings ?? []).map((recording) => recording.codec_name)),
),
[allRecordings],
);
const playbackCapabilities = usePlaybackCapabilities(codecNames);
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
const playbackDecision = useMemo(() => {
if (!allRecordings?.length) {
return undefined;
}
const vodPath = `/vod/${camera}/start/${recordingParams.after}/end/${recordingParams.before}/master.m3u8`;
return chooseRecordingPlayback({
apiHost,
recordings: allRecordings,
preference: playbackPreference ?? "sub",
vodPath,
capabilities: playbackCapabilities,
});
}, [
allRecordings,
apiHost,
camera,
playbackPreference,
playbackCapabilities,
recordingParams.after,
recordingParams.before,
]);
const recordings = useMemo(() => {
if (!allRecordings?.length) {
return allRecordings;
}
if (!playbackDecision || playbackDecision.variant === "main") {
return getRecordingsForPlaybackVariant(allRecordings, "main");
}
const selectedRecordings = getRecordingsForPlaybackVariant(allRecordings, "sub");
return selectedRecordings.length > 0 ? selectedRecordings : allRecordings;
}, [allRecordings, playbackDecision]);
2026-03-17 20:46:42 +03:00
useEffect(() => {
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
if (!allRecordings?.length) {
if (allRecordings?.length == 0) {
if (loadingTimeout) {
clearTimeout(loadingTimeout);
}
setIsLoading(false);
setIsBuffering(false);
2026-03-17 20:46:42 +03:00
setNoRecording(true);
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
setSource(undefined);
2026-03-17 20:46:42 +03:00
}
return;
}
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
if (!recordings?.length || !playbackDecision) {
return;
}
2026-03-17 20:46:42 +03:00
let startPosition = undefined;
if (startTimestamp) {
const inpointOffset = calculateInpointOffset(
recordingParams.after,
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
recordings[0],
2026-03-17 20:46:42 +03:00
);
startPosition = calculateSeekPosition(
startTimestamp,
recordings,
inpointOffset,
);
}
setSource({
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
playlist: playbackDecision.url,
2026-03-17 20:46:42 +03:00
startPosition,
});
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
setNoRecording(false);
2026-03-17 20:46:42 +03:00
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
allRecordings,
recordings,
startTimestamp,
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
playbackDecision,
recordingParams.after,
2026-03-17 20:46:42 +03:00
]);
useEffect(() => {
if (!controller || !recordings?.length) {
return;
}
if (playerRef.current) {
playerRef.current.autoplay = !isScrubbing;
}
setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000));
controller.newPlayback({
recordings: recordings ?? [],
timeRange,
});
// we only want this to change when controller or recordings update
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [controller, recordings]);
const inpointOffset = useMemo(
() => calculateInpointOffset(recordingParams.after, (recordings || [])[0]),
[recordingParams, recordings],
);
const onValidateClipEnd = useCallback(
(currentTime: number) => {
if (!onClipEnded || !controller || !recordings) {
return;
}
if (!isFirefox) {
onClipEnded();
}
// Firefox has a bug where clipEnded can be called prematurely due to buffering
// we need to validate if the current play-point is truly at the end of available recordings
const lastRecordingTime = recordings.at(-1)?.start_time;
if (
!lastRecordingTime ||
controller.getProgress(currentTime) < lastRecordingTime
) {
return;
}
onClipEnded();
},
[onClipEnded, controller, recordings],
);
return (
<>
{source && (
<HlsVideoPlayer
videoRef={playerRef}
containerRef={containerRef}
visible={!(isScrubbing || isLoading)}
currentSource={source}
hotKeys={hotKeys}
supportsFullscreen={supportsFullscreen}
fullscreen={fullscreen}
inpointOffset={inpointOffset}
onTimeUpdate={onTimeUpdate}
onPlayerLoaded={onPlayerLoaded}
onClipEnded={onValidateClipEnd}
onSeekToTime={(timestamp, play) => {
if (onSeekToTime) {
onSeekToTime(timestamp, play);
}
}}
onPlaying={() => {
if (isScrubbing) {
playerRef.current?.pause();
}
if (loadingTimeout) {
clearTimeout(loadingTimeout);
}
setNoRecording(false);
}}
setFullResolution={setFullResolution}
onUploadFrame={onUploadFrameToPlus}
toggleFullscreen={toggleFullscreen}
onError={(error) => {
if (error == "stalled" && !isScrubbing) {
setIsBuffering(true);
}
}}
isDetailMode={isDetailMode}
camera={contextCamera || camera}
currentTimeOverride={currentTime}
transformedOverlay={transformedOverlay}
/>
)}
{!isScrubbing && source && (
<div className="absolute right-3 top-3 z-50">
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
<RecordingPlaybackPreferenceSelect
className="h-8 w-32 bg-background/90 text-xs backdrop-blur"
2026-03-17 20:46:42 +03:00
value={playbackPreference ?? "sub"}
onValueChange={(value) =>
setPlaybackPreference(value as RecordingPlaybackPreference)
}
Added substream support, dynamic substream creation, and playback methods for This change adds first-class adaptive recording playback using main and sub recording variants. Frigate can now store multiple recording variants per camera, expose those variants through the recordings API, and serve variant-specific VOD playlists through routes such as /vod/variant/sub/.... The UI now uses the available recording variants and browser playback capability to choose an appropriate playback source, with a user-selectable Auto, Main, and Sub preference. This is applied across timeline playback, export preview, and object detail playback. The backend also includes a fallback path for sub playback: when a native sub recording is not available for a requested time range, Frigate can generate a lower-resolution sub recording from the main segment, store it under the standard sub variant, and mark it with transcoded_from_main. Additional changes include recording metadata for codec, resolution, bitrate, and variant; database migrations for recording variants and generated-sub tracking; tests for variant VOD selection and fallback behavior; improved storage graph sorting; and a small MQTT TLS guard so tls_insecure is only applied when TLS is configured. Substream Configuration Examples Record the main stream as the normal full-resolution recording and also record the camera substream as the sub variant: cameras: front_door: ffmpeg: inputs: - path: rtsp://user:password@192.168.1.10:554/main roles: - record record_variant: main - path: rtsp://user:password@192.168.1.10:554/sub roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true Using go2rtc restreams: go2rtc: streams: front_door: - rtsp://user:password@192.168.1.10:554/main front_door_sub: - rtsp://user:password@192.168.1.10:554/sub cameras: front_door: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_door input_args: preset-rtsp-restream roles: - record record_variant: main - path: rtsp://127.0.0.1:8554/front_door_sub input_args: preset-rtsp-restream roles: - detect - record record_variant: sub detect: width: 640 height: 360 fps: 5 record: enabled: true If record_variant is omitted on a record input, it defaults to main. Each camera can only use a given recording variant once, so the main and sub recording inputs should use distinct variant names.
2026-04-30 05:05:59 +03:00
/>
2026-03-17 20:46:42 +03:00
</div>
)}
<PreviewPlayer
className={cn(
className,
isScrubbing || isLoading ? "visible" : "hidden",
)}
camera={camera}
timeRange={timeRange}
cameraPreviews={cameraPreviews}
startTime={startTimestamp}
isScrubbing={isScrubbing}
onControllerReady={(previewController) =>
setPreviewController(previewController)
}
/>
{!isScrubbing && (isLoading || isBuffering) && !noRecording && (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
)}
{!isScrubbing && !isLoading && noRecording && (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
{t("noRecordingsFoundForThisTime")}
</div>
)}
</>
);
}