From a9a2eecebb532bc4e759b391920384c07cc1a936 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:11:34 -0500 Subject: [PATCH] add inherit and none to ffmpeg args widget (#22535) --- web/public/locales/en/views/settings.json | 2 + .../theme/widgets/FfmpegArgsWidget.tsx | 115 ++++++++++++++++-- 2 files changed, 104 insertions(+), 13 deletions(-) diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index a0a674735..f93439244 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1316,6 +1316,8 @@ "preset": "Preset", "manual": "Manual arguments", "inherit": "Inherit from camera setting", + "none": "None", + "useGlobalSetting": "Inherit from global setting", "selectPreset": "Select preset", "manualPlaceholder": "Enter FFmpeg arguments" }, diff --git a/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx b/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx index 415cd2603..dcf34fae8 100644 --- a/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx +++ b/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx @@ -1,7 +1,9 @@ import type { WidgetProps } from "@rjsf/utils"; import useSWR from "swr"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import get from "lodash/get"; +import isEqual from "lodash/isEqual"; import { Input } from "@/components/ui/input"; import { ConfigFormContext } from "@/types/configForm"; import { @@ -22,7 +24,7 @@ type FfmpegPresetResponse = { }; }; -type FfmpegArgsMode = "preset" | "manual" | "inherit"; +type FfmpegArgsMode = "preset" | "manual" | "inherit" | "none"; type PresetField = | "hwaccel_args" @@ -60,8 +62,8 @@ const resolveMode = ( defaultMode: FfmpegArgsMode, allowInherit: boolean, ): FfmpegArgsMode => { - if (allowInherit && (value === null || value === undefined)) { - return "inherit"; + if (value === null || value === undefined) { + return allowInherit ? "inherit" : "none"; } if (allowInherit && Array.isArray(value) && value.length === 0) { @@ -122,6 +124,19 @@ export function FfmpegArgsWidget(props: WidgetProps) { const hideDescription = options?.hideDescription === true; const useSplitLayout = options?.splitLayout !== false; + // Detect camera-level top-level fields (not inside inputs array). + // These should show "Use global setting" instead of "None". + const isInputScoped = id.includes("_inputs_"); + const showUseGlobalSetting = isCameraLevel && !isInputScoped && !allowInherit; + + // Extract the global value for this specific field to detect inheritance + const globalFieldValue = useMemo(() => { + if (!showUseGlobalSetting || !formContext?.globalValue || !presetField) { + return undefined; + } + return get(formContext.globalValue as Record, presetField); + }, [showUseGlobalSetting, formContext?.globalValue, presetField]); + const { data } = useSWR("ffmpeg/presets"); const presetOptions = useMemo( @@ -132,14 +147,48 @@ export function FfmpegArgsWidget(props: WidgetProps) { const canUsePresets = presetOptions.length > 0; const defaultMode: FfmpegArgsMode = canUsePresets ? "preset" : "manual"; - const detectedMode = useMemo( - () => resolveMode(value, presetOptions, defaultMode, allowInherit), - [value, presetOptions, defaultMode, allowInherit], - ); + // Detect if this field's value is effectively inherited from the global + // config (i.e. the camera does not override it). + const isInheritedFromGlobal = useMemo(() => { + if (!showUseGlobalSetting) return false; + if (value === undefined || value === null) return true; + if (globalFieldValue === undefined || globalFieldValue === null) + return false; + return isEqual(value, globalFieldValue); + }, [showUseGlobalSetting, value, globalFieldValue]); + + const detectedMode = useMemo(() => { + if (showUseGlobalSetting && isInheritedFromGlobal) { + return "inherit" as FfmpegArgsMode; + } + return resolveMode(value, presetOptions, defaultMode, allowInherit); + }, [ + showUseGlobalSetting, + isInheritedFromGlobal, + value, + presetOptions, + defaultMode, + allowInherit, + ]); const [mode, setMode] = useState(detectedMode); + // Track whether the user has explicitly changed mode to prevent the + // detected-mode sync from snapping back (e.g. when a user-selected + // preset happens to match the global value). + const userSetModeRef = useRef(false); + + const formIsClean = !formContext?.hasChanges; + + // Reset tracking when the widget identity changes (camera switch) + // or when the form returns to a clean state (after a successful save). useEffect(() => { + userSetModeRef.current = false; + }, [id, formIsClean]); + + useEffect(() => { + if (userSetModeRef.current) return; + if (!canUsePresets && detectedMode === "preset") { setMode("manual"); return; @@ -150,6 +199,7 @@ export function FfmpegArgsWidget(props: WidgetProps) { const handleModeChange = useCallback( (nextMode: FfmpegArgsMode) => { + userSetModeRef.current = true; setMode(nextMode); if (nextMode === "inherit") { @@ -157,6 +207,11 @@ export function FfmpegArgsWidget(props: WidgetProps) { return; } + if (nextMode === "none") { + onChange(undefined); + return; + } + if (nextMode === "preset") { const currentValue = typeof value === "string" ? value : undefined; const presetValue = @@ -203,7 +258,6 @@ export function FfmpegArgsWidget(props: WidgetProps) { return undefined; } - const isInputScoped = id.includes("_inputs_"); const prefix = isInputScoped ? "ffmpeg.inputs" : "ffmpeg"; if (presetField === "hwaccel_args") { @@ -227,7 +281,7 @@ export function FfmpegArgsWidget(props: WidgetProps) { } return undefined; - }, [id, presetField]); + }, [isInputScoped, presetField]); const translatedDescription = fallbackDescriptionKey && @@ -247,7 +301,25 @@ export function FfmpegArgsWidget(props: WidgetProps) { onValueChange={(next) => handleModeChange(next as FfmpegArgsMode)} className="gap-3" > - {allowInherit ? ( + {showUseGlobalSetting ? ( +
+ + +
+ ) : allowInherit ? (
- ) : null} + ) : ( +
+ + +
+ )}
- {mode === "inherit" ? null : mode === "preset" && canUsePresets ? ( + {mode === "inherit" || mode === "none" ? null : mode === "preset" && + canUsePresets ? (