diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 507945ab7..1a3c6a1c9 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -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.", ) diff --git a/frigate/config/config.py b/frigate/config/config.py index c8cef38fe..8b4a9d7d6 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -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 diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index c90d6d99f..ca60fa362 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -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." } } diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index acc039c7e..b67ec7434 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -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)." diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 28b667cbd..a5fe096ae 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -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" }, diff --git a/web/src/components/config-form/section-configs/birdseye.ts b/web/src/components/config-form/section-configs/birdseye.ts index f8affe660..e825c3a7e 100644 --- a/web/src/components/config-form/section-configs/birdseye.ts +++ b/web/src/components/config-form/section-configs/birdseye.ts @@ -22,6 +22,11 @@ const birdseye: SectionConfigOverrides = { "idle_heartbeat_fps", ], advancedFields: ["width", "height", "quality", "inactivity_threshold"], + uiSchema: { + mode: { + "ui:size": "xs", + }, + }, }, }; diff --git a/web/src/components/config-form/section-configs/ffmpeg.ts b/web/src/components/config-form/section-configs/ffmpeg.ts index d18bdbd2b..94a7d7843 100644 --- a/web/src/components/config-form/section-configs/ffmpeg.ts +++ b/web/src/components/config-form/section-configs/ffmpeg.ts @@ -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: { diff --git a/web/src/components/config-form/section-configs/mqtt.ts b/web/src/components/config-form/section-configs/mqtt.ts index 8fabe0ff6..5151b20b5 100644 --- a/web/src/components/config-form/section-configs/mqtt.ts +++ b/web/src/components/config-form/section-configs/mqtt.ts @@ -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" }, }, }, }, diff --git a/web/src/components/config-form/section-configs/timestamp_style.ts b/web/src/components/config-form/section-configs/timestamp_style.ts index c056d7d79..fbcf7dc8b 100644 --- a/web/src/components/config-form/section-configs/timestamp_style.ts +++ b/web/src/components/config-form/section-configs/timestamp_style.ts @@ -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", + }, + }, }, }; diff --git a/web/src/components/config-form/theme/components/index.tsx b/web/src/components/config-form/theme/components/index.tsx index db01d49ff..0cac38209 100644 --- a/web/src/components/config-form/theme/components/index.tsx +++ b/web/src/components/config-form/theme/components/index.tsx @@ -128,7 +128,7 @@ export function AdvancedCollapsible({ {label} - + {children} diff --git a/web/src/components/config-form/theme/frigateTheme.ts b/web/src/components/config-form/theme/frigateTheme.ts index d2d6c5cc3..1d4ac3107 100644 --- a/web/src/components/config-form/theme/frigateTheme.ts +++ b/web/src/components/config-form/theme/frigateTheme.ts @@ -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, diff --git a/web/src/components/config-form/theme/widgets/AudioLabelSwitchesWidget.tsx b/web/src/components/config-form/theme/widgets/AudioLabelSwitchesWidget.tsx index eb6780c0f..13233c6df 100644 --- a/web/src/components/config-form/theme/widgets/AudioLabelSwitchesWidget.tsx +++ b/web/src/components/config-form/theme/widgets/AudioLabelSwitchesWidget.tsx @@ -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, }} /> diff --git a/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx b/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx index 84702de02..b1926db60 100644 --- a/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx +++ b/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx @@ -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 (
@@ -244,6 +296,10 @@ export function FfmpegArgsWidget(props: WidgetProps) { } /> )} + + {fieldDescription ? ( +

{fieldDescription}

+ ) : null}
); } diff --git a/web/src/components/config-form/theme/widgets/InputRolesWidget.tsx b/web/src/components/config-form/theme/widgets/InputRolesWidget.tsx new file mode 100644 index 000000000..efe48bb98 --- /dev/null +++ b/web/src/components/config-form/theme/widgets/InputRolesWidget.tsx @@ -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 ( +
+ {INPUT_ROLES.map((role) => { + const checked = selectedRoles.includes(role); + const label = t(`configForm.inputRoles.options.${role}`, { + ns: "views/settings", + defaultValue: role, + }); + + return ( +
+ + toggleRole(role, !!enabled)} + /> +
+ ); + })} +
+ ); +} diff --git a/web/src/components/config-form/theme/widgets/ObjectLabelSwitchesWidget.tsx b/web/src/components/config-form/theme/widgets/ObjectLabelSwitchesWidget.tsx index 312821378..b7b2df571 100644 --- a/web/src/components/config-form/theme/widgets/ObjectLabelSwitchesWidget.tsx +++ b/web/src/components/config-form/theme/widgets/ObjectLabelSwitchesWidget.tsx @@ -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", }} /> ); diff --git a/web/src/views/settings/UiSettingsView.tsx b/web/src/views/settings/UiSettingsView.tsx index 11950935c..2bff797f4 100644 --- a/web/src/views/settings/UiSettingsView.tsx +++ b/web/src/views/settings/UiSettingsView.tsx @@ -222,7 +222,7 @@ export default function UiSettingsView() { ]; return ( -
+