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.
This app _tries_ to imitate KDE's Kirigami UI framework, and also makes use of
Breeze icons
- `./solid-src/src/assets/breeze[-dark]`: Icons used by TSX files
- `./solid-src/public/breeze[-dark]`: Icons used by CSS files
Breeze icons (Located in `./solid-src/public/breeze[-dark]`)
Vencoder is tested with FFmpeg 7.1.1, should be compatible with older versions
but is not guaranteed.
@@ -56,7 +53,7 @@ encoders supported by your FFmpeg install will show up.
- [ ] av1_nvenc
- [ ] av1_qsv
- [ ] av1_vaapi
- [ ] DNxHD
- [x] DNxHD (Does not provide options to deal with its pickiness yet)
- [ ] H.264
- [x] libx264
- [x] libx264rgb (Untested, but _should_ work)
+87 -2
View File
@@ -27,6 +27,7 @@ import { generateRandomString } from "./util/string";
import "./css/icons.css";
import BreezeIcon from "./components/BreezeIcon";
import AV1Options from "./components/AV1Options";
import DNxHDOptions from "./components/DNxHDOptions";
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
@@ -52,9 +53,19 @@ function App() {
RunningProcessInfo[]
>([]);
const [customFileExt, setCustomFileExt] = createSignal("");
const [globalopts, setGlobalopts] = createSignal("");
const [inputopts, setInputopts] = createSignal("");
const [outputopts, setOutputopts] = createSignal("");
const logs: { [id: number]: string[] } = {};
let supportedCodecs: CodecInfo[] = [];
let ffmpegParams: FFmpegParams = { vcodec: "" };
let ffmpegParams: FFmpegParams = {
vcodec: "",
useropts: {
global: "",
input: "",
output: "",
},
};
let successfulCount = 0;
let unsuccessfulCount = 0;
let totalCount = 0;
@@ -195,6 +206,11 @@ function App() {
ffmpegParams = {
vcodec: codecObj?.shortName ?? "",
useropts: {
global: "",
input: "",
output: "",
},
};
let encoder = newValue;
@@ -242,6 +258,11 @@ function App() {
preset: ffmpegParams.preset,
twopass: ffmpegParams.twopass,
vbitrate: ffmpegParams.vbitrate,
useropts: {
global: globalopts(),
input: inputopts(),
output: outputopts(),
},
};
setOutputCommand(generateOutputCommand(ffmpegParams));
@@ -520,7 +541,9 @@ function App() {
0
}
>
<label>Encoder</label>
<label for="videoEncoder">
Encoder
</label>
<select
name="videoEncoder"
id="videoEncoder"
@@ -569,7 +592,69 @@ function App() {
onParamChanged={onParametersChanged}
/>
</Match>
<Match
when={
selectedCodec()?.shortName ===
"dnxhd"
}
>
<DNxHDOptions
codec={selectedCodec()}
params={ffmpegParams}
onParamChanged={onParametersChanged}
/>
</Match>
</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 class="row flex-col p-medium">
<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" />
</button>
</div>
<label>Preset</label>
<label for="encodingPreset">Preset</label>
<select
class="k-dropdown"
name="encodingPreset"
+7 -4
View File
@@ -64,10 +64,12 @@ function LibaomOptions(props: {
<BreezeIcon icon="help-about" alt="Help" />
</button>
</div>
<label>Rate-control modes</label>
<label for="rateControlMode">Rate-control modes</label>
<select
class="k-dropdown"
onchange={(e) => setRateControlMode(e.target.value)}
name="rateControlMode"
id="rateControlMode"
>
<option value="Constant">Constant Quality</option>
<option value="Constrained">Constrained Quality</option>
@@ -80,7 +82,7 @@ function LibaomOptions(props: {
rateControlMode() === "Constrained"
}
>
<label>CRF</label>
<label for="crf">CRF</label>
<input
type="number"
name="crf"
@@ -97,12 +99,13 @@ function LibaomOptions(props: {
/>
</Show>
<Show when={rateControlMode() !== "Constant"}>
<label>Bitrate</label>
<div class="row gap2">
<label for="bitrate">Bitrate</label>
<div class="row gap2 align-items-center">
<input
type="number"
name="bitrate"
id="bitrate"
aria-label="Kbps"
value={props.params.vbitrate ?? DEFAULT_BITRATE}
oninput={(e) => {
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 BreezeIcon from "@/components/BreezeIcon";
import { onMount } from "solid-js";
@@ -10,7 +14,10 @@ function Librav1eOptions(props: {
}) {
onMount(() => {
props.onParamChanged("crf", undefined);
props.onParamChanged("vbitrate", undefined);
props.onParamChanged(
"vbitrate",
props.params.vbitrate ?? DEFAULT_BITRATE,
);
props.onParamChanged("speed", 5);
});
@@ -46,6 +53,19 @@ function Librav1eOptions(props: {
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>
</section>
);
+37 -6
View File
@@ -72,6 +72,12 @@ export const videoFileExtensions: { [key: string]: string } = {
vp9: "mkv",
};
export interface ExtraFFmpegArguments {
global: string;
input: string;
output: string;
}
export interface FFmpegParams {
inputFile?: string;
outputFile?: string;
@@ -93,6 +99,14 @@ export interface FFmpegParams {
faststart?: boolean;
doNotUseAn?: boolean;
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";
@@ -109,24 +123,41 @@ export function generateOutputCommand(params: FFmpegParams) {
? " -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) {
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
}k${faststart}${
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"
} -f null ${NULL_LOCATION} &&
ffmpeg -y -hwaccel auto ${commonOpts} ${
ffmpeg ${commonOpts} ${
params.vcodec === "hevc" ? "-x265-params pass=2" : "-pass 2"
} -c:a ${
params.acodec ?? "copy"
}${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.vbitrate === undefined ? "" : ` -b:v ${params.vbitrate}`
@@ -136,7 +167,7 @@ ffmpeg -y -hwaccel auto ${commonOpts} ${
params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`
}${
params.speed === undefined ? "" : ` -speed ${params.speed}`
} -progress - "${params.outputFile ?? "{output}"}"`;
} -progress -${outputopts} "${params.outputFile ?? "{output}"}"`;
}
export async function getLengthMicroseconds(target: string) {