Compare commits

...

2 Commits

Author SHA1 Message Date
linesofcodes 4cad43abe1 DNxHD support, Better screen reader support
Build / build (push) Successful in 1m12s
2025-08-19 13:02:39 +07:00
linesofcodes 3921c003bb Allows defining custom arguments 2025-08-19 10:47:08 +07:00
7 changed files with 211 additions and 20 deletions
+2 -5
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.
@@ -56,7 +53,7 @@ encoders supported by your FFmpeg install will show up.
- [ ] av1_nvenc - [ ] av1_nvenc
- [ ] av1_qsv - [ ] av1_qsv
- [ ] av1_vaapi - [ ] av1_vaapi
- [ ] DNxHD - [x] DNxHD (Does not provide options to deal with its pickiness yet)
- [ ] H.264 - [ ] H.264
- [x] libx264 - [x] libx264
- [x] libx264rgb (Untested, but _should_ work) - [x] libx264rgb (Untested, but _should_ work)
+87 -2
View File
@@ -27,6 +27,7 @@ import { generateRandomString } from "./util/string";
import "./css/icons.css"; import "./css/icons.css";
import BreezeIcon from "./components/BreezeIcon"; import BreezeIcon from "./components/BreezeIcon";
import AV1Options from "./components/AV1Options"; import AV1Options from "./components/AV1Options";
import DNxHDOptions from "./components/DNxHDOptions";
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]); const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
@@ -52,9 +53,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 +206,11 @@ function App() {
ffmpegParams = { ffmpegParams = {
vcodec: codecObj?.shortName ?? "", vcodec: codecObj?.shortName ?? "",
useropts: {
global: "",
input: "",
output: "",
},
}; };
let encoder = newValue; let encoder = newValue;
@@ -242,6 +258,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));
@@ -520,7 +541,9 @@ function App() {
0 0
} }
> >
<label>Encoder</label> <label for="videoEncoder">
Encoder
</label>
<select <select
name="videoEncoder" name="videoEncoder"
id="videoEncoder" id="videoEncoder"
@@ -569,7 +592,69 @@ function App() {
onParamChanged={onParametersChanged} onParamChanged={onParametersChanged}
/> />
</Match> </Match>
<Match
when={
selectedCodec()?.shortName ===
"dnxhd"
}
>
<DNxHDOptions
codec={selectedCodec()}
params={ffmpegParams}
onParamChanged={onParametersChanged}
/>
</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"
onsubmit={(e) => e.preventDefault()}
>
<label for="globalopts">
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 for="inputopts">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 for="outputopts">
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>
+55
View File
@@ -0,0 +1,55 @@
import { os } from "@neutralinojs/lib";
import { type CodecInfo, type FFmpegParams } from "../util/ffmpeg";
import BreezeIcon from "./BreezeIcon";
/**
* Options for H.264/H.265 codecs
*/
function DNxHDOptions(props: {
codec: CodecInfo | undefined;
params: FFmpegParams;
onParamChanged: (key: string, value: any) => void;
}) {
return (
<section id="commonLossyOptions">
<div class="row flex-col align-items-center">
<h3 class="k-form-section-title">Encoder Options</h3>
</div>
<div class="k-form">
<label>Help</label>
<div>
<button
class="icon-button"
onclick={() =>
os.open("https://askubuntu.com/a/907515")
}
title="DNxHD is a picky encoder."
>
<BreezeIcon icon="help-about" alt="Help" />
</button>
</div>
<label for="profile">Profile</label>
<select
class="k-dropdown"
name="profile"
id="profile"
value={props.params.outputopts?.profile ?? "dnxhd"}
oninput={(e) => {
props.onParamChanged("outputopts", {
profile: e.target.value,
});
}}
>
<option value="dnxhd">DNxHD</option>
<option value="dnxhr_444">DNxHR 444</option>
<option value="dnxhr_hqx">DNxHR HQX</option>
<option value="dnxhr_hq">DNxHR HQ</option>
<option value="dnxhr_sq">DNxHR SQ</option>
<option value="dnxhr_lb">DNxHR LB</option>
</select>
</div>
</section>
);
}
export default DNxHDOptions;
+1 -1
View File
@@ -63,7 +63,7 @@ function H264Options(props: {
<BreezeIcon icon="help-about" alt="Help" /> <BreezeIcon icon="help-about" alt="Help" />
</button> </button>
</div> </div>
<label>Preset</label> <label for="encodingPreset">Preset</label>
<select <select
class="k-dropdown" class="k-dropdown"
name="encodingPreset" name="encodingPreset"
+7 -4
View File
@@ -64,10 +64,12 @@ function LibaomOptions(props: {
<BreezeIcon icon="help-about" alt="Help" /> <BreezeIcon icon="help-about" alt="Help" />
</button> </button>
</div> </div>
<label>Rate-control modes</label> <label for="rateControlMode">Rate-control modes</label>
<select <select
class="k-dropdown" class="k-dropdown"
onchange={(e) => setRateControlMode(e.target.value)} onchange={(e) => setRateControlMode(e.target.value)}
name="rateControlMode"
id="rateControlMode"
> >
<option value="Constant">Constant Quality</option> <option value="Constant">Constant Quality</option>
<option value="Constrained">Constrained Quality</option> <option value="Constrained">Constrained Quality</option>
@@ -80,7 +82,7 @@ function LibaomOptions(props: {
rateControlMode() === "Constrained" rateControlMode() === "Constrained"
} }
> >
<label>CRF</label> <label for="crf">CRF</label>
<input <input
type="number" type="number"
name="crf" name="crf"
@@ -97,12 +99,13 @@ function LibaomOptions(props: {
/> />
</Show> </Show>
<Show when={rateControlMode() !== "Constant"}> <Show when={rateControlMode() !== "Constant"}>
<label>Bitrate</label> <label for="bitrate">Bitrate</label>
<div class="row gap2"> <div class="row gap2 align-items-center">
<input <input
type="number" type="number"
name="bitrate" name="bitrate"
id="bitrate" id="bitrate"
aria-label="Kbps"
value={props.params.vbitrate ?? DEFAULT_BITRATE} value={props.params.vbitrate ?? DEFAULT_BITRATE}
oninput={(e) => { oninput={(e) => {
props.onParamChanged( props.onParamChanged(
+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>
); );
+37 -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,14 @@ export interface FFmpegParams {
faststart?: boolean; faststart?: boolean;
doNotUseAn?: boolean; doNotUseAn?: boolean;
speed?: number; speed?: number;
/**
* Extra parameters defined by users
*/
useropts: ExtraFFmpegArguments;
/**
* Extra output parameters defined by Vencoder
*/
outputopts?: { [key: string]: string };
} }
const NULL_LOCATION = window.NL_OS === "Windows" ? "NUL" : "/dev/null"; const NULL_LOCATION = window.NL_OS === "Windows" ? "NUL" : "/dev/null";
@@ -109,24 +123,41 @@ 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.outputopts !== undefined) {
console.log(params.outputopts);
for (const key of Object.keys(params.outputopts)) {
outputopts += ` -${key} ${params.outputopts[key]}`.trimEnd();
}
}
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 +167,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) {