Compare commits
13 Commits
4cad43abe1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
8660bc9967
|
|||
|
86847cbeca
|
|||
|
3be7cd2407
|
|||
|
64cfdce699
|
|||
|
a949e5d46b
|
|||
|
958ecdca7d
|
|||
|
f1da312b95
|
|||
|
37568aa0d1
|
|||
|
466a7cedca
|
|||
|
77e91fde1c
|
|||
|
74cebbf180
|
|||
|
68d919ab1e
|
|||
|
d586a8f222
|
@@ -29,8 +29,8 @@ jobs:
|
||||
- name: Package application
|
||||
run: |
|
||||
cd ${{ github.workspace }}
|
||||
wget https://staticlines.dailitation.xyz/neutralinojs-v6.2.0.zip
|
||||
unzip neutralinojs-v6.2.0.zip -d bin/
|
||||
wget https://staticlines.dailitation.xyz/neutralinojs-v6.3.0.zip
|
||||
unzip neutralinojs-v6.3.0.zip -d bin/
|
||||
pnpx @neutralinojs/neu build
|
||||
- name: Upload artifacts
|
||||
uses: ChristopherHX/gitea-upload-artifact@v4
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
frontend:
|
||||
cd solid-src; \
|
||||
pnpm build
|
||||
|
||||
build: frontend
|
||||
neu build
|
||||
|
||||
release: frontend
|
||||
neu build --clean -r --embed-resources
|
||||
|
||||
@@ -48,7 +48,7 @@ encoders supported by your FFmpeg install will show up.
|
||||
- [ ] AV1
|
||||
- [x] libaom-av1
|
||||
- [x] librav1e (Partial support)
|
||||
- [ ] libsvtav1
|
||||
- [x] libsvtav1
|
||||
- [ ] av1_amf
|
||||
- [ ] av1_nvenc
|
||||
- [ ] av1_qsv
|
||||
@@ -59,26 +59,20 @@ encoders supported by your FFmpeg install will show up.
|
||||
- [x] libx264rgb (Untested, but _should_ work)
|
||||
- [ ] h264_amf
|
||||
- [ ] h264_nvenc
|
||||
- [ ] h264_qsv
|
||||
- [ ] h264_v4l2m2m
|
||||
- [x] h264_qsv
|
||||
- [ ] h264_vaapi
|
||||
- [ ] h264_vulkan
|
||||
- [ ] H.265
|
||||
- [x] libx265
|
||||
- [ ] h264_amf
|
||||
- [ ] h264_nvenc
|
||||
- [ ] h264_qsv
|
||||
- [ ] h264_v4l2m2m
|
||||
- [ ] h264_vaapi
|
||||
- [ ] h264_vulkan
|
||||
- [ ] VP8
|
||||
- [ ] libvpx
|
||||
- [ ] vp8_v4l2m2m
|
||||
- [ ] vp8_vaapi
|
||||
- [ ] h265_amf
|
||||
- [ ] h265_nvenc
|
||||
- [x] h265_qsv
|
||||
- [ ] h265_vaapi
|
||||
- [ ] h265_vulkan
|
||||
- [ ] VP9
|
||||
- [ ] libvpx-vp9
|
||||
- [ ] vp9_vaapi
|
||||
- [ ] vp9_qsv
|
||||
- [x] vp9_qsv (Really Basic)
|
||||
|
||||
## Gitea Actions
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env xdg-open
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.5
|
||||
Name=Vencoder
|
||||
Exec=/usr/bin/vencoder
|
||||
Terminal=false
|
||||
+13
-6
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
|
||||
"applicationId": "xyz.dailitation.linesofcodes.vencoder",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.1",
|
||||
"defaultMode": "window",
|
||||
"documentRoot": "/solid-src/dist/",
|
||||
"url": "/",
|
||||
"port": 5432,
|
||||
"enableServer": true,
|
||||
"enableNativeAPI": true,
|
||||
"singlePageServe": true,
|
||||
@@ -17,23 +18,29 @@
|
||||
"storage.*",
|
||||
"debug.log"
|
||||
],
|
||||
"dataLocation": "system",
|
||||
"storageLocation": "system",
|
||||
"logging": {
|
||||
"writeToLogFile": false
|
||||
},
|
||||
"modes": {
|
||||
"window": {
|
||||
"title": "Vencoder",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"minWidth": 600,
|
||||
"minHeight": 400,
|
||||
"icon": "/solid-src/public/vite.svg",
|
||||
"enableInspector": true
|
||||
"enableInspector": true,
|
||||
"openInspectorOnStartup": false
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"binaryName": "vencoder",
|
||||
"resourcesPath": "/solid-src/dist/",
|
||||
"extensionsPath": "/extensions/",
|
||||
"binaryVersion": "6.2.0",
|
||||
"clientVersion": "6.2.0",
|
||||
"binaryVersion": "6.3.0",
|
||||
"clientVersion": "6.3.0",
|
||||
"frontendLibrary": {
|
||||
"patchFile": "/solid-src/index.html",
|
||||
"devUrl": "http://localhost:5173"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="http://localhost:35731/__neutralino_globals.js"></script>
|
||||
<script src="http://localhost:5432/__neutralino_globals.js"></script>
|
||||
<title>Vencoder</title>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -9,17 +9,19 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@neutralinojs/lib": "^6.2.0",
|
||||
"@neutralinojs/lib": "^6.3.0",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"color-convert": "^3.1.0",
|
||||
"solid-js": "^1.9.9"
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"color-convert": "^3.1.2",
|
||||
"solid-js": "^1.9.9",
|
||||
"tailwindcss": "^4.1.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/node": "^24.6.2",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.2",
|
||||
"vite-plugin-solid": "^2.11.8"
|
||||
"vite": "^7.1.9",
|
||||
"vite-plugin-solid": "^2.11.9"
|
||||
},
|
||||
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
|
||||
"packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a"
|
||||
}
|
||||
|
||||
Generated
+728
-333
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,3 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@tailwindcss/oxide'
|
||||
- esbuild
|
||||
|
||||
+361
-380
@@ -14,25 +14,25 @@ import {
|
||||
generateOutputCommand,
|
||||
getAvailableCodecs,
|
||||
getLengthMicroseconds,
|
||||
getPixelFormats,
|
||||
playFile,
|
||||
videoFileExtensions,
|
||||
type CodecInfo,
|
||||
type CodecList,
|
||||
type FFmpegParams,
|
||||
} from "./util/ffmpeg";
|
||||
import Neutralino from "@neutralinojs/lib";
|
||||
import H264Options from "./components/H264Options";
|
||||
import { openFile } from "./util/oshelper";
|
||||
import { getTemporaryFilePath } from "./util/path";
|
||||
import { generateRandomString } from "./util/string";
|
||||
import { getVencoderFolder } from "./util/path";
|
||||
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"]);
|
||||
const commonCodecs = new Set(["h264", "hevc", "vp9", "av1", "dnxhd"]);
|
||||
|
||||
interface RunningProcessInfo {
|
||||
process: Neutralino.os.SpawnedProcess;
|
||||
interface FileQueueItem {
|
||||
command: string;
|
||||
file: string;
|
||||
length: number;
|
||||
}
|
||||
@@ -41,6 +41,7 @@ function App() {
|
||||
const [windowFocused, setWindowFocused] = createSignal(true);
|
||||
const [displayedCodecs, setDisplayedCodecs]: Signal<CodecInfo[]> =
|
||||
createSignal([] as CodecInfo[]);
|
||||
const [audioCodecList, setAudioCodecList] = createSignal([] as CodecInfo[]);
|
||||
const [fileList, setFileList] = createSignal([] as string[]);
|
||||
const [selectedClip, setSelectedClip] = createSignal("");
|
||||
const [outputCommand, setOutputCommand] = createSignal(
|
||||
@@ -49,15 +50,15 @@ function App() {
|
||||
const [showCommonCodecs, setShowCommonCodecs] = createSignal(true);
|
||||
const [selectedCodec, setSelectedCodec] = createSignal<CodecInfo>();
|
||||
const [selectedEncoder, setSelectedEncoder] = createSignal("");
|
||||
const [runningProcesses, setRunningProcesses] = createSignal<
|
||||
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[] = [];
|
||||
const [audioCodec, setAudioCodec] = createSignal("copy");
|
||||
const [audioEncoder, setAudioEncoder] = createSignal("");
|
||||
const [pixelFormatList, setPixelFormatList] = createSignal([] as string[]);
|
||||
const [pixelFormat, setPixelFormat] = createSignal("");
|
||||
let supportedCodecs: CodecList = { vcodecs: [], acodecs: [] };
|
||||
let ffmpegParams: FFmpegParams = {
|
||||
vcodec: "",
|
||||
useropts: {
|
||||
@@ -66,9 +67,6 @@ function App() {
|
||||
output: "",
|
||||
},
|
||||
};
|
||||
let successfulCount = 0;
|
||||
let unsuccessfulCount = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
function windowIsFocused() {
|
||||
setWindowFocused(true);
|
||||
@@ -78,55 +76,13 @@ function App() {
|
||||
setWindowFocused(false);
|
||||
}
|
||||
|
||||
function handleSpawnedProcessEvents(evt: CustomEvent) {
|
||||
switch (evt.detail.action) {
|
||||
case "stdErr":
|
||||
logs[evt.detail.id].push(evt.detail.data);
|
||||
break;
|
||||
case "exit":
|
||||
if (evt.detail.data === 0) {
|
||||
successfulCount += 1;
|
||||
} else {
|
||||
unsuccessfulCount += 1;
|
||||
|
||||
// If the exit code isn't 255 (the exit code of the program exiting because of cancellation)
|
||||
if (evt.detail.data !== 255) {
|
||||
Neutralino.os.showNotification(
|
||||
"File Encoding Failed",
|
||||
`Encoding for file "${runningProcesses()?.find((v) => v.process.id == evt.detail.id)?.file}" failed. Exit code ${evt.detail.data}.`,
|
||||
);
|
||||
|
||||
const tempFilename = `${getTemporaryFilePath()}/vencoder-ffmpeg-${generateRandomString(8)}.log`;
|
||||
Neutralino.filesystem.writeFile(
|
||||
tempFilename,
|
||||
logs[evt.detail.id].join("\n"),
|
||||
);
|
||||
openFile(tempFilename);
|
||||
}
|
||||
}
|
||||
|
||||
if (successfulCount + unsuccessfulCount === totalCount) {
|
||||
Neutralino.os.showNotification(
|
||||
"File(s) encoded.",
|
||||
`${successfulCount} files encoded successfully. ${unsuccessfulCount} failed or cancelled.`,
|
||||
);
|
||||
successfulCount = 0;
|
||||
unsuccessfulCount = 0;
|
||||
totalCount = 0;
|
||||
}
|
||||
|
||||
console.log(`FFmpeg exited with code: ${evt.detail.data}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
events.on("windowFocus", windowIsFocused);
|
||||
events.on("windowBlur", windowUnfocused);
|
||||
events.on("spawnedProcess", handleSpawnedProcessEvents);
|
||||
|
||||
supportedCodecs = await getAvailableCodecs();
|
||||
filterDisplayedCodecs();
|
||||
setAudioCodecList(supportedCodecs.acodecs);
|
||||
|
||||
const firstCodec = displayedCodecs()[0];
|
||||
|
||||
@@ -134,12 +90,13 @@ function App() {
|
||||
ffmpegParams.encoder = firstCodec.encoders[0];
|
||||
setSelectedCodec(firstCodec);
|
||||
setSelectedEncoder(firstCodec.encoders[0]);
|
||||
|
||||
setPixelFormatList(await getPixelFormats());
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
events.off("windowFocus", windowIsFocused);
|
||||
events.off("windowBlur", windowUnfocused);
|
||||
events.off("spawnedProcess", handleSpawnedProcessEvents);
|
||||
});
|
||||
|
||||
function removeBtnClicked() {
|
||||
@@ -180,12 +137,14 @@ function App() {
|
||||
function filterDisplayedCodecs() {
|
||||
if (showCommonCodecs()) {
|
||||
setDisplayedCodecs(
|
||||
supportedCodecs.filter((v) => commonCodecs.has(v.shortName)),
|
||||
supportedCodecs.vcodecs.filter((v) =>
|
||||
commonCodecs.has(v.shortName),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setDisplayedCodecs(supportedCodecs);
|
||||
setDisplayedCodecs(supportedCodecs.vcodecs);
|
||||
}
|
||||
|
||||
function showCommonCodecsChanged(e: InputEvent) {
|
||||
@@ -200,30 +159,47 @@ function App() {
|
||||
(v) => v.shortName === newValue,
|
||||
);
|
||||
|
||||
if (newValue !== "h264" && newValue !== "hevc") {
|
||||
ffmpegParams.twopass = false;
|
||||
let encoder = newValue;
|
||||
if (codecObj?.encoders.length !== 0) {
|
||||
encoder = codecObj?.encoders[0] ?? "";
|
||||
}
|
||||
setSelectedCodec(codecObj);
|
||||
setSelectedEncoder(encoder);
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
ffmpegParams = {
|
||||
vcodec: codecObj?.shortName ?? "",
|
||||
vcodec: selectedCodec()?.shortName ?? "",
|
||||
encoder: selectedEncoder(),
|
||||
useropts: {
|
||||
global: "",
|
||||
input: "",
|
||||
output: "",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
let encoder = newValue;
|
||||
if (codecObj?.encoders.length !== 0) {
|
||||
encoder = codecObj?.encoders[0] ?? "";
|
||||
function getAudioEncoders() {
|
||||
const codec = audioCodec();
|
||||
let encoders = audioCodecList().find(
|
||||
(v) => v.shortName === codec,
|
||||
)?.encoders;
|
||||
|
||||
if (encoders) {
|
||||
setAudioEncoder(encoders[0]);
|
||||
}
|
||||
ffmpegParams.encoder = encoder;
|
||||
setSelectedCodec(codecObj);
|
||||
setSelectedEncoder(encoder);
|
||||
|
||||
if (encoders instanceof Array && encoders.length === 0) {
|
||||
encoders = undefined;
|
||||
}
|
||||
|
||||
return encoders;
|
||||
}
|
||||
|
||||
function onParametersChanged(key: string, value: any) {
|
||||
// @ts-ignore
|
||||
function onParametersChanged<K extends keyof FFmpegParams>(
|
||||
key: K,
|
||||
value: any,
|
||||
) {
|
||||
ffmpegParams[key] = value;
|
||||
setOutputCommand(generateOutputCommand(ffmpegParams));
|
||||
}
|
||||
@@ -235,6 +211,7 @@ function App() {
|
||||
x: 120,
|
||||
y: 120,
|
||||
injectGlobals: true,
|
||||
processArgs: "--port=5434",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -245,10 +222,18 @@ function App() {
|
||||
encoder = undefined;
|
||||
}
|
||||
|
||||
let acodec = audioEncoder();
|
||||
|
||||
if (acodec === undefined || acodec === "") {
|
||||
acodec = audioCodec();
|
||||
}
|
||||
|
||||
const pixFmt = pixelFormat();
|
||||
|
||||
ffmpegParams = {
|
||||
vcodec: selectedCodec()?.shortName ?? "",
|
||||
encoder,
|
||||
acodec: ffmpegParams.acodec,
|
||||
acodec,
|
||||
abitrate: ffmpegParams.abitrate,
|
||||
crf: ffmpegParams.crf,
|
||||
doNotUseAn: ffmpegParams.doNotUseAn,
|
||||
@@ -263,6 +248,7 @@ function App() {
|
||||
input: inputopts(),
|
||||
output: outputopts(),
|
||||
},
|
||||
pixelFormat: pixFmt === "" ? undefined : pixFmt,
|
||||
};
|
||||
|
||||
setOutputCommand(generateOutputCommand(ffmpegParams));
|
||||
@@ -270,7 +256,7 @@ function App() {
|
||||
|
||||
async function convertClip(
|
||||
clip: string,
|
||||
): Promise<RunningProcessInfo | undefined> {
|
||||
): Promise<FileQueueItem | undefined> {
|
||||
ffmpegParams.inputFile = clip;
|
||||
|
||||
const fileName = (await Neutralino.filesystem.getPathParts(clip)).stem;
|
||||
@@ -282,14 +268,7 @@ function App() {
|
||||
? videoFileExtensions[selectedCodec()?.shortName ?? ""]
|
||||
: customExt;
|
||||
|
||||
switch (window.NL_OS) {
|
||||
case "Linux":
|
||||
ffmpegParams.outputFile = `${await Neutralino.os.getEnv("HOME")}/Vencoder/${fileName}.${fileExt}`;
|
||||
break;
|
||||
case "Windows":
|
||||
ffmpegParams.outputFile = `${await Neutralino.os.getEnv("HOMEPATH")}\\Vencoder\\${fileName}.${fileExt}`;
|
||||
break;
|
||||
}
|
||||
ffmpegParams.outputFile = `${await getVencoderFolder()}${fileName}.${fileExt}`;
|
||||
|
||||
const outputDir = (
|
||||
await Neutralino.filesystem.getPathParts(
|
||||
@@ -308,8 +287,8 @@ function App() {
|
||||
const userAnswer = await Neutralino.os.showMessageBox(
|
||||
"File already exists",
|
||||
`A file at ${ffmpegParams.outputFile} already exists. Would you like to overwrite it?`,
|
||||
Neutralino.os.MessageBoxChoice.YES_NO,
|
||||
Neutralino.os.Icon.QUESTION,
|
||||
Neutralino.MessageBoxChoice.YES_NO,
|
||||
Neutralino.Icon.QUESTION,
|
||||
);
|
||||
|
||||
if (userAnswer === "NO") {
|
||||
@@ -320,9 +299,7 @@ function App() {
|
||||
const length = await getLengthMicroseconds(clip);
|
||||
|
||||
return {
|
||||
process: await Neutralino.os.spawnProcess(
|
||||
generateOutputCommand(ffmpegParams),
|
||||
),
|
||||
command: generateOutputCommand(ffmpegParams),
|
||||
file: clip,
|
||||
length,
|
||||
};
|
||||
@@ -331,21 +308,21 @@ function App() {
|
||||
async function convertAllClicked() {
|
||||
const list = fileList();
|
||||
|
||||
totalCount = list.length;
|
||||
const queue: FileQueueItem[] = [];
|
||||
|
||||
const processes = (await Promise.all(list.map(convertClip))).filter(
|
||||
(v) => v !== undefined,
|
||||
);
|
||||
for (const file of list) {
|
||||
const info = await convertClip(file);
|
||||
|
||||
setRunningProcesses(processes);
|
||||
|
||||
processes.forEach((v) => (logs[v.process.id] = []));
|
||||
if (info !== undefined) {
|
||||
queue.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
await Neutralino.storage.setData(
|
||||
"filesBeingProcessed",
|
||||
JSON.stringify(
|
||||
processes.map((v) => ({
|
||||
id: v.process.id,
|
||||
queue.map((v) => ({
|
||||
com: v.command,
|
||||
in: v.file,
|
||||
len: v.length,
|
||||
})),
|
||||
@@ -359,7 +336,7 @@ function App() {
|
||||
y: 120,
|
||||
injectGlobals: true,
|
||||
maximizable: false,
|
||||
enableInspector: false,
|
||||
processArgs: "--port=5433",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -372,17 +349,11 @@ function App() {
|
||||
|
||||
console.log(result);
|
||||
|
||||
totalCount = 1;
|
||||
|
||||
setRunningProcesses([result]);
|
||||
|
||||
logs[result.process.id] = [];
|
||||
|
||||
await Neutralino.storage.setData(
|
||||
"filesBeingProcessed",
|
||||
JSON.stringify([
|
||||
{
|
||||
id: result.process.id,
|
||||
com: result.command,
|
||||
in: result.file,
|
||||
len: result.length,
|
||||
},
|
||||
@@ -401,289 +372,299 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<main class="row flex-col">
|
||||
<div class="container" style={{ flex: "1" }}>
|
||||
<div class="row h-full">
|
||||
<div class="row flex-col h-full">
|
||||
<header
|
||||
class={`k-page-header k-rborder ${windowFocused() ? "" : "window-blur"}`}
|
||||
<main class="row">
|
||||
<div class="row flex-col h-full">
|
||||
<header
|
||||
class={`k-page-header k-rborder ${windowFocused() ? "" : "window-blur"}`}
|
||||
>
|
||||
<div class="page-title">Vencoder</div>
|
||||
</header>
|
||||
<div
|
||||
class="row flex-col gap2 k-white-sidebar k-rborder h-full"
|
||||
style={{ padding: "8px" }}
|
||||
>
|
||||
<ul class="k-list-view bordered col">
|
||||
<For each={fileList()}>
|
||||
{(item, _) => (
|
||||
<li
|
||||
class={
|
||||
item == selectedClip() ? "selected" : ""
|
||||
}
|
||||
onclick={() => setSelectedClip(item)}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
<div class="row gap2">
|
||||
<button onclick={openBtnClicked} class="k-button">
|
||||
Open...
|
||||
</button>
|
||||
<button onclick={removeAllBtnClicked} class="k-button">
|
||||
Remove All
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedClip() === ""}
|
||||
onclick={removeBtnClicked}
|
||||
class="icon-button k-button"
|
||||
>
|
||||
<div class="page-title">Vencoder</div>
|
||||
</header>
|
||||
<div
|
||||
class="row flex-col gap2 k-white-sidebar k-rborder h-full"
|
||||
style={{ padding: "8px" }}
|
||||
<BreezeIcon
|
||||
icon="b b-trash-empty"
|
||||
alt="Remove Selected Video"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedClip() === ""}
|
||||
onclick={playBtnClicked}
|
||||
class="icon-button k-button"
|
||||
>
|
||||
<ul class="k-list-view bordered col">
|
||||
<For each={fileList()}>
|
||||
{(item, _) => (
|
||||
<li
|
||||
class={
|
||||
item == selectedClip()
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
onclick={() =>
|
||||
setSelectedClip(item)
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
<div class="row gap2">
|
||||
<button
|
||||
onclick={openBtnClicked}
|
||||
class="k-button"
|
||||
>
|
||||
Open...
|
||||
</button>
|
||||
<button
|
||||
onclick={removeAllBtnClicked}
|
||||
class="k-button"
|
||||
>
|
||||
Remove All
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedClip() === ""}
|
||||
onclick={removeBtnClicked}
|
||||
class="icon-button k-button"
|
||||
>
|
||||
<BreezeIcon
|
||||
icon="b b-trash-empty"
|
||||
alt="Remove Selected Video"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedClip() === ""}
|
||||
onclick={playBtnClicked}
|
||||
class="icon-button k-button"
|
||||
>
|
||||
<BreezeIcon
|
||||
icon="playback-start"
|
||||
alt="Preview Selected Video"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="icon-button k-button"
|
||||
onclick={settingsBtnPressed}
|
||||
>
|
||||
<BreezeIcon
|
||||
icon="configure"
|
||||
alt="Configure"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-col h-full" style={{ width: "100%" }}>
|
||||
<header
|
||||
class={`k-page-header ${windowFocused() ? "" : "window-blur"}`}
|
||||
<BreezeIcon
|
||||
icon="playback-start"
|
||||
alt="Preview Selected Video"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="icon-button k-button"
|
||||
onclick={settingsBtnPressed}
|
||||
>
|
||||
<div class="page-title">Conversion Settings</div>
|
||||
</header>
|
||||
<div
|
||||
class="col row flex-col"
|
||||
style={{
|
||||
padding:
|
||||
"var(--k-grid-unit) var(--k-small-spacing)",
|
||||
flex: "1",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<form
|
||||
class="k-form"
|
||||
onsubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<label for="targetCodec">Codec</label>
|
||||
<select
|
||||
class="k-dropdown"
|
||||
id="targetCodec"
|
||||
oninput={selectedCodecsChanged}
|
||||
>
|
||||
<For each={displayedCodecs()}>
|
||||
{(item, _) => (
|
||||
<option value={item.shortName}>
|
||||
{item.description}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
<div></div>
|
||||
<div class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="commonCodecs"
|
||||
id="commonCodecs"
|
||||
oninput={showCommonCodecsChanged}
|
||||
checked
|
||||
/>
|
||||
<label for="commonCodecs">
|
||||
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 !==
|
||||
0
|
||||
}
|
||||
>
|
||||
<label for="videoEncoder">
|
||||
Encoder
|
||||
</label>
|
||||
<select
|
||||
name="videoEncoder"
|
||||
id="videoEncoder"
|
||||
class="k-dropdown"
|
||||
value={selectedEncoder()}
|
||||
oninput={(e) =>
|
||||
setSelectedEncoder(
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<For
|
||||
each={selectedCodec()?.encoders}
|
||||
>
|
||||
{(item, _) => (
|
||||
<option>{item}</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</Show>
|
||||
</form>
|
||||
<Switch fallback={<div></div>}>
|
||||
<Match
|
||||
when={
|
||||
selectedCodec()?.shortName ===
|
||||
"h264" ||
|
||||
selectedCodec()?.shortName ===
|
||||
"hevc"
|
||||
}
|
||||
>
|
||||
<H264Options
|
||||
codec={selectedCodec()}
|
||||
params={ffmpegParams}
|
||||
onParamChanged={onParametersChanged}
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
selectedCodec()?.shortName === "av1"
|
||||
}
|
||||
>
|
||||
<AV1Options
|
||||
codec={selectedCodec()}
|
||||
encoder={selectedEncoder()}
|
||||
params={ffmpegParams}
|
||||
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>
|
||||
<pre
|
||||
id="outputCommand"
|
||||
class="k-text-field w-full col"
|
||||
>
|
||||
{outputCommand()}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="k-page-footer row gap2">
|
||||
<button
|
||||
class="k-button"
|
||||
onclick={convertAllClicked}
|
||||
>
|
||||
Convert All
|
||||
</button>
|
||||
<button
|
||||
class="k-button"
|
||||
onclick={convertSelectedClicked}
|
||||
disabled={selectedClip() === ""}
|
||||
>
|
||||
Convert Selected
|
||||
</button>
|
||||
</footer>
|
||||
<BreezeIcon icon="configure" alt="Configure" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-col h-full" style={{ width: "100%" }}>
|
||||
<header
|
||||
class={`k-page-header ${windowFocused() ? "" : "window-blur"}`}
|
||||
>
|
||||
<div class="page-title">Conversion Settings</div>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div>
|
||||
<form
|
||||
class="k-form"
|
||||
onsubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<label for="targetCodec">Codec</label>
|
||||
<select
|
||||
class="k-dropdown"
|
||||
id="targetCodec"
|
||||
oninput={selectedCodecsChanged}
|
||||
>
|
||||
<For each={displayedCodecs()}>
|
||||
{(item, _) => (
|
||||
<option value={item.shortName}>
|
||||
{item.description}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
<div></div>
|
||||
<div class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="commonCodecs"
|
||||
id="commonCodecs"
|
||||
oninput={showCommonCodecsChanged}
|
||||
checked
|
||||
/>
|
||||
<label for="commonCodecs">
|
||||
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 !== 0}>
|
||||
<label for="videoEncoder">Encoder</label>
|
||||
<select
|
||||
name="videoEncoder"
|
||||
id="videoEncoder"
|
||||
class="k-dropdown"
|
||||
value={selectedEncoder()}
|
||||
oninput={(e) =>
|
||||
setSelectedEncoder(e.target.value)
|
||||
}
|
||||
>
|
||||
<For each={selectedCodec()?.encoders}>
|
||||
{(item, _) => <option>{item}</option>}
|
||||
</For>
|
||||
</select>
|
||||
</Show>
|
||||
<label for="pixelFormat">Pixel Format</label>
|
||||
<select
|
||||
name="pixelFormat"
|
||||
id="pixelFormat"
|
||||
class="k-dropdown"
|
||||
title="This option is here for the people who knows what they're doing. Not all encoders will support every pixel format."
|
||||
value={pixelFormat()}
|
||||
oninput={(e) => setPixelFormat(e.target.value)}
|
||||
>
|
||||
<option value="">Same as source</option>
|
||||
<For each={pixelFormatList()}>
|
||||
{(item, _) => (
|
||||
<option value={item}>{item}</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</form>
|
||||
<div class="row flex-col align-items-center">
|
||||
<h3 class="k-form-section-title">
|
||||
Encoder Options
|
||||
</h3>
|
||||
</div>
|
||||
<Switch
|
||||
fallback={
|
||||
<div class="text-center mt-4">No options.</div>
|
||||
}
|
||||
>
|
||||
<Match
|
||||
when={
|
||||
selectedCodec()?.shortName === "h264" ||
|
||||
selectedCodec()?.shortName === "hevc"
|
||||
}
|
||||
>
|
||||
<H264Options
|
||||
codec={selectedCodec()}
|
||||
encoder={selectedEncoder()}
|
||||
params={ffmpegParams}
|
||||
onParamChanged={onParametersChanged}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={selectedCodec()?.shortName === "av1"}>
|
||||
<AV1Options
|
||||
codec={selectedCodec()}
|
||||
encoder={selectedEncoder()}
|
||||
params={ffmpegParams}
|
||||
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">Audio</h3>
|
||||
</div>
|
||||
<form class="k-form">
|
||||
<label for="audioCodec">Codec</label>
|
||||
<select
|
||||
class="k-dropdown"
|
||||
id="audioCodec"
|
||||
value={audioCodec()}
|
||||
oninput={(e) => setAudioCodec(e.target.value)}
|
||||
>
|
||||
<option value="copy">Copy from source</option>
|
||||
<For each={audioCodecList()}>
|
||||
{(item, _) => (
|
||||
<option value={item.shortName}>
|
||||
{item.description}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
<Show when={getAudioEncoders()}>
|
||||
<label for="audioEncoder">Encoder</label>
|
||||
<select
|
||||
class="k-dropdown"
|
||||
id="audioEncoder"
|
||||
value={audioEncoder()}
|
||||
oninput={(e) =>
|
||||
setAudioEncoder(e.target.value)
|
||||
}
|
||||
>
|
||||
<For each={getAudioEncoders()}>
|
||||
{(item, _) => (
|
||||
<option value={item}>{item}</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</Show>
|
||||
</form>
|
||||
<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>
|
||||
<footer class="k-page-footer row flex-col gap2">
|
||||
<div class="row flex-col">
|
||||
<label for="outputCommand">Command</label>
|
||||
<pre id="outputCommand" class="k-text-field col">
|
||||
{outputCommand()}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="row gap2">
|
||||
<button class="k-button" onclick={convertAllClicked}>
|
||||
Convert All
|
||||
</button>
|
||||
<button
|
||||
class="k-button"
|
||||
onclick={convertSelectedClicked}
|
||||
disabled={selectedClip() === ""}
|
||||
>
|
||||
Convert Selected
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import { Match, Switch } from "solid-js";
|
||||
import type { CodecInfo, FFmpegParams } from "@/util/ffmpeg";
|
||||
import type {
|
||||
CodecInfo,
|
||||
FFmpegParamChangedFunc,
|
||||
FFmpegParams,
|
||||
} from "@/util/ffmpeg";
|
||||
import LibaomOptions from "./encoders/libaom";
|
||||
import Librav1eOptions from "./encoders/librav1e";
|
||||
import LibSvtAv1Options from "./encoders/libsvtav1";
|
||||
|
||||
function AV1Options(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
encoder: string;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
onParamChanged: FFmpegParamChangedFunc;
|
||||
}) {
|
||||
return (
|
||||
<Switch fallback={<div>No options.</div>}>
|
||||
<Switch fallback={<div class="text-center mt-4">No options.</div>}>
|
||||
<Match when={props.encoder === "libaom-av1"}>
|
||||
<LibaomOptions {...props} />
|
||||
</Match>
|
||||
<Match when={props.encoder === "librav1e"}>
|
||||
<Librav1eOptions {...props} />
|
||||
</Match>
|
||||
<Match when={props.encoder === "libsvtav1"}>
|
||||
<LibSvtAv1Options {...props} />
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { os } from "@neutralinojs/lib";
|
||||
import { type CodecInfo, type FFmpegParams } from "../util/ffmpeg";
|
||||
import {
|
||||
type CodecInfo,
|
||||
type FFmpegParamChangedFunc,
|
||||
type FFmpegParams,
|
||||
} from "../util/ffmpeg";
|
||||
import BreezeIcon from "./BreezeIcon";
|
||||
|
||||
/**
|
||||
@@ -8,46 +12,39 @@ import BreezeIcon from "./BreezeIcon";
|
||||
function DNxHDOptions(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
onParamChanged: FFmpegParamChangedFunc;
|
||||
}) {
|
||||
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,
|
||||
});
|
||||
}}
|
||||
<section id="commonLossyOptions" 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."
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,169 +1,40 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { Match, Switch } from "solid-js";
|
||||
import {
|
||||
DEFAULT_BITRATE,
|
||||
type CodecInfo,
|
||||
type FFmpegParamChangedFunc,
|
||||
type FFmpegParams,
|
||||
} from "../util/ffmpeg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
import BreezeIcon from "./BreezeIcon";
|
||||
|
||||
const information = {
|
||||
h264: {
|
||||
defaultCrf: 23,
|
||||
},
|
||||
hevc: {
|
||||
defaultCrf: 28,
|
||||
},
|
||||
};
|
||||
import LibH26xOptions from "./encoders/libx264";
|
||||
import H264QsvOptions from "./encoders/h264qsv";
|
||||
|
||||
/**
|
||||
* Options for H.264/H.265 codecs
|
||||
*/
|
||||
function H264Options(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
encoder: string;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
onParamChanged: FFmpegParamChangedFunc;
|
||||
}) {
|
||||
const [twopass, setTwopass] = createSignal(false);
|
||||
const defaultCrf =
|
||||
props.codec?.shortName === "h264"
|
||||
? information.h264.defaultCrf
|
||||
: information.hevc.defaultCrf;
|
||||
|
||||
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">
|
||||
<div></div>
|
||||
<div class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={props.params.twopass?.toString()}
|
||||
onInput={(e) => {
|
||||
props.params.twopass = e.target.checked;
|
||||
props.onParamChanged("twopass", e.target.checked);
|
||||
setTwopass(e.target.checked);
|
||||
}}
|
||||
id="twopassCheck"
|
||||
/>
|
||||
<label for="twopassCheck">
|
||||
Use target bitrate instead of CRF
|
||||
</label>
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick={() =>
|
||||
os.open(
|
||||
"https://trac.ffmpeg.org/wiki/Encode/H.264#twopass",
|
||||
)
|
||||
}
|
||||
title="This will use the two-pass rate control mode instead of relying on a Constant Rate Factor (CRF) value."
|
||||
>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
<label for="encodingPreset">Preset</label>
|
||||
<select
|
||||
class="k-dropdown"
|
||||
name="encodingPreset"
|
||||
id="encodingPreset"
|
||||
value={props.params.preset ?? "medium"}
|
||||
oninput={(e) => {
|
||||
props.params.preset = e.target.value;
|
||||
props.onParamChanged("preset", e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="ultrafast">ultrafast</option>
|
||||
<option value="superfast">superfast</option>
|
||||
<option value="veryfast">veryfast</option>
|
||||
<option value="faster">faster</option>
|
||||
<option value="fast">fast</option>
|
||||
<option value="medium">medium (Default)</option>
|
||||
<option value="slow">slow</option>
|
||||
<option value="slower">slower</option>
|
||||
<option value="veryslow">veryslow</option>
|
||||
<option
|
||||
value="placebo"
|
||||
title="Don't use this option, it rarely helps."
|
||||
>
|
||||
placebo
|
||||
</option>
|
||||
</select>
|
||||
<Show
|
||||
when={twopass()}
|
||||
fallback={
|
||||
<>
|
||||
<label for="crf">CRF</label>
|
||||
<input
|
||||
type="number"
|
||||
name="crf"
|
||||
id="crf"
|
||||
min="0"
|
||||
max="51"
|
||||
value={props.params.crf ?? defaultCrf}
|
||||
oninput={(e) => {
|
||||
props.params.crf = parseInt(e.target.value);
|
||||
props.onParamChanged(
|
||||
"crf",
|
||||
parseInt(e.target.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<label for="bitrate">Bitrate</label>
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
name="bitrate"
|
||||
id="bitrate"
|
||||
value={props.params.vbitrate ?? DEFAULT_BITRATE}
|
||||
oninput={(e) => {
|
||||
props.params.vbitrate = parseInt(
|
||||
e.target.value,
|
||||
);
|
||||
props.onParamChanged(
|
||||
"vbitrate",
|
||||
parseInt(e.target.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span> Kbps</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.codec?.shortName === "h264"}>
|
||||
<div></div>
|
||||
<div class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={props.params.faststart?.toString()}
|
||||
onInput={(e) => {
|
||||
props.params.faststart = e.target.checked;
|
||||
props.onParamChanged(
|
||||
"faststart",
|
||||
e.target.checked,
|
||||
);
|
||||
}}
|
||||
id="fastStartCheck"
|
||||
/>
|
||||
<label for="fastStartCheck">Enable Fast Start</label>
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick={() =>
|
||||
os.open(
|
||||
"https://trac.ffmpeg.org/wiki/Encode/H.264#faststartforwebvideo",
|
||||
)
|
||||
}
|
||||
title="This will move some information to the beginning of your file and allow the video to begin playing before it is completely downloaded by the viewer, recommended for web videos. Click for more information."
|
||||
>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
<Switch fallback={<div class="text-center mt-4">No options.</div>}>
|
||||
<Match
|
||||
when={
|
||||
props.encoder === "libx264" ||
|
||||
props.encoder === "libx264rgb" ||
|
||||
props.encoder === "libx265"
|
||||
}
|
||||
>
|
||||
<LibH26xOptions {...props} />
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
props.encoder === "h264_qsv" || props.encoder === "hevc_qsv"
|
||||
}
|
||||
>
|
||||
<H264QsvOptions {...props} />
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import {
|
||||
DEFAULT_BITRATE,
|
||||
type CodecInfo,
|
||||
type FFmpegParams,
|
||||
} from "../util/ffmpeg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
import BreezeIcon from "./BreezeIcon";
|
||||
|
||||
function VP9Options(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
}) {
|
||||
const [twopass, setTwopass] = createSignal(false);
|
||||
const defaultCrf = 30;
|
||||
|
||||
return (
|
||||
<section id="commonLossyOptions" class="k-form">
|
||||
<div></div>
|
||||
<div class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={props.params.twopass?.toString()}
|
||||
onInput={(e) => {
|
||||
props.params.twopass = e.target.checked;
|
||||
props.onParamChanged("twopass", e.target.checked);
|
||||
setTwopass(e.target.checked);
|
||||
}}
|
||||
id="twopassCheck"
|
||||
/>
|
||||
<label for="twopassCheck">
|
||||
Use target bitrate instead of CRF
|
||||
</label>
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick={() =>
|
||||
os.open(
|
||||
"https://trac.ffmpeg.org/wiki/Encode/H.264#twopass",
|
||||
)
|
||||
}
|
||||
title="This will use the two-pass rate control mode instead of relying on a Constant Rate Factor (CRF) value."
|
||||
>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
<label for="encodingPreset">Preset</label>
|
||||
<select
|
||||
class="k-dropdown"
|
||||
name="encodingPreset"
|
||||
id="encodingPreset"
|
||||
value={props.params.preset ?? "medium"}
|
||||
oninput={(e) => {
|
||||
props.params.preset = e.target.value;
|
||||
props.onParamChanged("preset", e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="ultrafast">ultrafast</option>
|
||||
<option value="superfast">superfast</option>
|
||||
<option value="veryfast">veryfast</option>
|
||||
<option value="faster">faster</option>
|
||||
<option value="fast">fast</option>
|
||||
<option value="medium">medium (Default)</option>
|
||||
<option value="slow">slow</option>
|
||||
<option value="slower">slower</option>
|
||||
<option value="veryslow">veryslow</option>
|
||||
<option
|
||||
value="placebo"
|
||||
title="Don't use this option, it rarely helps."
|
||||
>
|
||||
placebo
|
||||
</option>
|
||||
</select>
|
||||
<Show
|
||||
when={twopass()}
|
||||
fallback={
|
||||
<>
|
||||
<label for="crf">CRF</label>
|
||||
<input
|
||||
type="number"
|
||||
name="crf"
|
||||
id="crf"
|
||||
min="0"
|
||||
max="51"
|
||||
value={props.params.crf ?? defaultCrf}
|
||||
oninput={(e) => {
|
||||
props.params.crf = parseInt(e.target.value);
|
||||
props.onParamChanged(
|
||||
"crf",
|
||||
parseInt(e.target.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<label for="bitrate">Bitrate</label>
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
name="bitrate"
|
||||
id="bitrate"
|
||||
value={props.params.vbitrate ?? DEFAULT_BITRATE}
|
||||
oninput={(e) => {
|
||||
props.params.vbitrate = parseInt(e.target.value);
|
||||
props.onParamChanged(
|
||||
"vbitrate",
|
||||
parseInt(e.target.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span> Kbps</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.codec?.shortName === "h264"}>
|
||||
<div></div>
|
||||
<div class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={props.params.faststart?.toString()}
|
||||
onInput={(e) => {
|
||||
props.params.faststart = e.target.checked;
|
||||
props.onParamChanged("faststart", e.target.checked);
|
||||
}}
|
||||
id="fastStartCheck"
|
||||
/>
|
||||
<label for="fastStartCheck">Enable Fast Start</label>
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick={() =>
|
||||
os.open(
|
||||
"https://trac.ffmpeg.org/wiki/Encode/H.264#faststartforwebvideo",
|
||||
)
|
||||
}
|
||||
title="This will move some information to the beginning of your file and allow the video to begin playing before it is completely downloaded by the viewer, recommended for web videos. Click for more information."
|
||||
>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default VP9Options;
|
||||
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
DEFAULT_BITRATE,
|
||||
type CodecInfo,
|
||||
type FFmpegParamChangedFunc,
|
||||
type FFmpegParams,
|
||||
} from "@/util/ffmpeg";
|
||||
import { createEffect, createSignal, onMount, Show } from "solid-js";
|
||||
|
||||
function H264QsvOptions({
|
||||
onParamChanged,
|
||||
}: {
|
||||
codec: CodecInfo | undefined;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: FFmpegParamChangedFunc;
|
||||
}) {
|
||||
const [rateControl, setRateControl] = createSignal("icq");
|
||||
const [globalQuality, setGlobalQuality] = createSignal(18);
|
||||
const [bitrate, setBitrate] = createSignal(DEFAULT_BITRATE);
|
||||
const [cqp, setCqp] = createSignal(18);
|
||||
|
||||
createEffect(() => {
|
||||
const opts: Record<string, string> = {};
|
||||
|
||||
switch (rateControl()) {
|
||||
case "icq":
|
||||
onParamChanged("vbitrate", undefined);
|
||||
const quality = globalQuality();
|
||||
opts["global_quality"] = quality.toString();
|
||||
break;
|
||||
case "cbr":
|
||||
onParamChanged("vbitrate", bitrate());
|
||||
opts["maxrate"] = `${bitrate() ?? DEFAULT_BITRATE}k`;
|
||||
break;
|
||||
case "vbr":
|
||||
onParamChanged("vbitrate", bitrate());
|
||||
break;
|
||||
case "cqp":
|
||||
onParamChanged("vbitrate", undefined);
|
||||
opts["q"] = cqp().toString();
|
||||
break;
|
||||
}
|
||||
|
||||
onParamChanged("outputopts", opts);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
onParamChanged("hwaccel", "qsv");
|
||||
});
|
||||
|
||||
return (
|
||||
<section id="encoderOptions" class="k-form">
|
||||
<label for="rateControl">Rate Control Mode</label>
|
||||
<select
|
||||
name="rateControl"
|
||||
id="rateControl"
|
||||
class="k-dropdown"
|
||||
value={rateControl()}
|
||||
oninput={(e) => setRateControl(e.target.value)}
|
||||
>
|
||||
<option value="cbr">CBR: Constant Bitrate</option>
|
||||
<option value="cqp">
|
||||
CQP: Constant Quantization Parameter
|
||||
</option>
|
||||
<option value="vbr">VBR: Variable Bitrate</option>
|
||||
<option value="icq">ICQ: Intelligent Constant Quality</option>
|
||||
</select>
|
||||
<Show when={rateControl() === "icq"}>
|
||||
<label for="globalQuality">Global Quality</label>
|
||||
<input
|
||||
type="number"
|
||||
name="globalQuality"
|
||||
id="globalQuality"
|
||||
min="1"
|
||||
max="51"
|
||||
value={globalQuality()}
|
||||
oninput={(e) => setGlobalQuality(parseInt(e.target.value))}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={rateControl() === "cqp"}>
|
||||
<label for="cqp">CQP</label>
|
||||
<input
|
||||
type="number"
|
||||
name="cqp"
|
||||
id="cqp"
|
||||
min="1"
|
||||
max="51"
|
||||
value={cqp()}
|
||||
oninput={(e) => setCqp(parseInt(e.target.value))}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={rateControl() === "cbr" || rateControl() === "vbr"}>
|
||||
<label for="bitrate">Bitrate</label>
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
name="bitrate"
|
||||
id="bitrate"
|
||||
value={bitrate()}
|
||||
oninput={(e) => setBitrate(parseInt(e.target.value))}
|
||||
/>
|
||||
<span> Kbps</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={rateControl() === "vbr" || rateControl() === "icq"}>
|
||||
<div></div>
|
||||
<div
|
||||
class="checkbox-container"
|
||||
title="May not be available on some platforms."
|
||||
>
|
||||
<input type="checkbox" name="lookAhead" id="lookAhead" />
|
||||
<label for="lookAhead">Look-ahead mode</label>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default H264QsvOptions;
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
DEFAULT_BITRATE,
|
||||
type CodecInfo,
|
||||
type FFmpegParamChangedFunc,
|
||||
type FFmpegParams,
|
||||
} from "@/util/ffmpeg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
@@ -12,7 +13,7 @@ const DEFAULT_CRF = 23;
|
||||
function LibaomOptions(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
onParamChanged: FFmpegParamChangedFunc;
|
||||
}) {
|
||||
const [rateControlMode, setRateControlMode] = createSignal("Constant");
|
||||
|
||||
@@ -45,79 +46,71 @@ function LibaomOptions(props: {
|
||||
});
|
||||
|
||||
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 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>
|
||||
<option value="2PassABR">2-Pass Average Bitrate</option>
|
||||
<option value="ABR">1-Pass Average Bitrate</option>
|
||||
</select>
|
||||
<Show
|
||||
when={
|
||||
rateControlMode() === "Constant" ||
|
||||
rateControlMode() === "Constrained"
|
||||
<section id="encoderOptions" 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."
|
||||
>
|
||||
<label for="crf">CRF</label>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
<option value="2PassABR">2-Pass Average Bitrate</option>
|
||||
<option value="ABR">1-Pass Average Bitrate</option>
|
||||
</select>
|
||||
<Show
|
||||
when={
|
||||
rateControlMode() === "Constant" ||
|
||||
rateControlMode() === "Constrained"
|
||||
}
|
||||
>
|
||||
<label for="crf">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 for="bitrate">Bitrate</label>
|
||||
<div class="row gap2 align-items-center">
|
||||
<input
|
||||
type="number"
|
||||
name="crf"
|
||||
id="crf"
|
||||
min="1"
|
||||
max="63"
|
||||
value={props.params.crf ?? DEFAULT_CRF}
|
||||
name="bitrate"
|
||||
id="bitrate"
|
||||
aria-label="Kbps"
|
||||
value={props.params.vbitrate ?? DEFAULT_BITRATE}
|
||||
oninput={(e) => {
|
||||
props.onParamChanged(
|
||||
"crf",
|
||||
"vbitrate",
|
||||
parseInt(e.target.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={rateControlMode() !== "Constant"}>
|
||||
<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(
|
||||
"vbitrate",
|
||||
parseInt(e.target.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span>Kbps</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<span>Kbps</span>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
DEFAULT_BITRATE,
|
||||
type CodecInfo,
|
||||
type FFmpegParamChangedFunc,
|
||||
type FFmpegParams,
|
||||
} from "@/util/ffmpeg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
@@ -10,10 +11,9 @@ import { onMount } from "solid-js";
|
||||
function Librav1eOptions(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
onParamChanged: FFmpegParamChangedFunc;
|
||||
}) {
|
||||
onMount(() => {
|
||||
props.onParamChanged("crf", undefined);
|
||||
props.onParamChanged(
|
||||
"vbitrate",
|
||||
props.params.vbitrate ?? DEFAULT_BITRATE,
|
||||
@@ -22,50 +22,48 @@ function Librav1eOptions(props: {
|
||||
});
|
||||
|
||||
return (
|
||||
<section id="encoderOptions">
|
||||
<div class="row flex-col align-items-center">
|
||||
<h3 class="k-form-section-title">Encoder Options</h3>
|
||||
<section id="encoderOptions" 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>
|
||||
<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>
|
||||
<label>Speed</label>
|
||||
<input
|
||||
type="number"
|
||||
name="speed"
|
||||
id="speed"
|
||||
min="0"
|
||||
max="10"
|
||||
value={props.params.speed ?? 5}
|
||||
oninput={(e) =>
|
||||
props.onParamChanged("speed", parseInt(e.target.value))
|
||||
}
|
||||
/>
|
||||
<label for="bitrate">Bitrate</label>
|
||||
<div class="row gap2 align-items-center">
|
||||
<input
|
||||
type="number"
|
||||
name="speed"
|
||||
id="speed"
|
||||
min="0"
|
||||
max="10"
|
||||
value={props.params.speed ?? 5}
|
||||
name="bitrate"
|
||||
id="bitrate"
|
||||
value={props.params.vbitrate ?? DEFAULT_BITRATE}
|
||||
oninput={(e) =>
|
||||
props.onParamChanged("speed", e.target.value)
|
||||
props.onParamChanged(
|
||||
"vbitrate",
|
||||
parseInt(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>
|
||||
<span>Kbps</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
type CodecInfo,
|
||||
type FFmpegParamChangedFunc,
|
||||
type FFmpegParams,
|
||||
} from "@/util/ffmpeg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
import BreezeIcon from "@/components/BreezeIcon";
|
||||
import { createEffect, createSignal } from "solid-js";
|
||||
|
||||
function LibSvtAv1Options({
|
||||
params,
|
||||
onParamChanged,
|
||||
}: {
|
||||
codec: CodecInfo | undefined;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: FFmpegParamChangedFunc;
|
||||
}) {
|
||||
const [gop, setGop] = createSignal("-1");
|
||||
const [filmGrain, setFilmGrain] = createSignal("0");
|
||||
const [tune, setTune] = createSignal("1");
|
||||
|
||||
createEffect(() => {
|
||||
const g = gop();
|
||||
const params = [`tune=${tune()}`];
|
||||
|
||||
if (filmGrain() !== "0") {
|
||||
params.push(`film-grain=${filmGrain()}`);
|
||||
}
|
||||
|
||||
onParamChanged("outputopts", {
|
||||
g: g === "-1" ? undefined : g,
|
||||
"svtav1-params": params.join(":"),
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<section id="encoderOptions" class="k-form">
|
||||
<label>Help</label>
|
||||
<div>
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick={() =>
|
||||
os.open(
|
||||
"https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md",
|
||||
)
|
||||
}
|
||||
title="Click to view the documentation for this encoder."
|
||||
>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
<label for="preset">Preset</label>
|
||||
<input
|
||||
type="number"
|
||||
name="preset"
|
||||
id="preset"
|
||||
title="A range from -2 to 13. Higher means faster but a loss in quality. Preset 13 is not intended for human use."
|
||||
value={params.preset ?? 5}
|
||||
oninput={(e) => onParamChanged("preset", e.target.value)}
|
||||
min="-2"
|
||||
max="13"
|
||||
/>
|
||||
<label for="crf">CRF</label>
|
||||
<input
|
||||
type="number"
|
||||
name="crf"
|
||||
id="crf"
|
||||
title="A range from 1 to 63. A good starting point for a 1080p video is 30"
|
||||
value={params.crf ?? 30}
|
||||
oninput={(e) => onParamChanged("crf", parseInt(e.target.value))}
|
||||
min="1"
|
||||
max="63"
|
||||
/>
|
||||
<label for="gop">GOP</label>
|
||||
<input
|
||||
type="number"
|
||||
name="gop"
|
||||
id="gop"
|
||||
title="How many frames will pass before the encoder will add a key frame. Specify -1 to leave the parameter unspecified."
|
||||
value={gop()}
|
||||
oninput={(e) => setGop(e.target.value)}
|
||||
min="-1"
|
||||
/>
|
||||
<label for="filmGrain">Film Grain</label>
|
||||
<input
|
||||
type="number"
|
||||
name="filmGrain"
|
||||
id="filmGrain"
|
||||
title="Film grains are hard to compress. The encoder can try to replace the film grain in the video with a synthetic grain."
|
||||
value={filmGrain()}
|
||||
oninput={(e) => setFilmGrain(e.target.value)}
|
||||
min="0"
|
||||
/>
|
||||
<label for="tune">Tune</label>
|
||||
<select
|
||||
name="tune"
|
||||
id="tune"
|
||||
class="k-dropdown"
|
||||
value={tune()}
|
||||
oninput={(e) => setTune(e.target.value)}
|
||||
>
|
||||
<option value="0">Subjective Quality</option>
|
||||
<option value="1">Objective Quality (PSNR)</option>
|
||||
</select>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default LibSvtAv1Options;
|
||||
@@ -0,0 +1,160 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import {
|
||||
DEFAULT_BITRATE,
|
||||
type CodecInfo,
|
||||
type FFmpegParamChangedFunc,
|
||||
type FFmpegParams,
|
||||
} from "@/util/ffmpeg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
import BreezeIcon from "@/components/BreezeIcon";
|
||||
|
||||
const information = {
|
||||
h264: {
|
||||
defaultCrf: 23,
|
||||
},
|
||||
hevc: {
|
||||
defaultCrf: 28,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for H.264/H.265 codecs
|
||||
*/
|
||||
function LibH26xOptions(props: {
|
||||
codec: CodecInfo | undefined;
|
||||
params: FFmpegParams;
|
||||
onParamChanged: FFmpegParamChangedFunc;
|
||||
}) {
|
||||
const [twopass, setTwopass] = createSignal(false);
|
||||
const defaultCrf =
|
||||
props.codec?.shortName === "h264"
|
||||
? information.h264.defaultCrf
|
||||
: information.hevc.defaultCrf;
|
||||
|
||||
return (
|
||||
<section id="commonLossyOptions" class="k-form">
|
||||
<div></div>
|
||||
<div class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={props.params.twopass?.toString()}
|
||||
onInput={(e) => {
|
||||
props.params.twopass = e.target.checked;
|
||||
props.onParamChanged("twopass", e.target.checked);
|
||||
setTwopass(e.target.checked);
|
||||
}}
|
||||
id="twopassCheck"
|
||||
/>
|
||||
<label for="twopassCheck">
|
||||
Use target bitrate instead of CRF
|
||||
</label>
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick={() =>
|
||||
os.open(
|
||||
"https://trac.ffmpeg.org/wiki/Encode/H.264#twopass",
|
||||
)
|
||||
}
|
||||
title="This will use the two-pass rate control mode instead of relying on a Constant Rate Factor (CRF) value."
|
||||
>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
<label for="encodingPreset">Preset</label>
|
||||
<select
|
||||
class="k-dropdown"
|
||||
name="encodingPreset"
|
||||
id="encodingPreset"
|
||||
value={props.params.preset ?? "medium"}
|
||||
oninput={(e) => {
|
||||
props.params.preset = e.target.value;
|
||||
props.onParamChanged("preset", e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="ultrafast">ultrafast</option>
|
||||
<option value="superfast">superfast</option>
|
||||
<option value="veryfast">veryfast</option>
|
||||
<option value="faster">faster</option>
|
||||
<option value="fast">fast</option>
|
||||
<option value="medium">medium (Default)</option>
|
||||
<option value="slow">slow</option>
|
||||
<option value="slower">slower</option>
|
||||
<option value="veryslow">veryslow</option>
|
||||
<option
|
||||
value="placebo"
|
||||
title="Don't use this option, it rarely helps."
|
||||
>
|
||||
placebo
|
||||
</option>
|
||||
</select>
|
||||
<Show
|
||||
when={twopass()}
|
||||
fallback={
|
||||
<>
|
||||
<label for="crf">CRF</label>
|
||||
<input
|
||||
type="number"
|
||||
name="crf"
|
||||
id="crf"
|
||||
min="0"
|
||||
max="51"
|
||||
value={props.params.crf ?? defaultCrf}
|
||||
oninput={(e) => {
|
||||
props.params.crf = parseInt(e.target.value);
|
||||
props.onParamChanged(
|
||||
"crf",
|
||||
parseInt(e.target.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<label for="bitrate">Bitrate</label>
|
||||
<div>
|
||||
<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>
|
||||
<Show when={props.codec?.shortName === "h264"}>
|
||||
<div></div>
|
||||
<div class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={props.params.faststart?.toString()}
|
||||
onInput={(e) => {
|
||||
props.params.faststart = e.target.checked;
|
||||
props.onParamChanged("faststart", e.target.checked);
|
||||
}}
|
||||
id="fastStartCheck"
|
||||
/>
|
||||
<label for="fastStartCheck">Enable Fast Start</label>
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick={() =>
|
||||
os.open(
|
||||
"https://trac.ffmpeg.org/wiki/Encode/H.264#faststartforwebvideo",
|
||||
)
|
||||
}
|
||||
title="This will move some information to the beginning of your file and allow the video to begin playing before it is completely downloaded by the viewer, recommended for web videos. Click for more information."
|
||||
>
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default LibH26xOptions;
|
||||
@@ -1,5 +1,7 @@
|
||||
/* An attempt of imitating KDE's Kirigami UI Framework */
|
||||
|
||||
@reference "./index.css";
|
||||
|
||||
:root {
|
||||
--k-grid-unit: 16px;
|
||||
--k-small-spacing: 4px;
|
||||
@@ -97,6 +99,7 @@ button {
|
||||
border-radius: var(--k-border-radius);
|
||||
box-shadow: var(--k-border-color) 0 1px;
|
||||
background-color: var(--k-primary-highlight);
|
||||
padding: 2px 6px;
|
||||
font-size: inherit;
|
||||
|
||||
&:hover {
|
||||
@@ -123,6 +126,7 @@ button {
|
||||
padding: var(--k-small-spacing) calc(var(--k-medium-spacing) + 12px)
|
||||
var(--k-small-spacing) var(--k-medium-spacing);
|
||||
border: 1px solid var(--k-border-color);
|
||||
border-radius: var(--k-border-radius);
|
||||
box-shadow: var(--k-border-color) 0 1px;
|
||||
max-width: 16em;
|
||||
font-size: inherit;
|
||||
@@ -177,6 +181,7 @@ input[type="number"] {
|
||||
border-bottom: 1px solid var(--k-border-color);
|
||||
width: fit-content;
|
||||
margin-bottom: var(--k-medium-spacing);
|
||||
@apply text-xl font-bold mt-4;
|
||||
}
|
||||
|
||||
.k-page-footer {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
--system-accent-color: accentcolor;
|
||||
@@ -92,3 +94,17 @@ h2 {
|
||||
gap: var(--k-medium-spacing);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: var(--k-grid-unit) var(--k-small-spacing);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
progress {
|
||||
accent-color: var(--system-accent-color);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { events, os, storage } from "@neutralinojs/lib";
|
||||
import { createSignal, onMount, onCleanup, Show, Index } from "solid-js";
|
||||
import { openFile } from "@/util/oshelper";
|
||||
import { getTemporaryFilePath, getVencoderFolder } from "@/util/path";
|
||||
import { generateRandomString } from "@/util/string";
|
||||
import { events, os, storage, type SpawnedProcess } from "@neutralinojs/lib";
|
||||
import { createSignal, onMount, onCleanup, Show } from "solid-js";
|
||||
|
||||
interface TargetFile {
|
||||
id: string;
|
||||
com: string;
|
||||
in: string;
|
||||
len: number;
|
||||
}
|
||||
@@ -29,15 +32,21 @@ interface FFmpegProgressInfo {
|
||||
function ProgressPage() {
|
||||
const [windowFocused, setWindowFocused] = createSignal(true);
|
||||
const [runningProcesses, setRunningProcesses] = createSignal<
|
||||
os.SpawnedProcess[]
|
||||
SpawnedProcess[]
|
||||
>([]);
|
||||
const [queueLength, setQueueLength] = createSignal(0);
|
||||
const [finished, setFinished] = createSignal(false);
|
||||
const [fileInfo, setFileInfo] = createSignal<TargetFile[]>([]);
|
||||
const progressObject: {
|
||||
[key: string]: ProgressInfo;
|
||||
} = {};
|
||||
const [progressList, setProgressList] = createSignal<ProgressInfo[]>([]);
|
||||
const [isCancelling, setIsCancelling] = createSignal(false);
|
||||
const filesBeingProcessed: Record<number, TargetFile> = {};
|
||||
const logs: { [id: number]: string[] } = {};
|
||||
let fileQueue: TargetFile[] = [];
|
||||
let successfulCount = 0;
|
||||
let unsuccessfulCount = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
function windowIsFocused() {
|
||||
setWindowFocused(false);
|
||||
@@ -47,6 +56,26 @@ function ProgressPage() {
|
||||
setWindowFocused(true);
|
||||
}
|
||||
|
||||
async function processFiles(files: TargetFile[]) {
|
||||
const processes = [];
|
||||
|
||||
for (const file of files) {
|
||||
const proc = await os.spawnProcess(file.com);
|
||||
|
||||
logs[proc.id] = [];
|
||||
processes.push(proc);
|
||||
|
||||
progressObject[proc.id] = {
|
||||
filename: file.in,
|
||||
percentage: 0,
|
||||
};
|
||||
|
||||
filesBeingProcessed[proc.id] = file;
|
||||
}
|
||||
|
||||
setRunningProcesses(processes);
|
||||
}
|
||||
|
||||
function handleSpawnedProcessEvents(evt: CustomEvent) {
|
||||
switch (evt.detail.action) {
|
||||
case "stdOut":
|
||||
@@ -55,14 +84,12 @@ function ProgressPage() {
|
||||
.split("\n")
|
||||
.map((v) => v.split("=")),
|
||||
);
|
||||
const file = fileInfo().find((v) => v.id === evt.detail.id);
|
||||
const file = filesBeingProcessed[evt.detail.id];
|
||||
|
||||
if (file === undefined) return;
|
||||
|
||||
progressObject[evt.detail.id] = {
|
||||
filename: file.in,
|
||||
percentage: (parseInt(info.out_time_us) / file.len) * 100,
|
||||
};
|
||||
progressObject[evt.detail.id].percentage =
|
||||
(parseInt(info.out_time_us) / file.len) * 100;
|
||||
|
||||
if (Number.isNaN(progressObject[evt.detail.id].percentage)) {
|
||||
progressObject[evt.detail.id].percentage = 0;
|
||||
@@ -71,17 +98,55 @@ function ProgressPage() {
|
||||
setProgressList(Object.values(progressObject));
|
||||
break;
|
||||
case "stdErr":
|
||||
logs[evt.detail.id].push(evt.detail.data);
|
||||
break;
|
||||
case "exit":
|
||||
console.log(`FFmpeg exited with code: ${evt.detail.data}`);
|
||||
|
||||
os.getSpawnedProcesses().then((processes) => {
|
||||
if (processes.length === 0) {
|
||||
setFinished(true);
|
||||
}
|
||||
if (evt.detail.data === 0) {
|
||||
progressObject[evt.detail.id].percentage = 100;
|
||||
setProgressList(Object.values(progressObject));
|
||||
successfulCount += 1;
|
||||
} else {
|
||||
unsuccessfulCount += 1;
|
||||
|
||||
setRunningProcesses(processes);
|
||||
});
|
||||
// If the exit code isn't 255 (the exit code of the program exiting because of cancellation)
|
||||
if (evt.detail.data !== 255) {
|
||||
Neutralino.os.showNotification(
|
||||
"File Encoding Failed",
|
||||
`Encoding for file "${filesBeingProcessed[evt.detail.id].in}" failed. Exit code ${evt.detail.data}.`,
|
||||
);
|
||||
|
||||
const tempFilename = `${getTemporaryFilePath()}/vencoder-ffmpeg-${generateRandomString(8)}.log`;
|
||||
Neutralino.filesystem.writeFile(
|
||||
tempFilename,
|
||||
logs[evt.detail.id].join("\n"),
|
||||
);
|
||||
openFile(tempFilename);
|
||||
}
|
||||
}
|
||||
|
||||
if (successfulCount + unsuccessfulCount === totalCount) {
|
||||
os.showNotification(
|
||||
"File(s) encoded.",
|
||||
`${successfulCount} files encoded successfully. ${unsuccessfulCount} failed or cancelled.`,
|
||||
);
|
||||
successfulCount = 0;
|
||||
unsuccessfulCount = 0;
|
||||
totalCount = 0;
|
||||
}
|
||||
|
||||
if (finished()) return;
|
||||
|
||||
const nextFile = fileQueue.pop();
|
||||
|
||||
if (nextFile === undefined) {
|
||||
setFinished(true);
|
||||
return;
|
||||
}
|
||||
|
||||
processFiles([nextFile]);
|
||||
setQueueLength(fileQueue.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -91,20 +156,16 @@ function ProgressPage() {
|
||||
events.on("windowBlur", windowUnfocused);
|
||||
events.on("spawnedProcess", handleSpawnedProcessEvents);
|
||||
|
||||
const processes = await os.getSpawnedProcesses();
|
||||
setRunningProcesses(processes);
|
||||
|
||||
const storedFileInfo: TargetFile[] = JSON.parse(
|
||||
await storage.getData("filesBeingProcessed"),
|
||||
);
|
||||
setFileInfo(storedFileInfo);
|
||||
totalCount = storedFileInfo.length;
|
||||
|
||||
for (let i = 0; i < processes.length; i++) {
|
||||
progressObject[processes[i].id] = {
|
||||
filename: storedFileInfo[i].in,
|
||||
percentage: 0,
|
||||
};
|
||||
}
|
||||
const file = storedFileInfo.pop()!;
|
||||
|
||||
processFiles([file]);
|
||||
fileQueue = storedFileInfo;
|
||||
setQueueLength(fileQueue.length);
|
||||
|
||||
setProgressList(Object.values(progressObject));
|
||||
});
|
||||
@@ -131,6 +192,14 @@ function ProgressPage() {
|
||||
setFinished(true);
|
||||
}
|
||||
|
||||
async function openFolder() {
|
||||
const folder = await getVencoderFolder();
|
||||
|
||||
if (folder) {
|
||||
os.open(folder);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main class="row flex-col">
|
||||
<div class="container row flex-col" style={{ flex: "1" }}>
|
||||
@@ -148,15 +217,15 @@ function ProgressPage() {
|
||||
<Show when={finished()}>
|
||||
Processes finished. You can close this window.
|
||||
</Show>
|
||||
<Index each={progressList()}>
|
||||
{(item, _) => (
|
||||
{progressList().map((item) => {
|
||||
return (
|
||||
<div
|
||||
class="row flex-col"
|
||||
style={{
|
||||
"padding-bottom": "var(--k-grid-unit)",
|
||||
}}
|
||||
>
|
||||
<label>{item().filename}</label>
|
||||
<label>{item.filename}</label>
|
||||
<div
|
||||
class="grid"
|
||||
style={{
|
||||
@@ -166,36 +235,41 @@ function ProgressPage() {
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<progress
|
||||
class="col"
|
||||
value={item().percentage}
|
||||
value={item.percentage}
|
||||
max="100"
|
||||
/>
|
||||
</div>
|
||||
<div class="row justify-content-center align-items-center">
|
||||
{Math.min(
|
||||
Math.round(item().percentage),
|
||||
Math.round(item.percentage),
|
||||
100,
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Index>
|
||||
);
|
||||
})}
|
||||
<div>{queueLength()} file(s) queued.</div>
|
||||
</div>
|
||||
<Show when={!finished()}>
|
||||
<footer
|
||||
class="p-medium row"
|
||||
style={{ "align-items": "end" }}
|
||||
<footer class="p-medium row" style={{ "align-items": "end" }}>
|
||||
<Show
|
||||
when={finished()}
|
||||
fallback={
|
||||
<button
|
||||
class="k-button"
|
||||
disabled={isCancelling()}
|
||||
onclick={cancelBtnClicked}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<button
|
||||
class="k-button"
|
||||
disabled={isCancelling()}
|
||||
onclick={cancelBtnClicked}
|
||||
>
|
||||
Cancel
|
||||
<button class="k-button" onclick={openFolder}>
|
||||
Open Folder
|
||||
</button>
|
||||
</footer>
|
||||
</Show>
|
||||
</Show>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -7,24 +7,27 @@ export interface CodecInfo {
|
||||
encoders: string[];
|
||||
}
|
||||
|
||||
export async function getAvailableCodecs(): Promise<CodecInfo[]> {
|
||||
export type CodecList = {
|
||||
vcodecs: CodecInfo[],
|
||||
acodecs: CodecInfo[]
|
||||
}
|
||||
|
||||
export async function getAvailableCodecs(): Promise<CodecList> {
|
||||
const seperator = "-------";
|
||||
const videoEncodingSupported = /.EV.../;
|
||||
const wideFormattingSpaces = / {2,}/;
|
||||
const decodeEncodeSpecification = / \(((decoders)|(encoders)):.+\)/g;
|
||||
const result = await Neutralino.os.execCommand("ffmpeg -codecs");
|
||||
const rawCodecList = result.stdOut
|
||||
.substring(result.stdOut.indexOf(seperator) + seperator.length)
|
||||
.split("\n");
|
||||
let codecs = [];
|
||||
let vcodecs = [];
|
||||
let acodecs = [];
|
||||
|
||||
for (let codec of rawCodecList) {
|
||||
codec = codec.trim();
|
||||
const flags = codec.substring(0, 6);
|
||||
|
||||
if (!videoEncodingSupported.test(flags)) {
|
||||
continue;
|
||||
}
|
||||
if (flags[1] !== "E") continue;
|
||||
|
||||
const nameAndDescription = codec
|
||||
.substring(7)
|
||||
@@ -48,15 +51,49 @@ export async function getAvailableCodecs(): Promise<CodecInfo[]> {
|
||||
.split(" ");
|
||||
}
|
||||
|
||||
codecs.push({
|
||||
flags,
|
||||
shortName,
|
||||
description,
|
||||
encoders,
|
||||
});
|
||||
if (flags[2] === "V") {
|
||||
vcodecs.push({
|
||||
flags,
|
||||
shortName,
|
||||
description,
|
||||
encoders,
|
||||
});
|
||||
} else if (flags[2] === "A") {
|
||||
acodecs.push({
|
||||
flags,
|
||||
shortName,
|
||||
description,
|
||||
encoders,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return codecs;
|
||||
return {
|
||||
vcodecs,
|
||||
acodecs
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPixelFormats(): Promise<string[]> {
|
||||
const seperator = "-----";
|
||||
const result = await Neutralino.os.execCommand("ffmpeg -pix_fmts");
|
||||
const rawFormatList = result.stdOut
|
||||
.substring(result.stdOut.indexOf(seperator) + seperator.length)
|
||||
.split("\n");
|
||||
let outputFormats = [];
|
||||
|
||||
for (let format of rawFormatList) {
|
||||
format = format.trim();
|
||||
const flags = format.substring(0, 5);
|
||||
|
||||
if (flags[1] !== "O") continue;
|
||||
|
||||
const parts = format.substring(6).split(/ +/);
|
||||
|
||||
outputFormats.push(parts[0]);
|
||||
}
|
||||
|
||||
return outputFormats;
|
||||
}
|
||||
|
||||
export function playFile(path: string) {
|
||||
@@ -99,6 +136,7 @@ export interface FFmpegParams {
|
||||
faststart?: boolean;
|
||||
doNotUseAn?: boolean;
|
||||
speed?: number;
|
||||
pixelFormat?: string;
|
||||
/**
|
||||
* Extra parameters defined by users
|
||||
*/
|
||||
@@ -106,9 +144,11 @@ export interface FFmpegParams {
|
||||
/**
|
||||
* Extra output parameters defined by Vencoder
|
||||
*/
|
||||
outputopts?: { [key: string]: string };
|
||||
outputopts?: { [key: string]: string | undefined };
|
||||
}
|
||||
|
||||
export type FFmpegParamChangedFunc = <K extends keyof FFmpegParams>(key: K, value: FFmpegParams[K]) => void;
|
||||
|
||||
const NULL_LOCATION = window.NL_OS === "Windows" ? "NUL" : "/dev/null";
|
||||
|
||||
/**
|
||||
@@ -117,13 +157,21 @@ const NULL_LOCATION = window.NL_OS === "Windows" ? "NUL" : "/dev/null";
|
||||
*/
|
||||
export const DEFAULT_BITRATE = 12000;
|
||||
|
||||
function quickSyncVp9Command(params: FFmpegParams, opts: {
|
||||
global: string,
|
||||
input: string,
|
||||
output: string,
|
||||
}) {
|
||||
return `ffmpeg -init_hw_device qsv=hw -filter_hw_device hw ${opts.global}${opts.input} -i "${params.inputFile ?? "{fileName}"}" -vf hwupload=extra_hw_frames=64,format=qsv -c:v vp9_qsv -c:a libopus${opts.output} -progress - "${params.outputFile ?? "{output}"}"`;
|
||||
}
|
||||
|
||||
export function generateOutputCommand(params: FFmpegParams) {
|
||||
let faststart =
|
||||
params.faststart && params.vcodec === "h264"
|
||||
? " -movflags +faststart"
|
||||
: "";
|
||||
|
||||
let globalopts = "-hwaccel auto -y";
|
||||
let globalopts = `-hwaccel ${params.hwaccel ?? "auto"} -y`;
|
||||
let inputopts =
|
||||
params.useropts.input !== "" ? " " + params.useropts.input : "";
|
||||
let outputopts =
|
||||
@@ -133,41 +181,50 @@ export function generateOutputCommand(params: FFmpegParams) {
|
||||
globalopts += " " + params.useropts.global;
|
||||
}
|
||||
|
||||
if (params.pixelFormat) {
|
||||
if (params.outputopts === undefined) {
|
||||
params.outputopts = {};
|
||||
}
|
||||
|
||||
params.outputopts = {
|
||||
"pix_fmt": params.pixelFormat
|
||||
};
|
||||
}
|
||||
|
||||
if (params.outputopts !== undefined) {
|
||||
console.log(params.outputopts);
|
||||
for (const key of Object.keys(params.outputopts)) {
|
||||
if (params.outputopts[key] === undefined) continue;
|
||||
|
||||
outputopts += ` -${key} ${params.outputopts[key]}`.trimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
if (params.twopass) {
|
||||
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 -${outputopts}`;
|
||||
|
||||
return `ffmpeg ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=1" : "-pass 1"} ${
|
||||
params.doNotUseAn ? "-vsync cfr" : "-an"
|
||||
} -f null ${NULL_LOCATION} &&
|
||||
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}"}"`;
|
||||
if (params.encoder === "vp9_qsv") {
|
||||
return quickSyncVp9Command(params, {
|
||||
global: globalopts,
|
||||
input: inputopts,
|
||||
output: outputopts
|
||||
})
|
||||
}
|
||||
|
||||
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}`
|
||||
}${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 -${outputopts} "${params.outputFile ?? "{output}"}"`;
|
||||
if (params.twopass) {
|
||||
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 -${outputopts}`;
|
||||
|
||||
return `ffmpeg ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=1" : "-pass 1"} ${params.doNotUseAn ? "-vsync cfr" : "-an"
|
||||
} -f null ${NULL_LOCATION} &&
|
||||
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 ${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}k`
|
||||
}${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 -${outputopts} "${params.outputFile ?? "{output}"}"`;
|
||||
}
|
||||
|
||||
export async function getLengthMicroseconds(target: string) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Neutralino from "@neutralinojs/lib";
|
||||
|
||||
export function getTemporaryFilePath() {
|
||||
switch (window.NL_OS) {
|
||||
case "Windows":
|
||||
@@ -8,3 +10,12 @@ export function getTemporaryFilePath() {
|
||||
return ".";
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVencoderFolder() {
|
||||
switch (window.NL_OS) {
|
||||
case "Linux":
|
||||
return `${await Neutralino.os.getEnv("HOME")}/Vencoder/`;
|
||||
case "Windows":
|
||||
return `${await Neutralino.os.getEnv("HOMEPATH")}\\Vencoder\\`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { defineConfig } from "vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
plugins: [solid(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
|
||||
Reference in New Issue
Block a user