add inherit and none to ffmpeg args widget (#22535)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

This commit is contained in:
Josh Hawkins 2026-03-19 14:11:34 -05:00 committed by GitHub
parent ede8b74371
commit a9a2eecebb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 104 additions and 13 deletions

View File

@ -1316,6 +1316,8 @@
"preset": "Preset", "preset": "Preset",
"manual": "Manual arguments", "manual": "Manual arguments",
"inherit": "Inherit from camera setting", "inherit": "Inherit from camera setting",
"none": "None",
"useGlobalSetting": "Inherit from global setting",
"selectPreset": "Select preset", "selectPreset": "Select preset",
"manualPlaceholder": "Enter FFmpeg arguments" "manualPlaceholder": "Enter FFmpeg arguments"
}, },

View File

@ -1,7 +1,9 @@
import type { WidgetProps } from "@rjsf/utils"; import type { WidgetProps } from "@rjsf/utils";
import useSWR from "swr"; 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 { useTranslation } from "react-i18next";
import get from "lodash/get";
import isEqual from "lodash/isEqual";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ConfigFormContext } from "@/types/configForm"; import { ConfigFormContext } from "@/types/configForm";
import { import {
@ -22,7 +24,7 @@ type FfmpegPresetResponse = {
}; };
}; };
type FfmpegArgsMode = "preset" | "manual" | "inherit"; type FfmpegArgsMode = "preset" | "manual" | "inherit" | "none";
type PresetField = type PresetField =
| "hwaccel_args" | "hwaccel_args"
@ -60,8 +62,8 @@ const resolveMode = (
defaultMode: FfmpegArgsMode, defaultMode: FfmpegArgsMode,
allowInherit: boolean, allowInherit: boolean,
): FfmpegArgsMode => { ): FfmpegArgsMode => {
if (allowInherit && (value === null || value === undefined)) { if (value === null || value === undefined) {
return "inherit"; return allowInherit ? "inherit" : "none";
} }
if (allowInherit && Array.isArray(value) && value.length === 0) { if (allowInherit && Array.isArray(value) && value.length === 0) {
@ -122,6 +124,19 @@ export function FfmpegArgsWidget(props: WidgetProps) {
const hideDescription = options?.hideDescription === true; const hideDescription = options?.hideDescription === true;
const useSplitLayout = options?.splitLayout !== false; 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<string, unknown>, presetField);
}, [showUseGlobalSetting, formContext?.globalValue, presetField]);
const { data } = useSWR<FfmpegPresetResponse>("ffmpeg/presets"); const { data } = useSWR<FfmpegPresetResponse>("ffmpeg/presets");
const presetOptions = useMemo( const presetOptions = useMemo(
@ -132,14 +147,48 @@ export function FfmpegArgsWidget(props: WidgetProps) {
const canUsePresets = presetOptions.length > 0; const canUsePresets = presetOptions.length > 0;
const defaultMode: FfmpegArgsMode = canUsePresets ? "preset" : "manual"; const defaultMode: FfmpegArgsMode = canUsePresets ? "preset" : "manual";
const detectedMode = useMemo( // Detect if this field's value is effectively inherited from the global
() => resolveMode(value, presetOptions, defaultMode, allowInherit), // config (i.e. the camera does not override it).
[value, presetOptions, defaultMode, allowInherit], 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<FfmpegArgsMode>(detectedMode); const [mode, setMode] = useState<FfmpegArgsMode>(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(() => { useEffect(() => {
userSetModeRef.current = false;
}, [id, formIsClean]);
useEffect(() => {
if (userSetModeRef.current) return;
if (!canUsePresets && detectedMode === "preset") { if (!canUsePresets && detectedMode === "preset") {
setMode("manual"); setMode("manual");
return; return;
@ -150,6 +199,7 @@ export function FfmpegArgsWidget(props: WidgetProps) {
const handleModeChange = useCallback( const handleModeChange = useCallback(
(nextMode: FfmpegArgsMode) => { (nextMode: FfmpegArgsMode) => {
userSetModeRef.current = true;
setMode(nextMode); setMode(nextMode);
if (nextMode === "inherit") { if (nextMode === "inherit") {
@ -157,6 +207,11 @@ export function FfmpegArgsWidget(props: WidgetProps) {
return; return;
} }
if (nextMode === "none") {
onChange(undefined);
return;
}
if (nextMode === "preset") { if (nextMode === "preset") {
const currentValue = typeof value === "string" ? value : undefined; const currentValue = typeof value === "string" ? value : undefined;
const presetValue = const presetValue =
@ -203,7 +258,6 @@ export function FfmpegArgsWidget(props: WidgetProps) {
return undefined; return undefined;
} }
const isInputScoped = id.includes("_inputs_");
const prefix = isInputScoped ? "ffmpeg.inputs" : "ffmpeg"; const prefix = isInputScoped ? "ffmpeg.inputs" : "ffmpeg";
if (presetField === "hwaccel_args") { if (presetField === "hwaccel_args") {
@ -227,7 +281,7 @@ export function FfmpegArgsWidget(props: WidgetProps) {
} }
return undefined; return undefined;
}, [id, presetField]); }, [isInputScoped, presetField]);
const translatedDescription = const translatedDescription =
fallbackDescriptionKey && fallbackDescriptionKey &&
@ -247,7 +301,25 @@ export function FfmpegArgsWidget(props: WidgetProps) {
onValueChange={(next) => handleModeChange(next as FfmpegArgsMode)} onValueChange={(next) => handleModeChange(next as FfmpegArgsMode)}
className="gap-3" className="gap-3"
> >
{allowInherit ? ( {showUseGlobalSetting ? (
<div className="flex items-center space-x-2">
<RadioGroupItem
value="inherit"
id={`${id}-inherit`}
disabled={disabled || readonly}
className={
mode === "inherit"
? "bg-selected from-selected/50 to-selected/90 text-selected"
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
}
/>
<label htmlFor={`${id}-inherit`} className="cursor-pointer text-sm">
{t("configForm.ffmpegArgs.useGlobalSetting", {
ns: "views/settings",
})}
</label>
</div>
) : allowInherit ? (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RadioGroupItem <RadioGroupItem
value="inherit" value="inherit"
@ -263,7 +335,23 @@ export function FfmpegArgsWidget(props: WidgetProps) {
{t("configForm.ffmpegArgs.inherit", { ns: "views/settings" })} {t("configForm.ffmpegArgs.inherit", { ns: "views/settings" })}
</label> </label>
</div> </div>
) : null} ) : (
<div className="flex items-center space-x-2">
<RadioGroupItem
value="none"
id={`${id}-none`}
disabled={disabled || readonly}
className={
mode === "none"
? "bg-selected from-selected/50 to-selected/90 text-selected"
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
}
/>
<label htmlFor={`${id}-none`} className="cursor-pointer text-sm">
{t("configForm.ffmpegArgs.none", { ns: "views/settings" })}
</label>
</div>
)}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RadioGroupItem <RadioGroupItem
value="preset" value="preset"
@ -296,7 +384,8 @@ export function FfmpegArgsWidget(props: WidgetProps) {
</div> </div>
</RadioGroup> </RadioGroup>
{mode === "inherit" ? null : mode === "preset" && canUsePresets ? ( {mode === "inherit" || mode === "none" ? null : mode === "preset" &&
canUsePresets ? (
<Select <Select
value={presetValue} value={presetValue}
onValueChange={handlePresetChange} onValueChange={handlePresetChange}