+41
-2
@@ -26,6 +26,7 @@ import { getTemporaryFilePath } from "./util/path";
|
||||
import { generateRandomString } from "./util/string";
|
||||
import "./css/icons.css";
|
||||
import BreezeIcon from "./components/BreezeIcon";
|
||||
import AV1Options from "./components/AV1Options";
|
||||
|
||||
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
|
||||
|
||||
@@ -52,6 +53,7 @@ function App() {
|
||||
const [runningProcesses, setRunningProcesses] = createSignal<
|
||||
RunningProcessInfo[]
|
||||
>([]);
|
||||
const [customFileExt, setCustomFileExt] = createSignal("");
|
||||
const logs: { [id: number]: string[] } = {};
|
||||
let supportedCodecs: CodecInfo[] = [];
|
||||
let ffmpegParams: FFmpegParams = { vcodec: "" };
|
||||
@@ -119,6 +121,8 @@ function App() {
|
||||
|
||||
const firstCodec = displayedCodecs()[0];
|
||||
|
||||
ffmpegParams.vcodec = firstCodec.shortName;
|
||||
ffmpegParams.encoder = firstCodec.encoders[0];
|
||||
setSelectedCodec(firstCodec);
|
||||
setSelectedEncoder(firstCodec.encoders[0]);
|
||||
});
|
||||
@@ -191,11 +195,16 @@ function App() {
|
||||
ffmpegParams.twopass = false;
|
||||
}
|
||||
|
||||
setSelectedCodec(codecObj);
|
||||
ffmpegParams = {
|
||||
vcodec: codecObj?.shortName ?? "",
|
||||
};
|
||||
|
||||
let encoder = newValue;
|
||||
if (codecObj?.encoders.length !== 0) {
|
||||
encoder = codecObj?.encoders[0] ?? "";
|
||||
}
|
||||
ffmpegParams.encoder = encoder;
|
||||
setSelectedCodec(codecObj);
|
||||
setSelectedEncoder(encoder);
|
||||
}
|
||||
|
||||
@@ -247,8 +256,12 @@ function App() {
|
||||
|
||||
const fileName = (await Neutralino.filesystem.getPathParts(clip)).stem;
|
||||
|
||||
const customExt = customFileExt();
|
||||
|
||||
const fileExt =
|
||||
videoFileExtensions[selectedCodec()?.shortName ?? ""] ?? "";
|
||||
customExt === ""
|
||||
? videoFileExtensions[selectedCodec()?.shortName ?? ""]
|
||||
: customExt;
|
||||
|
||||
switch (window.NL_OS) {
|
||||
case "Linux":
|
||||
@@ -327,6 +340,7 @@ function App() {
|
||||
y: 120,
|
||||
injectGlobals: true,
|
||||
maximizable: false,
|
||||
enableInspector: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -363,6 +377,7 @@ function App() {
|
||||
y: 120,
|
||||
injectGlobals: true,
|
||||
maximizable: false,
|
||||
enableInspector: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -489,6 +504,18 @@ function App() {
|
||||
Only show common codecs
|
||||
</label>
|
||||
</div>
|
||||
<label for="fileExt">File Extension</label>
|
||||
<input
|
||||
type="text"
|
||||
name="fileExt"
|
||||
id="fileExt"
|
||||
title="File extension without the dot. Leave blank to guess from codec."
|
||||
value={customFileExt()}
|
||||
oninput={(e) =>
|
||||
setCustomFileExt(e.target.value)
|
||||
}
|
||||
placeholder="Leave blank to guess from codec"
|
||||
/>
|
||||
<Show
|
||||
when={
|
||||
selectedCodec()?.encoders.length !==
|
||||
@@ -532,6 +559,18 @@ function App() {
|
||||
onParamChanged={onParametersChanged}
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
selectedCodec()?.shortName === "av1"
|
||||
}
|
||||
>
|
||||
<AV1Options
|
||||
codec={selectedCodec()}
|
||||
encoder={selectedEncoder()}
|
||||
params={ffmpegParams}
|
||||
onParamChanged={onParametersChanged}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="row flex-col p-medium">
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Match, Switch } from "solid-js";
|
||||
import type { CodecInfo, FFmpegParams } from "@/util/ffmpeg";
|
||||
import LibaomOptions from "./encoders/libaom";
|
||||
import Librav1eOptions from "./encoders/librav1e";
|
||||
|
||||
function AV1Options(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
encoder: string;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
}) {
|
||||
return (
|
||||
<Switch fallback={<div>No options.</div>}>
|
||||
<Match when={props.encoder === "libaom-av1"}>
|
||||
<LibaomOptions {...props} />
|
||||
</Match>
|
||||
<Match when={props.encoder === "librav1e"}>
|
||||
<Librav1eOptions {...props} />
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export default AV1Options;
|
||||
@@ -1,5 +1,9 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import type { CodecInfo, FFmpegParams } from "../util/ffmpeg";
|
||||
import {
|
||||
DEFAULT_BITRATE,
|
||||
type CodecInfo,
|
||||
type FFmpegParams,
|
||||
} from "../util/ffmpeg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
import BreezeIcon from "./BreezeIcon";
|
||||
|
||||
@@ -110,13 +114,12 @@ function H264Options(props: {
|
||||
}
|
||||
>
|
||||
<label for="bitrate">Bitrate</label>
|
||||
{/* Using 12 Mbps (YouTube's recommended bitrate for high frame rate 1080p video) as an arbitrary value */}
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
name="bitrate"
|
||||
id="bitrate"
|
||||
value={props.params.vbitrate ?? 12000}
|
||||
value={props.params.vbitrate ?? DEFAULT_BITRATE}
|
||||
oninput={(e) => {
|
||||
props.params.vbitrate = parseInt(
|
||||
e.target.value,
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
DEFAULT_BITRATE,
|
||||
type CodecInfo,
|
||||
type FFmpegParams,
|
||||
} from "@/util/ffmpeg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
import BreezeIcon from "@/components/BreezeIcon";
|
||||
import { createEffect, createSignal, Show } from "solid-js";
|
||||
|
||||
const DEFAULT_CRF = 23;
|
||||
|
||||
function LibaomOptions(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
}) {
|
||||
const [rateControlMode, setRateControlMode] = createSignal("Constant");
|
||||
|
||||
createEffect(() => {
|
||||
const mode = rateControlMode();
|
||||
|
||||
props.onParamChanged("twopass", mode === "2PassABR");
|
||||
|
||||
switch (mode) {
|
||||
case "Constant":
|
||||
props.onParamChanged("crf", props.params.crf ?? DEFAULT_CRF);
|
||||
props.onParamChanged("vbitrate", undefined);
|
||||
break;
|
||||
case "Constrained":
|
||||
props.onParamChanged("crf", props.params.crf ?? DEFAULT_CRF);
|
||||
props.onParamChanged(
|
||||
"vbitrate",
|
||||
props.params.vbitrate ?? DEFAULT_BITRATE,
|
||||
);
|
||||
break;
|
||||
case "2PassABR":
|
||||
case "ABR":
|
||||
props.onParamChanged("crf", undefined);
|
||||
props.onParamChanged(
|
||||
"vbitrate",
|
||||
props.params.vbitrate ?? DEFAULT_BITRATE,
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<section id="encoderOptions">
|
||||
<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://trac.ffmpeg.org/wiki/Encode/AV1#libaom",
|
||||
)
|
||||
}
|
||||
title="Click to view the documentation for this encoder."
|
||||
>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
<label>Rate-control modes</label>
|
||||
<select
|
||||
class="k-dropdown"
|
||||
onchange={(e) => setRateControlMode(e.target.value)}
|
||||
>
|
||||
<option value="Constant">Constant Quality</option>
|
||||
<option value="Constrained">Constrained Quality</option>
|
||||
<option value="2PassABR">2-Pass Average Bitrate</option>
|
||||
<option value="ABR">1-Pass Average Bitrate</option>
|
||||
</select>
|
||||
<Show
|
||||
when={
|
||||
rateControlMode() === "Constant" ||
|
||||
rateControlMode() === "Constrained"
|
||||
}
|
||||
>
|
||||
<label>CRF</label>
|
||||
<input
|
||||
type="number"
|
||||
name="crf"
|
||||
id="crf"
|
||||
min="1"
|
||||
max="63"
|
||||
value={props.params.crf ?? DEFAULT_CRF}
|
||||
oninput={(e) => {
|
||||
props.onParamChanged(
|
||||
"crf",
|
||||
parseInt(e.target.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={rateControlMode() !== "Constant"}>
|
||||
<label>Bitrate</label>
|
||||
<div class="row gap2">
|
||||
<input
|
||||
type="number"
|
||||
name="bitrate"
|
||||
id="bitrate"
|
||||
value={props.params.vbitrate ?? DEFAULT_BITRATE}
|
||||
oninput={(e) => {
|
||||
props.onParamChanged(
|
||||
"vbitrate",
|
||||
parseInt(e.target.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span>Kbps</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default LibaomOptions;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { type CodecInfo, type FFmpegParams } from "@/util/ffmpeg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
import BreezeIcon from "@/components/BreezeIcon";
|
||||
import { onMount } from "solid-js";
|
||||
|
||||
function Librav1eOptions(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
}) {
|
||||
onMount(() => {
|
||||
props.onParamChanged("crf", undefined);
|
||||
props.onParamChanged("vbitrate", undefined);
|
||||
props.onParamChanged("speed", 5);
|
||||
});
|
||||
|
||||
return (
|
||||
<section id="encoderOptions">
|
||||
<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://www.ffmpeg.org/ffmpeg-all.html#librav1e",
|
||||
)
|
||||
}
|
||||
title="Click to view the documentation for this encoder."
|
||||
>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
<label>Speed</label>
|
||||
<input
|
||||
type="number"
|
||||
name="speed"
|
||||
id="speed"
|
||||
min="0"
|
||||
max="10"
|
||||
value={props.params.speed ?? 5}
|
||||
oninput={(e) =>
|
||||
props.onParamChanged("speed", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default Librav1eOptions;
|
||||
@@ -67,9 +67,9 @@ export const videoFileExtensions: { [key: string]: string } = {
|
||||
dnxhd: "mov",
|
||||
h264: "mp4",
|
||||
hevc: "mp4",
|
||||
av1: "webm",
|
||||
vp8: "webm",
|
||||
vp9: "webm",
|
||||
av1: "mkv",
|
||||
vp8: "mkv",
|
||||
vp9: "mkv",
|
||||
};
|
||||
|
||||
export interface FFmpegParams {
|
||||
@@ -92,10 +92,17 @@ export interface FFmpegParams {
|
||||
preset?: string;
|
||||
faststart?: boolean;
|
||||
doNotUseAn?: boolean;
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
const NULL_LOCATION = window.NL_OS === "Windows" ? "NUL" : "/dev/null";
|
||||
|
||||
/**
|
||||
* Using 12 Mbps (YouTube's recommended bitrate for high frame rate 1080p
|
||||
* video) as an arbitrary value
|
||||
*/
|
||||
export const DEFAULT_BITRATE = 12000;
|
||||
|
||||
export function generateOutputCommand(params: FFmpegParams) {
|
||||
let faststart =
|
||||
params.faststart && params.vcodec === "h264"
|
||||
@@ -104,16 +111,16 @@ export function generateOutputCommand(params: FFmpegParams) {
|
||||
|
||||
if (params.twopass) {
|
||||
const commonOpts = `-i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec} -b:v ${
|
||||
params.vbitrate ?? 12000
|
||||
params.vbitrate ?? DEFAULT_BITRATE
|
||||
}k${faststart}${
|
||||
params.preset === undefined ? "" : ` -preset ${params.preset}`
|
||||
} -progress -`;
|
||||
|
||||
return `ffmpeg -hwaccel auto -y ${commonOpts} ${params.vcodec === "h264" ? "-pass 1" : "-x265-params pass=1"} ${
|
||||
return `ffmpeg -hwaccel auto -y ${commonOpts} ${params.vcodec === "h265" ? "-x265-params pass=1" : "-pass 1"} ${
|
||||
params.doNotUseAn ? "-vsync cfr" : "-an"
|
||||
} -f null ${NULL_LOCATION} &&
|
||||
ffmpeg -y -hwaccel auto ${commonOpts} ${
|
||||
params.vcodec === "h264" ? "-pass 2" : "-x265-params pass=2"
|
||||
params.vcodec === "h265" ? "-x265-params pass=2" : "-pass 2"
|
||||
} -c:a ${
|
||||
params.acodec ?? "copy"
|
||||
}${params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`} "${params.outputFile ?? "{output}"}"`;
|
||||
@@ -121,10 +128,14 @@ ffmpeg -y -hwaccel auto ${commonOpts} ${
|
||||
|
||||
return `ffmpeg -y -hwaccel auto -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec}${
|
||||
params.crf === undefined ? "" : ` -crf ${params.crf}`
|
||||
}${
|
||||
params.vbitrate === undefined ? "" : ` -b:v ${params.vbitrate}`
|
||||
}${faststart}${
|
||||
params.preset === undefined ? "" : ` -preset ${params.preset}`
|
||||
} -c:a ${params.acodec ?? "copy"}${
|
||||
params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`
|
||||
}${
|
||||
params.speed === undefined ? "" : ` -speed ${params.speed}`
|
||||
} -progress - "${params.outputFile ?? "{output}"}"`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user