import type { WidgetProps } from "@rjsf/utils"; import useSWR from "swr"; 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; type FfmpegPresetResponse = { hwaccel_args: string[]; input_args: string[]; output_args: { record: string[]; detect: string[]; }; }; type FfmpegArgsMode = "preset" | "manual" | "inherit" | "none"; type PresetField = | "hwaccel_args" | "input_args" | "output_args.record" | "output_args.detect"; const getPresetOptions = ( data: FfmpegPresetResponse | undefined, field: PresetField | undefined, ): string[] => { if (!data || !field) { return []; } if (field === "hwaccel_args") { return data.hwaccel_args; } if (field === "input_args") { return data.input_args; } if (field.startsWith("output_args.")) { const key = field.split(".")[1] as "record" | "detect"; return data.output_args?.[key] ?? []; } return []; }; const resolveMode = ( value: unknown, presets: string[], defaultMode: FfmpegArgsMode, allowInherit: boolean, ): FfmpegArgsMode => { if (value === null || value === undefined) { return allowInherit ? "inherit" : "none"; } if (allowInherit && Array.isArray(value) && value.length === 0) { return "inherit"; } if (Array.isArray(value)) { return "manual"; } if (typeof value === "string") { if (presets.length === 0) { return defaultMode; } return presets.includes(value) ? "preset" : "manual"; } return defaultMode; }; const normalizeManualText = (value: unknown): string => { if (Array.isArray(value)) { return value.join(" "); } if (typeof value === "string") { return value; } return ""; }; export function FfmpegArgsWidget(props: WidgetProps) { 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, disabled, readonly, options, placeholder, schema, id, } = props; const presetField = options?.ffmpegPresetField as PresetField | undefined; const allowInherit = options?.allowInherit === true; 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( () => getPresetOptions(data, presetField), [data, presetField], ); const canUsePresets = presetOptions.length > 0; const defaultMode: FfmpegArgsMode = canUsePresets ? "preset" : "manual"; // 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; } setMode(detectedMode); }, [canUsePresets, detectedMode]); const handleModeChange = useCallback( (nextMode: FfmpegArgsMode) => { userSetModeRef.current = true; setMode(nextMode); if (nextMode === "inherit") { onChange(undefined); return; } if (nextMode === "none") { onChange(undefined); return; } if (nextMode === "preset") { const currentValue = typeof value === "string" ? value : undefined; const presetValue = currentValue && presetOptions.includes(currentValue) ? currentValue : presetOptions[0]; if (presetValue) { onChange(presetValue); } return; } if (mode === "preset") { onChange(""); return; } const manualText = normalizeManualText(value); onChange(manualText); }, [mode, onChange, presetOptions, value], ); const handlePresetChange = useCallback( (preset: string) => { onChange(preset); }, [onChange], ); const handleManualChange = useCallback( (event: React.ChangeEvent) => { const newText = event.target.value; onChange(newText); }, [onChange], ); const manualValue = normalizeManualText(value); const presetValue = typeof value === "string" && presetOptions.includes(value) ? value : ""; const fallbackDescriptionKey = useMemo(() => { if (!presetField) { return undefined; } 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; }, [isInputScoped, 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 (
handleModeChange(next as FfmpegArgsMode)} className="gap-3" > {showUseGlobalSetting ? (
) : allowInherit ? (
) : (
)}
{mode === "inherit" || mode === "none" ? null : mode === "preset" && canUsePresets ? ( ) : ( )} {!hideDescription && !useSplitLayout && fieldDescription ? (

{fieldDescription}

) : null}
); }