This commit is contained in:
Josh Hawkins 2026-02-12 22:20:26 -06:00
parent ad00001049
commit 23c6e5231f
16 changed files with 183 additions and 46 deletions

View File

@ -35,13 +35,13 @@ DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [
class FfmpegOutputArgsConfig(FrigateBaseModel):
detect: Union[str, list[str]] = Field(
default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT,
title="Detect output args",
description="Default output args for detect role streams.",
title="Detect output arguments",
description="Default output arguments for detect role streams.",
)
record: Union[str, list[str]] = Field(
default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT,
title="Record output args",
description="Default output args for record role streams.",
title="Record output arguments",
description="Default output arguments for record role streams.",
)
@ -124,17 +124,17 @@ class CameraInput(FrigateBaseModel):
)
global_args: Union[str, list[str]] = Field(
default_factory=list,
title="FFmpeg global args",
title="FFmpeg global arguments",
description="FFmpeg global arguments for this input stream.",
)
hwaccel_args: Union[str, list[str]] = Field(
default_factory=list,
title="Hardware acceleration args",
title="Hardware acceleration arguments",
description="Hardware acceleration arguments for this input stream.",
)
input_args: Union[str, list[str]] = Field(
default_factory=list,
title="Input args",
title="Input arguments",
description="Input arguments specific to this stream.",
)

View File

@ -448,7 +448,7 @@ class FrigateConfig(FrigateBaseModel):
timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig,
title="Timestamp style",
description="Styling options for in-feed timestamps applied to recordings and snapshots.",
description="Styling options for in-feed timestamps applied to debug view and snapshots.",
)
# Classification Config

View File

@ -170,12 +170,12 @@
"label": "Output arguments",
"description": "Default output arguments used for different FFmpeg roles such as detect and record.",
"detect": {
"label": "Detect output args",
"description": "Default output args for detect role streams."
"label": "Detect output arguments",
"description": "Default output arguments for detect role streams."
},
"record": {
"label": "Record output args",
"description": "Default output args for record role streams."
"label": "Record output arguments",
"description": "Default output arguments for record role streams."
}
},
"retry_interval": {
@ -202,15 +202,15 @@
"description": "Roles for this input stream (for example: detect, record, audio)."
},
"global_args": {
"label": "FFmpeg global args",
"label": "FFmpeg global arguments",
"description": "FFmpeg global arguments for this input stream."
},
"hwaccel_args": {
"label": "Hardware acceleration args",
"label": "Hardware acceleration arguments",
"description": "Hardware acceleration arguments for this input stream."
},
"input_args": {
"label": "Input args",
"label": "Input arguments",
"description": "Input arguments specific to this stream."
}
}

View File

@ -1313,12 +1313,12 @@
"label": "Output arguments",
"description": "Default output arguments used for different FFmpeg roles such as detect and record.",
"detect": {
"label": "Detect output args",
"description": "Default output args for detect role streams."
"label": "Detect output arguments",
"description": "Default output arguments for detect role streams."
},
"record": {
"label": "Record output args",
"description": "Default output args for record role streams."
"label": "Record output arguments",
"description": "Default output arguments for record role streams."
}
},
"retry_interval": {
@ -1345,15 +1345,15 @@
"description": "Roles for this input stream (for example: detect, record, audio)."
},
"global_args": {
"label": "FFmpeg global args",
"label": "FFmpeg global arguments",
"description": "FFmpeg global arguments for this input stream."
},
"hwaccel_args": {
"label": "Hardware acceleration args",
"label": "Hardware acceleration arguments",
"description": "Hardware acceleration arguments for this input stream."
},
"input_args": {
"label": "Input args",
"label": "Input arguments",
"description": "Input arguments specific to this stream."
}
}
@ -1762,7 +1762,7 @@
},
"timestamp_style": {
"label": "Timestamp style",
"description": "Styling options for in-feed timestamps applied to recordings and snapshots.",
"description": "Styling options for in-feed timestamps applied to debug view and snapshots.",
"position": {
"label": "Timestamp position",
"description": "Position of the timestamp on the image (tl/tr/bl/br)."

View File

@ -1298,6 +1298,15 @@
"summary": "{{count}} selected",
"empty": "No zones available"
},
"inputRoles": {
"summary": "{{count}} roles selected",
"empty": "No roles available",
"options": {
"detect": "Detect",
"record": "Record",
"audio": "Audio"
}
},
"review": {
"title": "Review Settings"
},

View File

@ -22,6 +22,11 @@ const birdseye: SectionConfigOverrides = {
"idle_heartbeat_fps",
],
advancedFields: ["width", "height", "quality", "inactivity_threshold"],
uiSchema: {
mode: {
"ui:size": "xs",
},
},
},
};

