Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4cad43abe1
|
|||
|
3921c003bb
|
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user