mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-28 11:08:22 +03:00
add backend endpoint and frontend widget for ffmpeg presets and manual args
This commit is contained in:
parent
7f916dfb47
commit
3777f1d906
@ -196,6 +196,54 @@ def config(request: Request):
|
|||||||
return JSONResponse(content=config)
|
return JSONResponse(content=config)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ffmpeg/presets", dependencies=[Depends(allow_any_authenticated())])
|
||||||
|
def ffmpeg_presets():
|
||||||
|
"""Return available ffmpeg preset keys for config UI usage."""
|
||||||
|
|
||||||
|
# Whitelist based on documented presets in ffmpeg_presets.md
|
||||||
|
hwaccel_presets = {
|
||||||
|
"preset-rpi-64-h264",
|
||||||
|
"preset-rpi-64-h265",
|
||||||
|
"preset-vaapi",
|
||||||
|
"preset-intel-qsv-h264",
|
||||||
|
"preset-intel-qsv-h265",
|
||||||
|
"preset-nvidia",
|
||||||
|
"preset-jetson-h264",
|
||||||
|
"preset-jetson-h265",
|
||||||
|
"preset-rkmpp",
|
||||||
|
}
|
||||||
|
input_presets = {
|
||||||
|
"preset-http-jpeg-generic",
|
||||||
|
"preset-http-mjpeg-generic",
|
||||||
|
"preset-http-reolink",
|
||||||
|
"preset-rtmp-generic",
|
||||||
|
"preset-rtsp-generic",
|
||||||
|
"preset-rtsp-restream",
|
||||||
|
"preset-rtsp-restream-low-latency",
|
||||||
|
"preset-rtsp-udp",
|
||||||
|
"preset-rtsp-blue-iris",
|
||||||
|
}
|
||||||
|
record_output_presets = {
|
||||||
|
"preset-record-generic",
|
||||||
|
"preset-record-generic-audio-copy",
|
||||||
|
"preset-record-generic-audio-aac",
|
||||||
|
"preset-record-mjpeg",
|
||||||
|
"preset-record-jpeg",
|
||||||
|
"preset-record-ubiquiti",
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"hwaccel_args": hwaccel_presets,
|
||||||
|
"input_args": input_presets,
|
||||||
|
"output_args": {
|
||||||
|
"record": record_output_presets,
|
||||||
|
"detect": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config/raw_paths", dependencies=[Depends(require_role(["admin"]))])
|
@router.get("/config/raw_paths", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def config_raw_paths(request: Request):
|
def config_raw_paths(request: Request):
|
||||||
"""Admin-only endpoint that returns camera paths and go2rtc streams without credential masking."""
|
"""Admin-only endpoint that returns camera paths and go2rtc streams without credential masking."""
|
||||||
|
|||||||
@ -1196,6 +1196,12 @@
|
|||||||
"keyPlaceholder": "New key",
|
"keyPlaceholder": "New key",
|
||||||
"remove": "Remove"
|
"remove": "Remove"
|
||||||
},
|
},
|
||||||
|
"ffmpegArgs": {
|
||||||
|
"preset": "Preset",
|
||||||
|
"manual": "Manual arguments",
|
||||||
|
"selectPreset": "Select preset",
|
||||||
|
"manualPlaceholder": "Enter FFmpeg arguments"
|
||||||
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"detect": "Detection",
|
"detect": "Detection",
|
||||||
"record": "Recording",
|
"record": "Recording",
|
||||||
|
|||||||
@ -1,8 +1,30 @@
|
|||||||
import type { SectionConfigOverrides } from "./types";
|
import type { SectionConfigOverrides } from "./types";
|
||||||
|
|
||||||
|
const arrayAsTextWidget = {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ffmpegArgsWidget = (presetField: string) => ({
|
||||||
|
"ui:widget": "FfmpegArgsWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
ffmpegPresetField: presetField,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const ffmpeg: SectionConfigOverrides = {
|
const ffmpeg: SectionConfigOverrides = {
|
||||||
base: {
|
base: {
|
||||||
sectionDocs: "/configuration/ffmpeg_presets",
|
sectionDocs: "/configuration/ffmpeg_presets",
|
||||||
|
fieldDocs: {
|
||||||
|
hwaccel_args: "/configuration/ffmpeg_presets#hwaccel-presets",
|
||||||
|
"inputs.hwaccel_args": "/configuration/ffmpeg_presets#hwaccel-presets",
|
||||||
|
input_args: "/configuration/ffmpeg_presets#input-args-presets",
|
||||||
|
"inputs.input_args": "/configuration/ffmpeg_presets#input-args-presets",
|
||||||
|
"output_args.record": "/configuration/ffmpeg_presets#output-args-presets",
|
||||||
|
},
|
||||||
restartRequired: [],
|
restartRequired: [],
|
||||||
fieldOrder: [
|
fieldOrder: [
|
||||||
"inputs",
|
"inputs",
|
||||||
@ -36,94 +58,26 @@ const ffmpeg: SectionConfigOverrides = {
|
|||||||
"gpu",
|
"gpu",
|
||||||
],
|
],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
global_args: {
|
global_args: arrayAsTextWidget,
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
||||||
"ui:options": {
|
input_args: ffmpegArgsWidget("input_args"),
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hwaccel_args: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
input_args: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
output_args: {
|
output_args: {
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
detect: arrayAsTextWidget,
|
||||||
"ui:options": {
|
record: ffmpegArgsWidget("output_args.record"),
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
detect: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
record: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
items: {
|
items: {
|
||||||
detect: {
|
detect: arrayAsTextWidget,
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
record: ffmpegArgsWidget("output_args.record"),
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
record: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputs: {
|
inputs: {
|
||||||
items: {
|
items: {
|
||||||
global_args: {
|
global_args: arrayAsTextWidget,
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
||||||
"ui:options": {
|
input_args: ffmpegArgsWidget("input_args"),
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hwaccel_args: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
input_args: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
output_args: {
|
output_args: {
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
items: {
|
items: {
|
||||||
detect: {
|
detect: arrayAsTextWidget,
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
record: ffmpegArgsWidget("output_args.record"),
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
record: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -151,41 +105,12 @@ const ffmpeg: SectionConfigOverrides = {
|
|||||||
"gpu",
|
"gpu",
|
||||||
],
|
],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
global_args: {
|
global_args: arrayAsTextWidget,
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
||||||
"ui:options": {
|
input_args: ffmpegArgsWidget("input_args"),
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hwaccel_args: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
input_args: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
output_args: {
|
output_args: {
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
detect: arrayAsTextWidget,
|
||||||
"ui:options": {
|
record: ffmpegArgsWidget("output_args.record"),
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
detect: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
record: {
|
|
||||||
"ui:widget": "ArrayAsTextWidget",
|
|
||||||
"ui:options": {
|
|
||||||
suppressMultiSchema: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import type { FormValidation, UiSchema } from "@rjsf/utils";
|
|||||||
import {
|
import {
|
||||||
modifySchemaForSection,
|
modifySchemaForSection,
|
||||||
getEffectiveDefaultsForSection,
|
getEffectiveDefaultsForSection,
|
||||||
|
sanitizeOverridesForSection,
|
||||||
} from "./section-special-cases";
|
} from "./section-special-cases";
|
||||||
import { getSectionValidation } from "../section-validations";
|
import { getSectionValidation } from "../section-validations";
|
||||||
import {
|
import {
|
||||||
@ -348,6 +349,13 @@ export function ConfigSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(current)) {
|
if (Array.isArray(current)) {
|
||||||
|
if (
|
||||||
|
current.length === 0 &&
|
||||||
|
(base === undefined || base === null) &&
|
||||||
|
(defaults === undefined || defaults === null)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(base === undefined &&
|
(base === undefined &&
|
||||||
defaults !== undefined &&
|
defaults !== undefined &&
|
||||||
@ -366,6 +374,10 @@ export function ConfigSection({
|
|||||||
|
|
||||||
const result: JsonObject = {};
|
const result: JsonObject = {};
|
||||||
for (const [key, value] of Object.entries(currentObj)) {
|
for (const [key, value] of Object.entries(currentObj)) {
|
||||||
|
if (value === undefined && baseObj && baseObj[key] !== undefined) {
|
||||||
|
result[key] = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const overrideValue = buildOverrides(
|
const overrideValue = buildOverrides(
|
||||||
value,
|
value,
|
||||||
baseObj ? baseObj[key] : undefined,
|
baseObj ? baseObj[key] : undefined,
|
||||||
@ -376,6 +388,18 @@ export function ConfigSection({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (baseObj) {
|
||||||
|
for (const [key, baseValue] of Object.entries(baseObj)) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(currentObj, key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (baseValue === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result[key] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Object.keys(result).length > 0 ? result : undefined;
|
return Object.keys(result).length > 0 ? result : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -506,25 +530,34 @@ export function ConfigSection({
|
|||||||
rawData,
|
rawData,
|
||||||
effectiveSchemaDefaults,
|
effectiveSchemaDefaults,
|
||||||
);
|
);
|
||||||
|
const sanitizedOverrides = sanitizeOverridesForSection(
|
||||||
|
sectionPath,
|
||||||
|
level,
|
||||||
|
overrides,
|
||||||
|
);
|
||||||
|
|
||||||
if (!overrides || Object.keys(overrides).length === 0) {
|
if (
|
||||||
|
!sanitizedOverrides ||
|
||||||
|
typeof sanitizedOverrides !== "object" ||
|
||||||
|
Object.keys(sanitizedOverrides).length === 0
|
||||||
|
) {
|
||||||
setPendingData(null);
|
setPendingData(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsRestart = requiresRestartForOverrides(overrides);
|
const needsRestart = requiresRestartForOverrides(sanitizedOverrides);
|
||||||
|
|
||||||
await axios.put("config/set", {
|
await axios.put("config/set", {
|
||||||
requires_restart: needsRestart ? 1 : 0,
|
requires_restart: needsRestart ? 1 : 0,
|
||||||
update_topic: updateTopic,
|
update_topic: updateTopic,
|
||||||
config_data: {
|
config_data: {
|
||||||
[basePath]: overrides,
|
[basePath]: sanitizedOverrides,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// log save to console for debugging
|
// log save to console for debugging
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log("Saved config data:", {
|
console.log("Saved config data:", {
|
||||||
[basePath]: overrides,
|
[basePath]: sanitizedOverrides,
|
||||||
update_topic: updateTopic,
|
update_topic: updateTopic,
|
||||||
requires_restart: needsRestart ? 1 : 0,
|
requires_restart: needsRestart ? 1 : 0,
|
||||||
});
|
});
|
||||||
@ -776,6 +809,9 @@ export function ConfigSection({
|
|||||||
cameraName,
|
cameraName,
|
||||||
globalValue,
|
globalValue,
|
||||||
cameraValue,
|
cameraValue,
|
||||||
|
hasChanges,
|
||||||
|
formData: (pendingData || formData) as ConfigSectionData,
|
||||||
|
onFormDataChange: (data: ConfigSectionData) => handleChange(data),
|
||||||
// For widgets that need access to full camera config (e.g., zone names)
|
// For widgets that need access to full camera config (e.g., zone names)
|
||||||
fullCameraConfig:
|
fullCameraConfig:
|
||||||
level === "camera" && cameraName
|
level === "camera" && cameraName
|
||||||
|
|||||||
@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
import { RJSFSchema } from "@rjsf/utils";
|
import { RJSFSchema } from "@rjsf/utils";
|
||||||
import { applySchemaDefaults } from "@/lib/config-schema";
|
import { applySchemaDefaults } from "@/lib/config-schema";
|
||||||
|
import { isJsonObject } from "@/lib/utils";
|
||||||
|
import { JsonObject } from "@/types/configForm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sections that require special handling at the global level.
|
* Sections that require special handling at the global level.
|
||||||
@ -102,3 +104,47 @@ export function getEffectiveDefaultsForSection(
|
|||||||
|
|
||||||
return schemaDefaults;
|
return schemaDefaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize overrides payloads for section-specific quirks.
|
||||||
|
*/
|
||||||
|
export function sanitizeOverridesForSection(
|
||||||
|
sectionPath: string,
|
||||||
|
level: string,
|
||||||
|
overrides: unknown,
|
||||||
|
): unknown {
|
||||||
|
if (!overrides || !isJsonObject(overrides)) {
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sectionPath === "ffmpeg" && level === "camera") {
|
||||||
|
const overridesObj = overrides as JsonObject;
|
||||||
|
const inputs = overridesObj.inputs;
|
||||||
|
if (!Array.isArray(inputs)) {
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedInputs = inputs.map((input) => {
|
||||||
|
if (!isJsonObject(input)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedInput = { ...input } as JsonObject;
|
||||||
|
["global_args", "hwaccel_args", "input_args"].forEach((key) => {
|
||||||
|
const value = cleanedInput[key];
|
||||||
|
if (Array.isArray(value) && value.length === 0) {
|
||||||
|
delete cleanedInput[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cleanedInput;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...overridesObj,
|
||||||
|
inputs: cleanedInputs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import { ObjectLabelSwitchesWidget } from "./widgets/ObjectLabelSwitchesWidget";
|
|||||||
import { AudioLabelSwitchesWidget } from "./widgets/AudioLabelSwitchesWidget";
|
import { AudioLabelSwitchesWidget } from "./widgets/AudioLabelSwitchesWidget";
|
||||||
import { ZoneSwitchesWidget } from "./widgets/ZoneSwitchesWidget";
|
import { ZoneSwitchesWidget } from "./widgets/ZoneSwitchesWidget";
|
||||||
import { ArrayAsTextWidget } from "./widgets/ArrayAsTextWidget";
|
import { ArrayAsTextWidget } from "./widgets/ArrayAsTextWidget";
|
||||||
|
import { FfmpegArgsWidget } from "./widgets/FfmpegArgsWidget";
|
||||||
|
|
||||||
import { FieldTemplate } from "./templates/FieldTemplate";
|
import { FieldTemplate } from "./templates/FieldTemplate";
|
||||||
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
|
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
|
||||||
@ -48,6 +49,7 @@ export const frigateTheme: FrigateTheme = {
|
|||||||
SelectWidget: SelectWidget,
|
SelectWidget: SelectWidget,
|
||||||
CheckboxWidget: SwitchWidget,
|
CheckboxWidget: SwitchWidget,
|
||||||
ArrayAsTextWidget: ArrayAsTextWidget,
|
ArrayAsTextWidget: ArrayAsTextWidget,
|
||||||
|
FfmpegArgsWidget: FfmpegArgsWidget,
|
||||||
// Custom widgets
|
// Custom widgets
|
||||||
switch: SwitchWidget,
|
switch: SwitchWidget,
|
||||||
password: PasswordWidget,
|
password: PasswordWidget,
|
||||||
|
|||||||
@ -0,0 +1,249 @@
|
|||||||
|
import type { WidgetProps } from "@rjsf/utils";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
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";
|
||||||
|
|
||||||
|
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,
|
||||||
|
): FfmpegArgsMode => {
|
||||||
|
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 { t } = useTranslation(["views/settings"]);
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
readonly,
|
||||||
|
options,
|
||||||
|
placeholder,
|
||||||
|
schema,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
const presetField = options?.ffmpegPresetField as PresetField | undefined;
|
||||||
|
|
||||||
|
const { data } = useSWR<FfmpegPresetResponse>("ffmpeg/presets");
|
||||||
|
|
||||||
|
const presetOptions = useMemo(
|
||||||
|
() => getPresetOptions(data, presetField),
|
||||||
|
[data, presetField],
|
||||||
|
);
|
||||||
|
|
||||||
|
const canUsePresets = presetOptions.length > 0;
|
||||||
|
const defaultMode: FfmpegArgsMode = canUsePresets ? "preset" : "manual";
|
||||||
|
|
||||||
|
const detectedMode = useMemo(
|
||||||
|
() => resolveMode(value, presetOptions, defaultMode),
|
||||||
|
[value, presetOptions, defaultMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [mode, setMode] = useState<FfmpegArgsMode>(detectedMode);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canUsePresets) {
|
||||||
|
setMode("manual");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMode(detectedMode);
|
||||||
|
}, [canUsePresets, detectedMode]);
|
||||||
|
|
||||||
|
const handleModeChange = useCallback(
|
||||||
|
(nextMode: FfmpegArgsMode) => {
|
||||||
|
setMode(nextMode);
|
||||||
|
|
||||||
|
if (nextMode === "preset") {
|
||||||
|
const currentValue = typeof value === "string" ? value : undefined;
|
||||||
|
const presetValue =
|
||||||
|
currentValue && presetOptions.includes(currentValue)
|
||||||
|
? currentValue
|
||||||
|
: presetOptions[0];
|
||||||
|
if (presetValue) {
|
||||||
|
onChange(presetValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manualText = normalizeManualText(value);
|
||||||
|
onChange(manualText);
|
||||||
|
},
|
||||||
|
[onChange, presetOptions, value],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePresetChange = useCallback(
|
||||||
|
(preset: string) => {
|
||||||
|
onChange(preset);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleManualChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newText = event.target.value;
|
||||||
|
onChange(newText);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const manualValue = normalizeManualText(value);
|
||||||
|
const presetValue =
|
||||||
|
typeof value === "string" && presetOptions.includes(value) ? value : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<RadioGroup
|
||||||
|
value={mode}
|
||||||
|
onValueChange={(next) => handleModeChange(next as FfmpegArgsMode)}
|
||||||
|
className="gap-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
value="preset"
|
||||||
|
id={`${id}-preset`}
|
||||||
|
disabled={disabled || readonly || !canUsePresets}
|
||||||
|
className={
|
||||||
|
mode === "preset"
|
||||||
|
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
||||||
|
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label htmlFor={`${id}-preset`} className="cursor-pointer text-sm">
|
||||||
|
{t("configForm.ffmpegArgs.preset", { ns: "views/settings" })}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
value="manual"
|
||||||
|
id={`${id}-manual`}
|
||||||
|
disabled={disabled || readonly}
|
||||||
|
className={
|
||||||
|
mode === "manual"
|
||||||
|
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
||||||
|
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label htmlFor={`${id}-manual`} className="cursor-pointer text-sm">
|
||||||
|
{t("configForm.ffmpegArgs.manual", { ns: "views/settings" })}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
{mode === "preset" && canUsePresets ? (
|
||||||
|
<Select
|
||||||
|
value={presetValue}
|
||||||
|
onValueChange={handlePresetChange}
|
||||||
|
disabled={disabled || readonly}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={id} className="w-full">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
placeholder ||
|
||||||
|
schema.title ||
|
||||||
|
t("configForm.ffmpegArgs.selectPreset", {
|
||||||
|
ns: "views/settings",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{presetOptions.map((preset) => (
|
||||||
|
<SelectItem key={preset} value={preset}>
|
||||||
|
{preset}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id={id}
|
||||||
|
value={manualValue}
|
||||||
|
onChange={handleManualChange}
|
||||||
|
disabled={disabled || readonly}
|
||||||
|
placeholder={
|
||||||
|
placeholder ||
|
||||||
|
t("configForm.ffmpegArgs.manualPlaceholder", {
|
||||||
|
ns: "views/settings",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user