Allows defining custom arguments

This commit is contained in:
2025-08-19 10:47:08 +07:00
parent 01306a9688
commit 3921c003bb
4 changed files with 113 additions and 13 deletions
+1 -4
View File
@@ -5,10 +5,7 @@ A tool to interactively (re-)encode videos using FFmpeg.
Uses Neutralino.js and Solid.js. Uses Neutralino.js and Solid.js.
This app _tries_ to imitate KDE's Kirigami UI framework, and also makes use of This app _tries_ to imitate KDE's Kirigami UI framework, and also makes use of
Breeze icons Breeze icons (Located in `./solid-src/public/breeze[-dark]`)
- `./solid-src/src/assets/breeze[-dark]`: Icons used by TSX files
- `./solid-src/public/breeze[-dark]`: Icons used by CSS files
Vencoder is tested with FFmpeg 7.1.1, should be compatible with older versions Vencoder is tested with FFmpeg 7.1.1, should be compatible with older versions
but is not guaranteed. but is not guaranteed.
+64 -1
View File
@@ -52,9 +52,19 @@ function App() {
RunningProcessInfo[] RunningProcessInfo[]
>([]); >([]);
const [customFileExt, setCustomFileExt] = createSignal(""); const [customFileExt, setCustomFileExt] = createSignal("");
const [globalopts, setGlobalopts] = createSignal("");
const [inputopts, setInputopts] = createSignal("");
const [outputopts, setOutputopts] = createSignal("");
const logs: { [id: number]: string[] } = {}; const logs: { [id: number]: string[] } = {};
let supportedCodecs: CodecInfo[] = []; let supportedCodecs: CodecInfo[] = [];
let ffmpegParams: FFmpegParams = { vcodec: "" }; let ffmpegParams: FFmpegParams = {
vcodec: "",
useropts: {
global: "",
input: "",
output: "",
},
};
let successfulCount = 0; let successfulCount = 0;
let unsuccessfulCount = 0; let unsuccessfulCount = 0;
let totalCount = 0; let totalCount = 0;
@@ -195,6 +205,11 @@ function App() {
ffmpegParams = { ffmpegParams = {
vcodec: codecObj?.shortName ?? "", vcodec: codecObj?.shortName ?? "",
useropts: {
global: "",
input: "",
output: "",
},
}; };
let encoder = newValue; let encoder = newValue;
@@ -242,6 +257,11 @@ function App() {
preset: ffmpegParams.preset, preset: ffmpegParams.preset,
twopass: ffmpegParams.twopass, twopass: ffmpegParams.twopass,
vbitrate: ffmpegParams.vbitrate, vbitrate: ffmpegParams.vbitrate,
useropts: {
global: globalopts(),
input: inputopts(),
output: outputopts(),
},
}; };
setOutputCommand(generateOutputCommand(ffmpegParams)); setOutputCommand(generateOutputCommand(ffmpegParams));
@@ -570,6 +590,49 @@ function App() {
/> />
</Match> </Match>
</Switch> </Switch>
<div class="row flex-col align-items-center">
<h3 class="k-form-section-title">
Extra Arguments
</h3>
</div>
<form class="k-form">
<label>Global Options</label>
<input
type="text"
name="globalopts"
id="globalopts"
value={globalopts()}
oninput={(e) => {
ffmpegParams.useropts.global =
e.target.value;
setGlobalopts(e.target.value);
}}
/>
<label>Input Options</label>
<input
type="text"
name="inputopts"
id="inputopts"
value={inputopts()}
oninput={(e) => {
ffmpegParams.useropts.input =
e.target.value;
setInputopts(e.target.value);
}}
/>
<label>Output Options</label>
<input
type="text"
name="outputopts"
id="outputopts"
value={outputopts()}
oninput={(e) => {
ffmpegParams.useropts.output =
e.target.value;
setOutputopts(e.target.value);
}}
/>
</form>
</div> </div>
<div class="row flex-col p-medium"> <div class="row flex-col p-medium">
<label for="outputCommand">Command</label> <label for="outputCommand">Command</label>
+22 -2
View File
@@ -1,4 +1,8 @@
import { type CodecInfo, type FFmpegParams } from "@/util/ffmpeg"; import {
DEFAULT_BITRATE,
type CodecInfo,
type FFmpegParams,
} from "@/util/ffmpeg";
import { os } from "@neutralinojs/lib"; import { os } from "@neutralinojs/lib";
import BreezeIcon from "@/components/BreezeIcon"; import BreezeIcon from "@/components/BreezeIcon";
import { onMount } from "solid-js"; import { onMount } from "solid-js";
@@ -10,7 +14,10 @@ function Librav1eOptions(props: {
}) { }) {
onMount(() => { onMount(() => {
props.onParamChanged("crf", undefined); props.onParamChanged("crf", undefined);
props.onParamChanged("vbitrate", undefined); props.onParamChanged(
"vbitrate",
props.params.vbitrate ?? DEFAULT_BITRATE,
);
props.onParamChanged("speed", 5); props.onParamChanged("speed", 5);
}); });
@@ -46,6 +53,19 @@ function Librav1eOptions(props: {
props.onParamChanged("speed", e.target.value) props.onParamChanged("speed", e.target.value)
} }
/> />
<label for="bitrate">Bitrate</label>
<div class="row gap2 align-items-center">
<input
type="number"
name="bitrate"
id="bitrate"
value={props.params.vbitrate ?? DEFAULT_BITRATE}
oninput={(e) =>
props.onParamChanged("vbitrate", e.target.value)
}
/>
<span>Kbps</span>
</div>
</div> </div>
</section> </section>
); );
+26 -6
View File
@@ -72,6 +72,12 @@ export const videoFileExtensions: { [key: string]: string } = {
vp9: "mkv", vp9: "mkv",
}; };
export interface ExtraFFmpegArguments {
global: string;
input: string;
output: string;
}
export interface FFmpegParams { export interface FFmpegParams {
inputFile?: string; inputFile?: string;
outputFile?: string; outputFile?: string;
@@ -93,6 +99,10 @@ export interface FFmpegParams {
faststart?: boolean; faststart?: boolean;
doNotUseAn?: boolean; doNotUseAn?: boolean;
speed?: number; speed?: number;
/**
* Extra parameters defined by users
*/
useropts: ExtraFFmpegArguments;
} }
const NULL_LOCATION = window.NL_OS === "Windows" ? "NUL" : "/dev/null"; const NULL_LOCATION = window.NL_OS === "Windows" ? "NUL" : "/dev/null";
@@ -109,24 +119,34 @@ export function generateOutputCommand(params: FFmpegParams) {
? " -movflags +faststart" ? " -movflags +faststart"
: ""; : "";
let globalopts = "-hwaccel auto -y";
let inputopts =
params.useropts.input !== "" ? " " + params.useropts.input : "";
let outputopts =
params.useropts.output !== "" ? " " + params.useropts.output : "";
if (params.useropts.global !== "") {
globalopts += " " + params.useropts.global;
}
if (params.twopass) { if (params.twopass) {
const commonOpts = `-i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec} -b:v ${ const commonOpts = `${globalopts}${inputopts} -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec} -b:v ${
params.vbitrate ?? DEFAULT_BITRATE params.vbitrate ?? DEFAULT_BITRATE
}k${faststart}${ }k${faststart}${
params.preset === undefined ? "" : ` -preset ${params.preset}` params.preset === undefined ? "" : ` -preset ${params.preset}`
} -progress -`; } -progress -${outputopts}`;
return `ffmpeg -hwaccel auto -y ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=1" : "-pass 1"} ${ return `ffmpeg ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=1" : "-pass 1"} ${
params.doNotUseAn ? "-vsync cfr" : "-an" params.doNotUseAn ? "-vsync cfr" : "-an"
} -f null ${NULL_LOCATION} && } -f null ${NULL_LOCATION} &&
ffmpeg -y -hwaccel auto ${commonOpts} ${ ffmpeg ${commonOpts} ${
params.vcodec === "hevc" ? "-x265-params pass=2" : "-pass 2" params.vcodec === "hevc" ? "-x265-params pass=2" : "-pass 2"
} -c:a ${ } -c:a ${
params.acodec ?? "copy" params.acodec ?? "copy"
}${params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`} "${params.outputFile ?? "{output}"}"`; }${params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`} "${params.outputFile ?? "{output}"}"`;
} }
return `ffmpeg -y -hwaccel auto -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec}${ return `ffmpeg ${globalopts}${inputopts} -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec}${
params.crf === undefined ? "" : ` -crf ${params.crf}` params.crf === undefined ? "" : ` -crf ${params.crf}`
}${ }${
params.vbitrate === undefined ? "" : ` -b:v ${params.vbitrate}` params.vbitrate === undefined ? "" : ` -b:v ${params.vbitrate}`
@@ -136,7 +156,7 @@ ffmpeg -y -hwaccel auto ${commonOpts} ${
params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k` params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`
}${ }${
params.speed === undefined ? "" : ` -speed ${params.speed}` params.speed === undefined ? "" : ` -speed ${params.speed}`
} -progress - "${params.outputFile ?? "{output}"}"`; } -progress -${outputopts} "${params.outputFile ?? "{output}"}"`;
} }
export async function getLengthMicroseconds(target: string) { export async function getLengthMicroseconds(target: string) {