13 Commits

Author SHA1 Message Date
linesofcodes 8660bc9967 Fixes progress report
Build / build (push) Successful in 1m35s
2025-10-04 14:28:00 +07:00
linesofcodes 86847cbeca [Experimental] Synchronously process files
Build / build (push) Successful in 1m46s
2025-10-04 13:52:54 +07:00
linesofcodes 3be7cd2407 Fought with TypeScript again
Build / build (push) Successful in 1m13s
2025-10-02 16:01:14 +07:00
linesofcodes 64cfdce699 H.264 & H.265 QuickSync
Build / build (push) Has been cancelled
2025-10-02 15:59:39 +07:00
linesofcodes a949e5d46b Fought with TypeScript
Build / build (push) Successful in 2m57s
2025-10-01 21:32:15 +07:00
linesofcodes 958ecdca7d SVT-AV1 & TailwindCSS
Build / build (push) Failing after 1m40s
2025-10-01 21:06:39 +07:00
linesofcodes f1da312b95 Make app more Linux package manager friendly
Build / build (push) Successful in 1m39s
2025-09-23 18:36:35 +07:00
linesofcodes 37568aa0d1 Pixel Format & Audio Encoder fix
Build / build (push) Successful in 1m14s
2025-09-22 00:24:52 +07:00
linesofcodes 466a7cedca Audio Encoders
Build / build (push) Successful in 1m27s
2025-09-21 16:00:38 +07:00
linesofcodes 77e91fde1c Updated Neutralino.js 2025-09-21 12:05:51 +07:00
linesofcodes 74cebbf180 Audio Codecs & Slightly Improve UX
Build / build (push) Successful in 1m28s
2025-09-21 10:56:22 +07:00
linesofcodes 68d919ab1e Update packages & vp9_qsv
Build / build (push) Successful in 1m28s
2025-09-15 12:38:26 +07:00
linesofcodes d586a8f222 Fix output builds
Build / build (push) Successful in 1m26s
2025-08-26 20:08:04 +07:00
25 changed files with 2088 additions and 1128 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ jobs:
- name: Package application - name: Package application
run: | run: |
cd ${{ github.workspace }} cd ${{ github.workspace }}
wget https://staticlines.dailitation.xyz/neutralinojs-v6.2.0.zip wget https://staticlines.dailitation.xyz/neutralinojs-v6.3.0.zip
unzip neutralinojs-v6.2.0.zip -d bin/ unzip neutralinojs-v6.3.0.zip -d bin/
pnpx @neutralinojs/neu build pnpx @neutralinojs/neu build
- name: Upload artifacts - name: Upload artifacts
uses: ChristopherHX/gitea-upload-artifact@v4 uses: ChristopherHX/gitea-upload-artifact@v4
+10
View File
@@ -0,0 +1,10 @@
frontend:
cd solid-src; \
pnpm build
build: frontend
neu build
release: frontend
neu build --clean -r --embed-resources
+8 -14
View File
@@ -48,7 +48,7 @@ encoders supported by your FFmpeg install will show up.
- [ ] AV1 - [ ] AV1
- [x] libaom-av1 - [x] libaom-av1
- [x] librav1e (Partial support) - [x] librav1e (Partial support)
- [ ] libsvtav1 - [x] libsvtav1
- [ ] av1_amf - [ ] av1_amf
- [ ] av1_nvenc - [ ] av1_nvenc
- [ ] av1_qsv - [ ] av1_qsv
@@ -59,26 +59,20 @@ encoders supported by your FFmpeg install will show up.
- [x] libx264rgb (Untested, but _should_ work) - [x] libx264rgb (Untested, but _should_ work)
- [ ] h264_amf - [ ] h264_amf
- [ ] h264_nvenc - [ ] h264_nvenc
- [ ] h264_qsv - [x] h264_qsv
- [ ] h264_v4l2m2m
- [ ] h264_vaapi - [ ] h264_vaapi
- [ ] h264_vulkan - [ ] h264_vulkan
- [ ] H.265 - [ ] H.265
- [x] libx265 - [x] libx265
- [ ] h264_amf - [ ] h265_amf
- [ ] h264_nvenc - [ ] h265_nvenc
- [ ] h264_qsv - [x] h265_qsv
- [ ] h264_v4l2m2m - [ ] h265_vaapi
- [ ] h264_vaapi - [ ] h265_vulkan
- [ ] h264_vulkan
- [ ] VP8
- [ ] libvpx
- [ ] vp8_v4l2m2m
- [ ] vp8_vaapi
- [ ] VP9 - [ ] VP9
- [ ] libvpx-vp9 - [ ] libvpx-vp9
- [ ] vp9_vaapi - [ ] vp9_vaapi
- [ ] vp9_qsv - [x] vp9_qsv (Really Basic)
## Gitea Actions ## Gitea Actions
+7
View File
@@ -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
View File
@@ -1,10 +1,11 @@
{ {
"$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json", "$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
"applicationId": "xyz.dailitation.linesofcodes.vencoder", "applicationId": "xyz.dailitation.linesofcodes.vencoder",
"version": "1.0.0", "version": "0.1.1",
"defaultMode": "window", "defaultMode": "window",
"documentRoot": "/solid-src/dist/", "documentRoot": "/solid-src/dist/",
"url": "/", "url": "/",
"port": 5432,
"enableServer": true, "enableServer": true,
"enableNativeAPI": true, "enableNativeAPI": true,
"singlePageServe": true, "singlePageServe": true,
@@ -17,23 +18,29 @@
"storage.*", "storage.*",
"debug.log" "debug.log"
], ],
"dataLocation": "system",
"storageLocation": "system",
"logging": {
"writeToLogFile": false
},
"modes": { "modes": {
"window": { "window": {
"title": "Vencoder", "title": "Vencoder",
"width": 800, "width": 1280,
"height": 600, "height": 720,
"minWidth": 600, "minWidth": 600,
"minHeight": 400, "minHeight": 400,
"icon": "/solid-src/public/vite.svg", "icon": "/solid-src/public/vite.svg",
"enableInspector": true "enableInspector": true,
"openInspectorOnStartup": false
} }
}, },
"cli": { "cli": {
"binaryName": "vencoder", "binaryName": "vencoder",
"resourcesPath": "/solid-src/dist/", "resourcesPath": "/solid-src/dist/",
"extensionsPath": "/extensions/", "extensionsPath": "/extensions/",
"binaryVersion": "6.2.0", "binaryVersion": "6.3.0",
"clientVersion": "6.2.0", "clientVersion": "6.3.0",
"frontendLibrary": { "frontendLibrary": {
"patchFile": "/solid-src/index.html", "patchFile": "/solid-src/index.html",
"devUrl": "http://localhost:5173" "devUrl": "http://localhost:5173"
+1 -1
View File
@@ -5,7 +5,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> <title>Vencoder</title>
</head> </head>
+9 -7
View File
@@ -9,17 +9,19 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@neutralinojs/lib": "^6.2.0", "@neutralinojs/lib": "^6.3.0",
"@solidjs/router": "^0.15.3", "@solidjs/router": "^0.15.3",
"color-convert": "^3.1.0", "@tailwindcss/vite": "^4.1.14",
"solid-js": "^1.9.9" "color-convert": "^3.1.2",
"solid-js": "^1.9.9",
"tailwindcss": "^4.1.14"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.3.0", "@types/node": "^24.6.2",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"vite": "^7.1.2", "vite": "^7.1.9",
"vite-plugin-solid": "^2.11.8" "vite-plugin-solid": "^2.11.9"
}, },
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748" "packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a"
} }
+728 -333
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,2 +1,3 @@
onlyBuiltDependencies: onlyBuiltDependencies:
- '@tailwindcss/oxide'
- esbuild - esbuild
+165 -184
View File
@@ -14,25 +14,25 @@ import {
generateOutputCommand, generateOutputCommand,
getAvailableCodecs, getAvailableCodecs,
getLengthMicroseconds, getLengthMicroseconds,
getPixelFormats,
playFile, playFile,
videoFileExtensions, videoFileExtensions,
type CodecInfo, type CodecInfo,
type CodecList,
type FFmpegParams, type FFmpegParams,
} from "./util/ffmpeg"; } from "./util/ffmpeg";
import Neutralino from "@neutralinojs/lib"; import Neutralino from "@neutralinojs/lib";
import H264Options from "./components/H264Options"; import H264Options from "./components/H264Options";
import { openFile } from "./util/oshelper"; import { getVencoderFolder } from "./util/path";
import { getTemporaryFilePath } from "./util/path";
import { generateRandomString } from "./util/string";
import "./css/icons.css"; import "./css/icons.css";
import BreezeIcon from "./components/BreezeIcon"; import BreezeIcon from "./components/BreezeIcon";
import AV1Options from "./components/AV1Options"; import AV1Options from "./components/AV1Options";
import DNxHDOptions from "./components/DNxHDOptions"; 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 { interface FileQueueItem {
process: Neutralino.os.SpawnedProcess; command: string;
file: string; file: string;
length: number; length: number;
} }
@@ -41,6 +41,7 @@ function App() {
const [windowFocused, setWindowFocused] = createSignal(true); const [windowFocused, setWindowFocused] = createSignal(true);
const [displayedCodecs, setDisplayedCodecs]: Signal<CodecInfo[]> = const [displayedCodecs, setDisplayedCodecs]: Signal<CodecInfo[]> =
createSignal([] as CodecInfo[]); createSignal([] as CodecInfo[]);
const [audioCodecList, setAudioCodecList] = createSignal([] as CodecInfo[]);
const [fileList, setFileList] = createSignal([] as string[]); const [fileList, setFileList] = createSignal([] as string[]);
const [selectedClip, setSelectedClip] = createSignal(""); const [selectedClip, setSelectedClip] = createSignal("");
const [outputCommand, setOutputCommand] = createSignal( const [outputCommand, setOutputCommand] = createSignal(
@@ -49,15 +50,15 @@ function App() {
const [showCommonCodecs, setShowCommonCodecs] = createSignal(true); const [showCommonCodecs, setShowCommonCodecs] = createSignal(true);
const [selectedCodec, setSelectedCodec] = createSignal<CodecInfo>(); const [selectedCodec, setSelectedCodec] = createSignal<CodecInfo>();
const [selectedEncoder, setSelectedEncoder] = createSignal(""); const [selectedEncoder, setSelectedEncoder] = createSignal("");
const [runningProcesses, setRunningProcesses] = createSignal<
RunningProcessInfo[]
>([]);
const [customFileExt, setCustomFileExt] = createSignal(""); const [customFileExt, setCustomFileExt] = createSignal("");
const [globalopts, setGlobalopts] = createSignal(""); const [globalopts, setGlobalopts] = createSignal("");
const [inputopts, setInputopts] = createSignal(""); const [inputopts, setInputopts] = createSignal("");
const [outputopts, setOutputopts] = createSignal(""); const [outputopts, setOutputopts] = createSignal("");
const logs: { [id: number]: string[] } = {}; const [audioCodec, setAudioCodec] = createSignal("copy");
let supportedCodecs: CodecInfo[] = []; const [audioEncoder, setAudioEncoder] = createSignal("");
const [pixelFormatList, setPixelFormatList] = createSignal([] as string[]);
const [pixelFormat, setPixelFormat] = createSignal("");
let supportedCodecs: CodecList = { vcodecs: [], acodecs: [] };
let ffmpegParams: FFmpegParams = { let ffmpegParams: FFmpegParams = {
vcodec: "", vcodec: "",
useropts: { useropts: {
@@ -66,9 +67,6 @@ function App() {
output: "", output: "",
}, },
}; };
let successfulCount = 0;
let unsuccessfulCount = 0;
let totalCount = 0;
function windowIsFocused() { function windowIsFocused() {
setWindowFocused(true); setWindowFocused(true);
@@ -78,55 +76,13 @@ function App() {
setWindowFocused(false); 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 () => { onMount(async () => {
events.on("windowFocus", windowIsFocused); events.on("windowFocus", windowIsFocused);
events.on("windowBlur", windowUnfocused); events.on("windowBlur", windowUnfocused);
events.on("spawnedProcess", handleSpawnedProcessEvents);
supportedCodecs = await getAvailableCodecs(); supportedCodecs = await getAvailableCodecs();
filterDisplayedCodecs(); filterDisplayedCodecs();
setAudioCodecList(supportedCodecs.acodecs);
const firstCodec = displayedCodecs()[0]; const firstCodec = displayedCodecs()[0];
@@ -134,12 +90,13 @@ function App() {
ffmpegParams.encoder = firstCodec.encoders[0]; ffmpegParams.encoder = firstCodec.encoders[0];
setSelectedCodec(firstCodec); setSelectedCodec(firstCodec);
setSelectedEncoder(firstCodec.encoders[0]); setSelectedEncoder(firstCodec.encoders[0]);
setPixelFormatList(await getPixelFormats());
}); });
onCleanup(() => { onCleanup(() => {
events.off("windowFocus", windowIsFocused); events.off("windowFocus", windowIsFocused);
events.off("windowBlur", windowUnfocused); events.off("windowBlur", windowUnfocused);
events.off("spawnedProcess", handleSpawnedProcessEvents);
}); });
function removeBtnClicked() { function removeBtnClicked() {
@@ -180,12 +137,14 @@ function App() {
function filterDisplayedCodecs() { function filterDisplayedCodecs() {
if (showCommonCodecs()) { if (showCommonCodecs()) {
setDisplayedCodecs( setDisplayedCodecs(
supportedCodecs.filter((v) => commonCodecs.has(v.shortName)), supportedCodecs.vcodecs.filter((v) =>
commonCodecs.has(v.shortName),
),
); );
return; return;
} }
setDisplayedCodecs(supportedCodecs); setDisplayedCodecs(supportedCodecs.vcodecs);
} }
function showCommonCodecsChanged(e: InputEvent) { function showCommonCodecsChanged(e: InputEvent) {
@@ -200,30 +159,47 @@ function App() {
(v) => v.shortName === newValue, (v) => v.shortName === newValue,
); );
if (newValue !== "h264" && newValue !== "hevc") { let encoder = newValue;
ffmpegParams.twopass = false; if (codecObj?.encoders.length !== 0) {
encoder = codecObj?.encoders[0] ?? "";
}
setSelectedCodec(codecObj);
setSelectedEncoder(encoder);
} }
createEffect(() => {
ffmpegParams = { ffmpegParams = {
vcodec: codecObj?.shortName ?? "", vcodec: selectedCodec()?.shortName ?? "",
encoder: selectedEncoder(),
useropts: { useropts: {
global: "", global: "",
input: "", input: "",
output: "", output: "",
}, },
}; };
});
let encoder = newValue; function getAudioEncoders() {
if (codecObj?.encoders.length !== 0) { const codec = audioCodec();
encoder = codecObj?.encoders[0] ?? ""; let encoders = audioCodecList().find(
} (v) => v.shortName === codec,
ffmpegParams.encoder = encoder; )?.encoders;
setSelectedCodec(codecObj);
setSelectedEncoder(encoder); if (encoders) {
setAudioEncoder(encoders[0]);
} }
function onParametersChanged(key: string, value: any) { if (encoders instanceof Array && encoders.length === 0) {
// @ts-ignore encoders = undefined;
}
return encoders;
}
function onParametersChanged<K extends keyof FFmpegParams>(
key: K,
value: any,
) {
ffmpegParams[key] = value; ffmpegParams[key] = value;
setOutputCommand(generateOutputCommand(ffmpegParams)); setOutputCommand(generateOutputCommand(ffmpegParams));
} }
@@ -235,6 +211,7 @@ function App() {
x: 120, x: 120,
y: 120, y: 120,
injectGlobals: true, injectGlobals: true,
processArgs: "--port=5434",
}); });
} }
@@ -245,10 +222,18 @@ function App() {
encoder = undefined; encoder = undefined;
} }
let acodec = audioEncoder();
if (acodec === undefined || acodec === "") {
acodec = audioCodec();
}
const pixFmt = pixelFormat();
ffmpegParams = { ffmpegParams = {
vcodec: selectedCodec()?.shortName ?? "", vcodec: selectedCodec()?.shortName ?? "",
encoder, encoder,
acodec: ffmpegParams.acodec, acodec,
abitrate: ffmpegParams.abitrate, abitrate: ffmpegParams.abitrate,
crf: ffmpegParams.crf, crf: ffmpegParams.crf,
doNotUseAn: ffmpegParams.doNotUseAn, doNotUseAn: ffmpegParams.doNotUseAn,
@@ -263,6 +248,7 @@ function App() {
input: inputopts(), input: inputopts(),
output: outputopts(), output: outputopts(),
}, },
pixelFormat: pixFmt === "" ? undefined : pixFmt,
}; };
setOutputCommand(generateOutputCommand(ffmpegParams)); setOutputCommand(generateOutputCommand(ffmpegParams));
@@ -270,7 +256,7 @@ function App() {
async function convertClip( async function convertClip(
clip: string, clip: string,
): Promise<RunningProcessInfo | undefined> { ): Promise<FileQueueItem | undefined> {
ffmpegParams.inputFile = clip; ffmpegParams.inputFile = clip;
const fileName = (await Neutralino.filesystem.getPathParts(clip)).stem; const fileName = (await Neutralino.filesystem.getPathParts(clip)).stem;
@@ -282,14 +268,7 @@ function App() {
? videoFileExtensions[selectedCodec()?.shortName ?? ""] ? videoFileExtensions[selectedCodec()?.shortName ?? ""]
: customExt; : customExt;
switch (window.NL_OS) { ffmpegParams.outputFile = `${await getVencoderFolder()}${fileName}.${fileExt}`;
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;
}
const outputDir = ( const outputDir = (
await Neutralino.filesystem.getPathParts( await Neutralino.filesystem.getPathParts(
@@ -308,8 +287,8 @@ function App() {
const userAnswer = await Neutralino.os.showMessageBox( const userAnswer = await Neutralino.os.showMessageBox(
"File already exists", "File already exists",
`A file at ${ffmpegParams.outputFile} already exists. Would you like to overwrite it?`, `A file at ${ffmpegParams.outputFile} already exists. Would you like to overwrite it?`,
Neutralino.os.MessageBoxChoice.YES_NO, Neutralino.MessageBoxChoice.YES_NO,
Neutralino.os.Icon.QUESTION, Neutralino.Icon.QUESTION,
); );
if (userAnswer === "NO") { if (userAnswer === "NO") {
@@ -320,9 +299,7 @@ function App() {
const length = await getLengthMicroseconds(clip); const length = await getLengthMicroseconds(clip);
return { return {
process: await Neutralino.os.spawnProcess( command: generateOutputCommand(ffmpegParams),
generateOutputCommand(ffmpegParams),
),
file: clip, file: clip,
length, length,
}; };
@@ -331,21 +308,21 @@ function App() {
async function convertAllClicked() { async function convertAllClicked() {
const list = fileList(); const list = fileList();
totalCount = list.length; const queue: FileQueueItem[] = [];
const processes = (await Promise.all(list.map(convertClip))).filter( for (const file of list) {
(v) => v !== undefined, const info = await convertClip(file);
);
setRunningProcesses(processes); if (info !== undefined) {
queue.push(info);
processes.forEach((v) => (logs[v.process.id] = [])); }
}
await Neutralino.storage.setData( await Neutralino.storage.setData(
"filesBeingProcessed", "filesBeingProcessed",
JSON.stringify( JSON.stringify(
processes.map((v) => ({ queue.map((v) => ({
id: v.process.id, com: v.command,
in: v.file, in: v.file,
len: v.length, len: v.length,
})), })),
@@ -359,7 +336,7 @@ function App() {
y: 120, y: 120,
injectGlobals: true, injectGlobals: true,
maximizable: false, maximizable: false,
enableInspector: false, processArgs: "--port=5433",
}); });
} }
@@ -372,17 +349,11 @@ function App() {
console.log(result); console.log(result);
totalCount = 1;
setRunningProcesses([result]);
logs[result.process.id] = [];
await Neutralino.storage.setData( await Neutralino.storage.setData(
"filesBeingProcessed", "filesBeingProcessed",
JSON.stringify([ JSON.stringify([
{ {
id: result.process.id, com: result.command,
in: result.file, in: result.file,
len: result.length, len: result.length,
}, },
@@ -401,9 +372,7 @@ function App() {
} }
return ( return (
<main class="row flex-col"> <main class="row">
<div class="container" style={{ flex: "1" }}>
<div class="row h-full">
<div class="row flex-col h-full"> <div class="row flex-col h-full">
<header <header
class={`k-page-header k-rborder ${windowFocused() ? "" : "window-blur"}`} class={`k-page-header k-rborder ${windowFocused() ? "" : "window-blur"}`}
@@ -419,13 +388,9 @@ function App() {
{(item, _) => ( {(item, _) => (
<li <li
class={ class={
item == selectedClip() item == selectedClip() ? "selected" : ""
? "selected"
: ""
}
onclick={() =>
setSelectedClip(item)
} }
onclick={() => setSelectedClip(item)}
> >
{item} {item}
</li> </li>
@@ -433,16 +398,10 @@ function App() {
</For> </For>
</ul> </ul>
<div class="row gap2"> <div class="row gap2">
<button <button onclick={openBtnClicked} class="k-button">
onclick={openBtnClicked}
class="k-button"
>
Open... Open...
</button> </button>
<button <button onclick={removeAllBtnClicked} class="k-button">
onclick={removeAllBtnClicked}
class="k-button"
>
Remove All Remove All
</button> </button>
<button <button
@@ -469,10 +428,7 @@ function App() {
class="icon-button k-button" class="icon-button k-button"
onclick={settingsBtnPressed} onclick={settingsBtnPressed}
> >
<BreezeIcon <BreezeIcon icon="configure" alt="Configure" />
icon="configure"
alt="Configure"
/>
</button> </button>
</div> </div>
</div> </div>
@@ -483,14 +439,7 @@ function App() {
> >
<div class="page-title">Conversion Settings</div> <div class="page-title">Conversion Settings</div>
</header> </header>
<div <div class="page-content">
class="col row flex-col"
style={{
padding:
"var(--k-grid-unit) var(--k-small-spacing)",
flex: "1",
}}
>
<div> <div>
<form <form
class="k-form" class="k-form"
@@ -535,56 +484,63 @@ function App() {
} }
placeholder="Leave blank to guess from codec" placeholder="Leave blank to guess from codec"
/> />
<Show <Show when={selectedCodec()?.encoders.length !== 0}>
when={ <label for="videoEncoder">Encoder</label>
selectedCodec()?.encoders.length !==
0
}
>
<label for="videoEncoder">
Encoder
</label>
<select <select
name="videoEncoder" name="videoEncoder"
id="videoEncoder" id="videoEncoder"
class="k-dropdown" class="k-dropdown"
value={selectedEncoder()} value={selectedEncoder()}
oninput={(e) => oninput={(e) =>
setSelectedEncoder( setSelectedEncoder(e.target.value)
e.target.value,
)
} }
> >
<For <For each={selectedCodec()?.encoders}>
each={selectedCodec()?.encoders} {(item, _) => <option>{item}</option>}
>
{(item, _) => (
<option>{item}</option>
)}
</For> </For>
</select> </select>
</Show> </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> </form>
<Switch fallback={<div></div>}> <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 <Match
when={ when={
selectedCodec()?.shortName === selectedCodec()?.shortName === "h264" ||
"h264" || selectedCodec()?.shortName === "hevc"
selectedCodec()?.shortName ===
"hevc"
} }
> >
<H264Options <H264Options
codec={selectedCodec()} codec={selectedCodec()}
encoder={selectedEncoder()}
params={ffmpegParams} params={ffmpegParams}
onParamChanged={onParametersChanged} onParamChanged={onParametersChanged}
/> />
</Match> </Match>
<Match <Match when={selectedCodec()?.shortName === "av1"}>
when={
selectedCodec()?.shortName === "av1"
}
>
<AV1Options <AV1Options
codec={selectedCodec()} codec={selectedCodec()}
encoder={selectedEncoder()} encoder={selectedEncoder()}
@@ -593,10 +549,7 @@ function App() {
/> />
</Match> </Match>
<Match <Match
when={ when={selectedCodec()?.shortName === "dnxhd"}
selectedCodec()?.shortName ===
"dnxhd"
}
> >
<DNxHDOptions <DNxHDOptions
codec={selectedCodec()} codec={selectedCodec()}
@@ -605,6 +558,44 @@ function App() {
/> />
</Match> </Match>
</Switch> </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"> <div class="row flex-col align-items-center">
<h3 class="k-form-section-title"> <h3 class="k-form-section-title">
Extra Arguments Extra Arguments
@@ -614,9 +605,7 @@ function App() {
class="k-form" class="k-form"
onsubmit={(e) => e.preventDefault()} onsubmit={(e) => e.preventDefault()}
> >
<label for="globalopts"> <label for="globalopts">Global Options</label>
Global Options
</label>
<input <input
type="text" type="text"
name="globalopts" name="globalopts"
@@ -640,9 +629,7 @@ function App() {
setInputopts(e.target.value); setInputopts(e.target.value);
}} }}
/> />
<label for="outputopts"> <label for="outputopts">Output Options</label>
Output Options
</label>
<input <input
type="text" type="text"
name="outputopts" name="outputopts"
@@ -656,21 +643,16 @@ function App() {
/> />
</form> </form>
</div> </div>
<div class="row flex-col p-medium"> </div>
<footer class="k-page-footer row flex-col gap2">
<div class="row flex-col">
<label for="outputCommand">Command</label> <label for="outputCommand">Command</label>
<pre <pre id="outputCommand" class="k-text-field col">
id="outputCommand"
class="k-text-field w-full col"
>
{outputCommand()} {outputCommand()}
</pre> </pre>
</div> </div>
</div> <div class="row gap2">
<footer class="k-page-footer row gap2"> <button class="k-button" onclick={convertAllClicked}>
<button
class="k-button"
onclick={convertAllClicked}
>
Convert All Convert All
</button> </button>
<button <button
@@ -680,10 +662,9 @@ function App() {
> >
Convert Selected Convert Selected
</button> </button>
</div>
</footer> </footer>
</div> </div>
</div>
</div>
</main> </main>
); );
} }
+11 -3
View File
@@ -1,22 +1,30 @@
import { Match, Switch } from "solid-js"; 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 LibaomOptions from "./encoders/libaom";
import Librav1eOptions from "./encoders/librav1e"; import Librav1eOptions from "./encoders/librav1e";
import LibSvtAv1Options from "./encoders/libsvtav1";
function AV1Options(props: { function AV1Options(props: {
codec: CodecInfo | undefined; codec: CodecInfo | undefined;
encoder: string; encoder: string;
params: FFmpegParams; params: FFmpegParams;
onParamChanged: (key: string, value: any) => void; onParamChanged: FFmpegParamChangedFunc;
}) { }) {
return ( return (
<Switch fallback={<div>No options.</div>}> <Switch fallback={<div class="text-center mt-4">No options.</div>}>
<Match when={props.encoder === "libaom-av1"}> <Match when={props.encoder === "libaom-av1"}>
<LibaomOptions {...props} /> <LibaomOptions {...props} />
</Match> </Match>
<Match when={props.encoder === "librav1e"}> <Match when={props.encoder === "librav1e"}>
<Librav1eOptions {...props} /> <Librav1eOptions {...props} />
</Match> </Match>
<Match when={props.encoder === "libsvtav1"}>
<LibSvtAv1Options {...props} />
</Match>
</Switch> </Switch>
); );
} }
+8 -11
View File
@@ -1,5 +1,9 @@
import { os } from "@neutralinojs/lib"; 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"; import BreezeIcon from "./BreezeIcon";
/** /**
@@ -8,21 +12,15 @@ import BreezeIcon from "./BreezeIcon";
function DNxHDOptions(props: { function DNxHDOptions(props: {
codec: CodecInfo | undefined; codec: CodecInfo | undefined;
params: FFmpegParams; params: FFmpegParams;
onParamChanged: (key: string, value: any) => void; onParamChanged: FFmpegParamChangedFunc;
}) { }) {
return ( return (
<section id="commonLossyOptions"> <section id="commonLossyOptions" class="k-form">
<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> <label>Help</label>
<div> <div>
<button <button
class="icon-button" class="icon-button"
onclick={() => onclick={() => os.open("https://askubuntu.com/a/907515")}
os.open("https://askubuntu.com/a/907515")
}
title="DNxHD is a picky encoder." title="DNxHD is a picky encoder."
> >
<BreezeIcon icon="help-about" alt="Help" /> <BreezeIcon icon="help-about" alt="Help" />
@@ -47,7 +45,6 @@ function DNxHDOptions(props: {
<option value="dnxhr_sq">DNxHR SQ</option> <option value="dnxhr_sq">DNxHR SQ</option>
<option value="dnxhr_lb">DNxHR LB</option> <option value="dnxhr_lb">DNxHR LB</option>
</select> </select>
</div>
</section> </section>
); );
} }
+20 -149
View File
@@ -1,169 +1,40 @@
import { createSignal, Show } from "solid-js"; import { Match, Switch } from "solid-js";
import { import {
DEFAULT_BITRATE,
type CodecInfo, type CodecInfo,
type FFmpegParamChangedFunc,
type FFmpegParams, type FFmpegParams,
} from "../util/ffmpeg"; } from "../util/ffmpeg";
import { os } from "@neutralinojs/lib"; import LibH26xOptions from "./encoders/libx264";
import BreezeIcon from "./BreezeIcon"; import H264QsvOptions from "./encoders/h264qsv";
const information = {
h264: {
defaultCrf: 23,
},
hevc: {
defaultCrf: 28,
},
};
/** /**
* Options for H.264/H.265 codecs * Options for H.264/H.265 codecs
*/ */
function H264Options(props: { function H264Options(props: {
codec: CodecInfo | undefined; codec: CodecInfo | undefined;
encoder: string;
params: FFmpegParams; 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 ( return (
<section id="commonLossyOptions"> <Switch fallback={<div class="text-center mt-4">No options.</div>}>
<div class="row flex-col align-items-center"> <Match
<h3 class="k-form-section-title">Encoder Options</h3> when={
</div> props.encoder === "libx264" ||
<div class="k-form"> props.encoder === "libx264rgb" ||
<div></div> props.encoder === "libx265"
<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> <LibH26xOptions {...props} />
<div> </Match>
<input <Match
type="number" when={
name="bitrate" props.encoder === "h264_qsv" || props.encoder === "hevc_qsv"
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" /> <H264QsvOptions {...props} />
</button> </Match>
</div> </Switch>
</Show>
</div>
</section>
); );
} }
+145
View File
@@ -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;
+4 -11
View File
@@ -1,6 +1,7 @@
import { import {
DEFAULT_BITRATE, DEFAULT_BITRATE,
type CodecInfo, type CodecInfo,
type FFmpegParamChangedFunc,
type FFmpegParams, type FFmpegParams,
} from "@/util/ffmpeg"; } from "@/util/ffmpeg";
import { os } from "@neutralinojs/lib"; import { os } from "@neutralinojs/lib";
@@ -12,7 +13,7 @@ const DEFAULT_CRF = 23;
function LibaomOptions(props: { function LibaomOptions(props: {
codec: CodecInfo | undefined; codec: CodecInfo | undefined;
params: FFmpegParams; params: FFmpegParams;
onParamChanged: (key: string, value: any) => void; onParamChanged: FFmpegParamChangedFunc;
}) { }) {
const [rateControlMode, setRateControlMode] = createSignal("Constant"); const [rateControlMode, setRateControlMode] = createSignal("Constant");
@@ -45,11 +46,7 @@ function LibaomOptions(props: {
}); });
return ( return (
<section id="encoderOptions"> <section id="encoderOptions" class="k-form">
<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> <label>Help</label>
<div> <div>
<button <button
@@ -91,10 +88,7 @@ function LibaomOptions(props: {
max="63" max="63"
value={props.params.crf ?? DEFAULT_CRF} value={props.params.crf ?? DEFAULT_CRF}
oninput={(e) => { oninput={(e) => {
props.onParamChanged( props.onParamChanged("crf", parseInt(e.target.value));
"crf",
parseInt(e.target.value),
);
}} }}
/> />
</Show> </Show>
@@ -117,7 +111,6 @@ function LibaomOptions(props: {
<span>Kbps</span> <span>Kbps</span>
</div> </div>
</Show> </Show>
</div>
</section> </section>
); );
} }
+8 -10
View File
@@ -1,6 +1,7 @@
import { import {
DEFAULT_BITRATE, DEFAULT_BITRATE,
type CodecInfo, type CodecInfo,
type FFmpegParamChangedFunc,
type FFmpegParams, type FFmpegParams,
} from "@/util/ffmpeg"; } from "@/util/ffmpeg";
import { os } from "@neutralinojs/lib"; import { os } from "@neutralinojs/lib";
@@ -10,10 +11,9 @@ import { onMount } from "solid-js";
function Librav1eOptions(props: { function Librav1eOptions(props: {
codec: CodecInfo | undefined; codec: CodecInfo | undefined;
params: FFmpegParams; params: FFmpegParams;
onParamChanged: (key: string, value: any) => void; onParamChanged: FFmpegParamChangedFunc;
}) { }) {
onMount(() => { onMount(() => {
props.onParamChanged("crf", undefined);
props.onParamChanged( props.onParamChanged(
"vbitrate", "vbitrate",
props.params.vbitrate ?? DEFAULT_BITRATE, props.params.vbitrate ?? DEFAULT_BITRATE,
@@ -22,11 +22,7 @@ function Librav1eOptions(props: {
}); });
return ( return (
<section id="encoderOptions"> <section id="encoderOptions" class="k-form">
<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> <label>Help</label>
<div> <div>
<button <button
@@ -50,7 +46,7 @@ function Librav1eOptions(props: {
max="10" max="10"
value={props.params.speed ?? 5} value={props.params.speed ?? 5}
oninput={(e) => oninput={(e) =>
props.onParamChanged("speed", e.target.value) props.onParamChanged("speed", parseInt(e.target.value))
} }
/> />
<label for="bitrate">Bitrate</label> <label for="bitrate">Bitrate</label>
@@ -61,12 +57,14 @@ function Librav1eOptions(props: {
id="bitrate" id="bitrate"
value={props.params.vbitrate ?? DEFAULT_BITRATE} value={props.params.vbitrate ?? DEFAULT_BITRATE}
oninput={(e) => oninput={(e) =>
props.onParamChanged("vbitrate", e.target.value) props.onParamChanged(
"vbitrate",
parseInt(e.target.value),
)
} }
/> />
<span>Kbps</span> <span>Kbps</span>
</div> </div>
</div>
</section> </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;
+5
View File
@@ -1,5 +1,7 @@
/* An attempt of imitating KDE's Kirigami UI Framework */ /* An attempt of imitating KDE's Kirigami UI Framework */
@reference "./index.css";
:root { :root {
--k-grid-unit: 16px; --k-grid-unit: 16px;
--k-small-spacing: 4px; --k-small-spacing: 4px;
@@ -97,6 +99,7 @@ button {
border-radius: var(--k-border-radius); border-radius: var(--k-border-radius);
box-shadow: var(--k-border-color) 0 1px; box-shadow: var(--k-border-color) 0 1px;
background-color: var(--k-primary-highlight); background-color: var(--k-primary-highlight);
padding: 2px 6px;
font-size: inherit; font-size: inherit;
&:hover { &:hover {
@@ -123,6 +126,7 @@ button {
padding: var(--k-small-spacing) calc(var(--k-medium-spacing) + 12px) padding: var(--k-small-spacing) calc(var(--k-medium-spacing) + 12px)
var(--k-small-spacing) var(--k-medium-spacing); var(--k-small-spacing) var(--k-medium-spacing);
border: 1px solid var(--k-border-color); border: 1px solid var(--k-border-color);
border-radius: var(--k-border-radius);
box-shadow: var(--k-border-color) 0 1px; box-shadow: var(--k-border-color) 0 1px;
max-width: 16em; max-width: 16em;
font-size: inherit; font-size: inherit;
@@ -177,6 +181,7 @@ input[type="number"] {
border-bottom: 1px solid var(--k-border-color); border-bottom: 1px solid var(--k-border-color);
width: fit-content; width: fit-content;
margin-bottom: var(--k-medium-spacing); margin-bottom: var(--k-medium-spacing);
@apply text-xl font-bold mt-4;
} }
.k-page-footer { .k-page-footer {
+16
View File
@@ -1,3 +1,5 @@
@import "tailwindcss";
:root { :root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
--system-accent-color: accentcolor; --system-accent-color: accentcolor;
@@ -92,3 +94,17 @@ h2 {
gap: var(--k-medium-spacing); gap: var(--k-medium-spacing);
align-items: center; 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);
}
+112 -38
View File
@@ -1,8 +1,11 @@
import { events, os, storage } from "@neutralinojs/lib"; import { openFile } from "@/util/oshelper";
import { createSignal, onMount, onCleanup, Show, Index } from "solid-js"; 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 { interface TargetFile {
id: string; com: string;
in: string; in: string;
len: number; len: number;
} }
@@ -29,15 +32,21 @@ interface FFmpegProgressInfo {
function ProgressPage() { function ProgressPage() {
const [windowFocused, setWindowFocused] = createSignal(true); const [windowFocused, setWindowFocused] = createSignal(true);
const [runningProcesses, setRunningProcesses] = createSignal< const [runningProcesses, setRunningProcesses] = createSignal<
os.SpawnedProcess[] SpawnedProcess[]
>([]); >([]);
const [queueLength, setQueueLength] = createSignal(0);
const [finished, setFinished] = createSignal(false); const [finished, setFinished] = createSignal(false);
const [fileInfo, setFileInfo] = createSignal<TargetFile[]>([]);
const progressObject: { const progressObject: {
[key: string]: ProgressInfo; [key: string]: ProgressInfo;
} = {}; } = {};
const [progressList, setProgressList] = createSignal<ProgressInfo[]>([]); const [progressList, setProgressList] = createSignal<ProgressInfo[]>([]);
const [isCancelling, setIsCancelling] = createSignal(false); 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() { function windowIsFocused() {
setWindowFocused(false); setWindowFocused(false);
@@ -47,6 +56,26 @@ function ProgressPage() {
setWindowFocused(true); 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) { function handleSpawnedProcessEvents(evt: CustomEvent) {
switch (evt.detail.action) { switch (evt.detail.action) {
case "stdOut": case "stdOut":
@@ -55,14 +84,12 @@ function ProgressPage() {
.split("\n") .split("\n")
.map((v) => v.split("=")), .map((v) => v.split("=")),
); );
const file = fileInfo().find((v) => v.id === evt.detail.id); const file = filesBeingProcessed[evt.detail.id];
if (file === undefined) return; if (file === undefined) return;
progressObject[evt.detail.id] = { progressObject[evt.detail.id].percentage =
filename: file.in, (parseInt(info.out_time_us) / file.len) * 100;
percentage: (parseInt(info.out_time_us) / file.len) * 100,
};
if (Number.isNaN(progressObject[evt.detail.id].percentage)) { if (Number.isNaN(progressObject[evt.detail.id].percentage)) {
progressObject[evt.detail.id].percentage = 0; progressObject[evt.detail.id].percentage = 0;
@@ -71,17 +98,55 @@ function ProgressPage() {
setProgressList(Object.values(progressObject)); setProgressList(Object.values(progressObject));
break; break;
case "stdErr": case "stdErr":
logs[evt.detail.id].push(evt.detail.data);
break; break;
case "exit": case "exit":
console.log(`FFmpeg exited with code: ${evt.detail.data}`); console.log(`FFmpeg exited with code: ${evt.detail.data}`);
os.getSpawnedProcesses().then((processes) => { if (evt.detail.data === 0) {
if (processes.length === 0) { progressObject[evt.detail.id].percentage = 100;
setFinished(true); setProgressList(Object.values(progressObject));
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 "${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);
}
} }
setRunningProcesses(processes); 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; break;
} }
} }
@@ -91,20 +156,16 @@ function ProgressPage() {
events.on("windowBlur", windowUnfocused); events.on("windowBlur", windowUnfocused);
events.on("spawnedProcess", handleSpawnedProcessEvents); events.on("spawnedProcess", handleSpawnedProcessEvents);
const processes = await os.getSpawnedProcesses();
setRunningProcesses(processes);
const storedFileInfo: TargetFile[] = JSON.parse( const storedFileInfo: TargetFile[] = JSON.parse(
await storage.getData("filesBeingProcessed"), await storage.getData("filesBeingProcessed"),
); );
setFileInfo(storedFileInfo); totalCount = storedFileInfo.length;
for (let i = 0; i < processes.length; i++) { const file = storedFileInfo.pop()!;
progressObject[processes[i].id] = {
filename: storedFileInfo[i].in, processFiles([file]);
percentage: 0, fileQueue = storedFileInfo;
}; setQueueLength(fileQueue.length);
}
setProgressList(Object.values(progressObject)); setProgressList(Object.values(progressObject));
}); });
@@ -131,6 +192,14 @@ function ProgressPage() {
setFinished(true); setFinished(true);
} }
async function openFolder() {
const folder = await getVencoderFolder();
if (folder) {
os.open(folder);
}
}
return ( return (
<main class="row flex-col"> <main class="row flex-col">
<div class="container row flex-col" style={{ flex: "1" }}> <div class="container row flex-col" style={{ flex: "1" }}>
@@ -148,15 +217,15 @@ function ProgressPage() {
<Show when={finished()}> <Show when={finished()}>
Processes finished. You can close this window. Processes finished. You can close this window.
</Show> </Show>
<Index each={progressList()}> {progressList().map((item) => {
{(item, _) => ( return (
<div <div
class="row flex-col" class="row flex-col"
style={{ style={{
"padding-bottom": "var(--k-grid-unit)", "padding-bottom": "var(--k-grid-unit)",
}} }}
> >
<label>{item().filename}</label> <label>{item.filename}</label>
<div <div
class="grid" class="grid"
style={{ style={{
@@ -166,27 +235,27 @@ function ProgressPage() {
<div class="row justify-content-center align-items-center"> <div class="row justify-content-center align-items-center">
<progress <progress
class="col" class="col"
value={item().percentage} value={item.percentage}
max="100" max="100"
/> />
</div> </div>
<div class="row justify-content-center align-items-center"> <div class="row justify-content-center align-items-center">
{Math.min( {Math.min(
Math.round(item().percentage), Math.round(item.percentage),
100, 100,
)} )}
% %
</div> </div>
</div> </div>
</div> </div>
)} );
</Index> })}
<div>{queueLength()} file(s) queued.</div>
</div> </div>
<Show when={!finished()}> <footer class="p-medium row" style={{ "align-items": "end" }}>
<footer <Show
class="p-medium row" when={finished()}
style={{ "align-items": "end" }} fallback={
>
<button <button
class="k-button" class="k-button"
disabled={isCancelling()} disabled={isCancelling()}
@@ -194,8 +263,13 @@ function ProgressPage() {
> >
Cancel Cancel
</button> </button>
</footer> }
>
<button class="k-button" onclick={openFolder}>
Open Folder
</button>
</Show> </Show>
</footer>
</div> </div>
</main> </main>
); );
+88 -31
View File
@@ -7,24 +7,27 @@ export interface CodecInfo {
encoders: string[]; encoders: string[];
} }
export async function getAvailableCodecs(): Promise<CodecInfo[]> { export type CodecList = {
vcodecs: CodecInfo[],
acodecs: CodecInfo[]
}
export async function getAvailableCodecs(): Promise<CodecList> {
const seperator = "-------"; const seperator = "-------";
const videoEncodingSupported = /.EV.../;
const wideFormattingSpaces = / {2,}/; const wideFormattingSpaces = / {2,}/;
const decodeEncodeSpecification = / \(((decoders)|(encoders)):.+\)/g; const decodeEncodeSpecification = / \(((decoders)|(encoders)):.+\)/g;
const result = await Neutralino.os.execCommand("ffmpeg -codecs"); const result = await Neutralino.os.execCommand("ffmpeg -codecs");
const rawCodecList = result.stdOut const rawCodecList = result.stdOut
.substring(result.stdOut.indexOf(seperator) + seperator.length) .substring(result.stdOut.indexOf(seperator) + seperator.length)
.split("\n"); .split("\n");
let codecs = []; let vcodecs = [];
let acodecs = [];
for (let codec of rawCodecList) { for (let codec of rawCodecList) {
codec = codec.trim(); codec = codec.trim();
const flags = codec.substring(0, 6); const flags = codec.substring(0, 6);
if (!videoEncodingSupported.test(flags)) { if (flags[1] !== "E") continue;
continue;
}
const nameAndDescription = codec const nameAndDescription = codec
.substring(7) .substring(7)
@@ -48,15 +51,49 @@ export async function getAvailableCodecs(): Promise<CodecInfo[]> {
.split(" "); .split(" ");
} }
codecs.push({ if (flags[2] === "V") {
vcodecs.push({
flags,
shortName,
description,
encoders,
});
} else if (flags[2] === "A") {
acodecs.push({
flags, flags,
shortName, shortName,
description, description,
encoders, 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) { export function playFile(path: string) {
@@ -99,6 +136,7 @@ export interface FFmpegParams {
faststart?: boolean; faststart?: boolean;
doNotUseAn?: boolean; doNotUseAn?: boolean;
speed?: number; speed?: number;
pixelFormat?: string;
/** /**
* Extra parameters defined by users * Extra parameters defined by users
*/ */
@@ -106,9 +144,11 @@ export interface FFmpegParams {
/** /**
* Extra output parameters defined by Vencoder * 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"; 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; 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) { export function generateOutputCommand(params: FFmpegParams) {
let faststart = let faststart =
params.faststart && params.vcodec === "h264" params.faststart && params.vcodec === "h264"
? " -movflags +faststart" ? " -movflags +faststart"
: ""; : "";
let globalopts = "-hwaccel auto -y"; let globalopts = `-hwaccel ${params.hwaccel ?? "auto"} -y`;
let inputopts = let inputopts =
params.useropts.input !== "" ? " " + params.useropts.input : ""; params.useropts.input !== "" ? " " + params.useropts.input : "";
let outputopts = let outputopts =
@@ -133,40 +181,49 @@ export function generateOutputCommand(params: FFmpegParams) {
globalopts += " " + params.useropts.global; globalopts += " " + params.useropts.global;
} }
if (params.pixelFormat) {
if (params.outputopts === undefined) {
params.outputopts = {};
}
params.outputopts = {
"pix_fmt": params.pixelFormat
};
}
if (params.outputopts !== undefined) { if (params.outputopts !== undefined) {
console.log(params.outputopts);
for (const key of Object.keys(params.outputopts)) { for (const key of Object.keys(params.outputopts)) {
if (params.outputopts[key] === undefined) continue;
outputopts += ` -${key} ${params.outputopts[key]}`.trimEnd(); outputopts += ` -${key} ${params.outputopts[key]}`.trimEnd();
} }
} }
if (params.encoder === "vp9_qsv") {
return quickSyncVp9Command(params, {
global: globalopts,
input: inputopts,
output: outputopts
})
}
if (params.twopass) { if (params.twopass) {
const commonOpts = `${globalopts}${inputopts} -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec} -b:v ${ const commonOpts = `${globalopts}${inputopts} -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec} -b:v ${params.vbitrate ?? DEFAULT_BITRATE
params.vbitrate ?? DEFAULT_BITRATE }k${faststart}${params.preset === undefined ? "" : ` -preset ${params.preset}`
}k${faststart}${
params.preset === undefined ? "" : ` -preset ${params.preset}`
} -progress -${outputopts}`; } -progress -${outputopts}`;
return `ffmpeg ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=1" : "-pass 1"} ${ return `ffmpeg ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=1" : "-pass 1"} ${params.doNotUseAn ? "-vsync cfr" : "-an"
params.doNotUseAn ? "-vsync cfr" : "-an"
} -f null ${NULL_LOCATION} && } -f null ${NULL_LOCATION} &&
ffmpeg ${commonOpts} ${ ffmpeg ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=2" : "-pass 2"
params.vcodec === "hevc" ? "-x265-params pass=2" : "-pass 2" } -c:a ${params.acodec ?? "copy"
} -c:a ${
params.acodec ?? "copy"
}${params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`} "${params.outputFile ?? "{output}"}"`; }${params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`} "${params.outputFile ?? "{output}"}"`;
} }
return `ffmpeg ${globalopts}${inputopts} -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec}${ return `ffmpeg ${globalopts}${inputopts} -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec}${params.crf === undefined ? "" : ` -crf ${params.crf}`
params.crf === undefined ? "" : ` -crf ${params.crf}` }${params.vbitrate === undefined ? "" : ` -b:v ${params.vbitrate}k`
}${ }${faststart}${params.preset === undefined ? "" : ` -preset ${params.preset}`
params.vbitrate === undefined ? "" : ` -b:v ${params.vbitrate}` } -c:a ${params.acodec ?? "copy"}${params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`
}${faststart}${ }${params.speed === undefined ? "" : ` -speed ${params.speed}`
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}"}"`; } -progress -${outputopts} "${params.outputFile ?? "{output}"}"`;
} }
+11
View File
@@ -1,3 +1,5 @@
import Neutralino from "@neutralinojs/lib";
export function getTemporaryFilePath() { export function getTemporaryFilePath() {
switch (window.NL_OS) { switch (window.NL_OS) {
case "Windows": case "Windows":
@@ -8,3 +10,12 @@ export function getTemporaryFilePath() {
return "."; 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\\`;
}
}
+2 -1
View File
@@ -1,9 +1,10 @@
import tailwindcss from "@tailwindcss/vite";
import { fileURLToPath, URL } from "node:url"; import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import solid from "vite-plugin-solid"; import solid from "vite-plugin-solid";
export default defineConfig({ export default defineConfig({
plugins: [solid()], plugins: [solid(), tailwindcss()],
resolve: { resolve: {
alias: { alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)), "@": fileURLToPath(new URL("./src", import.meta.url)),