View File

@ -77,7 +77,12 @@ const ffmpeg: SectionConfigOverrides = {
path: {
"ui:options": { size: "full" },
},
global_args: arrayAsTextWidget,
roles: {
"ui:widget": "inputRoles",
},
global_args: {
"ui:widget": "hidden",
},
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
input_args: ffmpegArgsWidget("input_args"),
output_args: {

View File

@ -48,23 +48,8 @@ const mqtt: SectionConfigOverrides = {
],
liveValidate: true,
uiSchema: {
host: {
"ui:options": { size: "sm" },
},
topic_prefix: {
"ui:options": { size: "md" },
},
client_id: {
"ui:options": { size: "sm" },
},
tls_ca_certs: {
"ui:options": { size: "md" },
},
tls_client_cert: {
"ui:options": { size: "md" },
},
tls_client_key: {
"ui:options": { size: "md" },
password: {
"ui:options": { size: "xs" },
},
},
},

View File

@ -7,6 +7,14 @@ const timestampStyle: SectionConfigOverrides = {
fieldOrder: ["position", "format", "color", "thickness"],
hiddenFields: ["effect", "enabled_in_config"],
advancedFields: [],
uiSchema: {
position: {
"ui:size": "xs",
},
format: {
"ui:size": "xs",
},
},
},
};

View File

@ -128,7 +128,7 @@ export function AdvancedCollapsible({
{label}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-2 space-y-4 rounded-lg border border-border/60 bg-muted/20 p-4">
<CollapsibleContent className="mt-2 space-y-4 rounded-lg border border-border/60 bg-background_alt/70 p-4">
{children}
</CollapsibleContent>
</Collapsible>

View File

@ -23,6 +23,7 @@ import { AudioLabelSwitchesWidget } from "./widgets/AudioLabelSwitchesWidget";
import { ZoneSwitchesWidget } from "./widgets/ZoneSwitchesWidget";
import { ArrayAsTextWidget } from "./widgets/ArrayAsTextWidget";
import { FfmpegArgsWidget } from "./widgets/FfmpegArgsWidget";
import { InputRolesWidget } from "./widgets/InputRolesWidget";
import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget";
import { FieldTemplate } from "./templates/FieldTemplate";
@ -55,6 +56,7 @@ export const frigateTheme: FrigateTheme = {
CheckboxWidget: SwitchWidget,
ArrayAsTextWidget: ArrayAsTextWidget,
FfmpegArgsWidget: FfmpegArgsWidget,
inputRoles: InputRolesWidget,
// Custom widgets
switch: SwitchWidget,
password: PasswordWidget,

View File

@ -92,7 +92,8 @@ export function AudioLabelSwitchesWidget(props: WidgetProps) {
getEntities,
getDisplayLabel: getAudioLabelDisplayName,
i18nKey: "audioLabels",
listClassName: "max-h-64 overflow-y-auto scrollbar-container",
listClassName:
"max-h-none overflow-visible md:max-h-64 md:overflow-y-auto md:overscroll-contain md:scrollbar-container",
enableSearch: true,
}}
/>

View File

@ -3,6 +3,7 @@ import useSWR from "swr";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Input } from "@/components/ui/input";
import { ConfigFormContext } from "@/types/configForm";
import {
Select,
SelectContent,
@ -86,7 +87,17 @@ const normalizeManualText = (value: unknown): string => {
};
export function FfmpegArgsWidget(props: WidgetProps) {
const { t } = useTranslation(["views/settings"]);
const formContext = props.registry?.formContext as
| ConfigFormContext
| undefined;
const i18nNamespace = formContext?.i18nNamespace as string | undefined;
const isCameraLevel = formContext?.level === "camera";
const effectiveNamespace = isCameraLevel ? "config/cameras" : i18nNamespace;
const { t, i18n } = useTranslation([
effectiveNamespace || i18nNamespace || "common",
i18nNamespace || "common",
"views/settings",
]);
const {
value,
onChange,
@ -165,6 +176,47 @@ export function FfmpegArgsWidget(props: WidgetProps) {
const manualValue = normalizeManualText(value);
const presetValue =
typeof value === "string" && presetOptions.includes(value) ? value : "";
const fallbackDescriptionKey = useMemo(() => {
if (!presetField) {
return undefined;
}
const isInputScoped = id.includes("_inputs_");
const prefix = isInputScoped ? "ffmpeg.inputs" : "ffmpeg";
if (presetField === "hwaccel_args") {
return `${prefix}.hwaccel_args.description`;
}
if (presetField === "input_args") {
return `${prefix}.input_args.description`;
}
if (presetField === "output_args.record") {
return isInputScoped
? "ffmpeg.inputs.output_args.record.description"
: "ffmpeg.output_args.record.description";
}
if (presetField === "output_args.detect") {
return isInputScoped
? "ffmpeg.inputs.output_args.detect.description"
: "ffmpeg.output_args.detect.description";
}
return undefined;
}, [id, presetField]);
const translatedDescription =
fallbackDescriptionKey &&
effectiveNamespace &&
i18n.exists(fallbackDescriptionKey, { ns: effectiveNamespace })
? t(fallbackDescriptionKey, { ns: effectiveNamespace })
: "";
const fieldDescription =
typeof schema.description === "string" && schema.description.length > 0
? schema.description
: translatedDescription;
return (
<div className="space-y-2">
@ -244,6 +296,10 @@ export function FfmpegArgsWidget(props: WidgetProps) {
}
/>
)}
{fieldDescription ? (
<p className="text-xs text-muted-foreground">{fieldDescription}</p>
) : null}
</div>
);
}

View File

@ -0,0 +1,65 @@
import type { WidgetProps } from "@rjsf/utils";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Switch } from "@/components/ui/switch";
const INPUT_ROLES = ["detect", "record", "audio"] as const;
function normalizeValue(value: unknown): string[] {
if (Array.isArray(value)) {
return value.filter((item): item is string => typeof item === "string");
}
if (typeof value === "string" && value.trim()) {
return [value.trim()];
}
return [];
}
export function InputRolesWidget(props: WidgetProps) {
const { id, value, disabled, readonly, onChange } = props;
const { t } = useTranslation(["views/settings"]);
const selectedRoles = useMemo(() => normalizeValue(value), [value]);
const toggleRole = (role: string, enabled: boolean) => {
if (enabled) {
if (!selectedRoles.includes(role)) {
onChange([...selectedRoles, role]);
}
return;
}
onChange(selectedRoles.filter((item) => item !== role));
};
return (
<div className="space-y-2 rounded-lg bg-secondary p-2 pr-0 md:max-w-md">
{INPUT_ROLES.map((role) => {
const checked = selectedRoles.includes(role);
const label = t(`configForm.inputRoles.options.${role}`, {
ns: "views/settings",
defaultValue: role,
});
return (
<div
key={role}
className="flex items-center justify-between rounded-md px-3 py-0"
>
<label htmlFor={`${id}-${role}`} className="text-sm">
{label}
</label>
<Switch
id={`${id}-${role}`}
checked={checked}
disabled={disabled || readonly}
onCheckedChange={(enabled) => toggleRole(role, !!enabled)}
/>
</div>
);
})}
</div>
);
}

View File

@ -93,7 +93,8 @@ export function ObjectLabelSwitchesWidget(props: WidgetProps) {
getEntities: getObjectLabels,
getDisplayLabel: getObjectLabelDisplayName,
i18nKey: "objectLabels",
listClassName: "max-h-64 overflow-y-auto scrollbar-container",
listClassName:
"max-h-none overflow-visible md:max-h-64 md:overflow-y-auto md:overscroll-contain md:scrollbar-container",
}}
/>
);

View File

@ -222,7 +222,7 @@ export default function UiSettingsView() {
];
return (
<div className="flex size-full flex-col md:flex-row">
<div className="flex size-full flex-col md:flex-row md:pb-8">
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
<div className="w-full max-w-5xl space-y-6">