mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-27 12: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
@@ -10,7 +10,12 @@ import {
|
||||
DebugReplayContent,
|
||||
SaveDebugReplayOverlay,
|
||||
} from "./DebugReplayDialog";
|
||||
import { ExportMode, GeneralFilter } from "@/types/filter";
|
||||
import {
|
||||
DEFAULT_DRAWER_FEATURES,
|
||||
DrawerFeatures,
|
||||
ExportMode,
|
||||
GeneralFilter,
|
||||
} from "@/types/filter";
|
||||
import ReviewActivityCalendar from "./ReviewActivityCalendar";
|
||||
import { SelectSeparator } from "../ui/select";
|
||||
import {
|
||||
@@ -31,6 +36,14 @@ import { StartExportResponse } from "@/types/export";
|
||||
import { ShareTimestampContent } from "./ShareTimestampDialog";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FaTriangleExclamation } from "react-icons/fa6";
|
||||
import { MdHighQuality } from "react-icons/md";
|
||||
import { QualitySelectorContent } from "../player/QualitySelector";
|
||||
import {
|
||||
AutoQualityReason,
|
||||
PlaybackQuality,
|
||||
RecordingCoverage,
|
||||
} from "@/types/record";
|
||||
|
||||
type DrawerMode =
|
||||
| "none"
|
||||
@@ -39,25 +52,8 @@ type DrawerMode =
|
||||
| "calendar"
|
||||
| "filter"
|
||||
| "debug-replay"
|
||||
| "share-timestamp";
|
||||
|
||||
const DRAWER_FEATURES = [
|
||||
"export",
|
||||
"calendar",
|
||||
"filter",
|
||||
"debug-replay",
|
||||
"share-timestamp",
|
||||
"motion-search",
|
||||
] as const;
|
||||
export type DrawerFeatures = (typeof DRAWER_FEATURES)[number];
|
||||
const DEFAULT_DRAWER_FEATURES: DrawerFeatures[] = [
|
||||
"export",
|
||||
"calendar",
|
||||
"filter",
|
||||
"debug-replay",
|
||||
"share-timestamp",
|
||||
"motion-search",
|
||||
];
|
||||
| "share-timestamp"
|
||||
| "quality";
|
||||
|
||||
type MobileReviewSettingsDrawerProps = {
|
||||
features?: DrawerFeatures[];
|
||||
@@ -84,6 +80,12 @@ type MobileReviewSettingsDrawerProps = {
|
||||
setRange: (range: TimeRange | undefined) => void;
|
||||
setMode: (mode: ExportMode) => void;
|
||||
setShowExportPreview: (showPreview: boolean) => void;
|
||||
quality?: PlaybackQuality;
|
||||
onSetQuality?: (quality: PlaybackQuality) => void;
|
||||
qualityStreams?: RecordingCoverage["streams"];
|
||||
qualityAutoLow?: boolean;
|
||||
qualityAutoLowReason?: AutoQualityReason;
|
||||
qualityMainUnsupported?: boolean;
|
||||
};
|
||||
export default function MobileReviewSettingsDrawer({
|
||||
features = DEFAULT_DRAWER_FEATURES,
|
||||
@@ -110,12 +112,19 @@ export default function MobileReviewSettingsDrawer({
|
||||
setRange,
|
||||
setMode,
|
||||
setShowExportPreview,
|
||||
quality,
|
||||
onSetQuality,
|
||||
qualityStreams,
|
||||
qualityAutoLow,
|
||||
qualityAutoLowReason,
|
||||
qualityMainUnsupported,
|
||||
}: MobileReviewSettingsDrawerProps) {
|
||||
const { t } = useTranslation([
|
||||
"views/recording",
|
||||
"components/dialog",
|
||||
"views/replay",
|
||||
"views/events",
|
||||
"components/player",
|
||||
"common",
|
||||
]);
|
||||
const isAdmin = useIsAdmin();
|
||||
@@ -395,6 +404,21 @@ export default function MobileReviewSettingsDrawer({
|
||||
{t("filter")}
|
||||
</Button>
|
||||
)}
|
||||
{features.includes("quality") && onSetQuality && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label={t("quality.label", { ns: "components/player" })}
|
||||
onClick={() => setDrawerMode("quality")}
|
||||
>
|
||||
<div className="relative">
|
||||
<MdHighQuality className="size-5 rounded-md bg-secondary-foreground fill-secondary p-1" />
|
||||
{qualityAutoLow && (
|
||||
<FaTriangleExclamation className="absolute -bottom-1 -right-1 size-2.5 text-danger" />
|
||||
)}
|
||||
</div>
|
||||
{t("quality.label", { ns: "components/player" })}
|
||||
</Button>
|
||||
)}
|
||||
{features.includes("share-timestamp") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
@@ -623,6 +647,33 @@ export default function MobileReviewSettingsDrawer({
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (drawerMode == "quality") {
|
||||
content = (
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="relative mb-2 h-8 w-full">
|
||||
<div
|
||||
className="absolute left-0 text-selected"
|
||||
onClick={() => setDrawerMode("select")}
|
||||
>
|
||||
{t("button.back", { ns: "common" })}
|
||||
</div>
|
||||
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
|
||||
{t("quality.label", { ns: "components/player" })}
|
||||
</div>
|
||||
</div>
|
||||
<QualitySelectorContent
|
||||
quality={quality ?? "auto"}
|
||||
onSetQuality={(newQuality) => {
|
||||
onSetQuality?.(newQuality);
|
||||
setDrawerMode("none");
|
||||
}}
|
||||
streams={qualityStreams}
|
||||
autoLow={qualityAutoLow}
|
||||
autoLowReason={qualityAutoLowReason}
|
||||
mainUnsupported={qualityMainUnsupported}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (drawerMode == "share-timestamp") {
|
||||
content = (
|
||||
<div className="w-full">
|
||||
|
||||
Reference in New Issue
Block a user