6 Commits

Author SHA1 Message Date
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
11 changed files with 838 additions and 692 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
+1 -1
View File
@@ -78,7 +78,7 @@ encoders supported by your FFmpeg install will show up.
- [ ] VP9 - [ ] VP9
- [ ] libvpx-vp9 - [ ] libvpx-vp9
- [ ] vp9_vaapi - [ ] vp9_vaapi
- [ ] vp9_qsv - [x] vp9_qsv (Really Basic)
## Gitea Actions ## Gitea Actions
+6 -5
View File
@@ -20,20 +20,21 @@
"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"
+2 -2
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="/__neutralino_globals.js"></script>
<title>Vencoder</title> <title>Vencoder</title>
</head> </head>
@@ -14,4 +14,4 @@
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
</html> </html>
+5 -5
View File
@@ -9,17 +9,17 @@
"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", "color-convert": "^3.1.2",
"solid-js": "^1.9.9" "solid-js": "^1.9.9"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.3.0", "@types/node": "^24.5.2",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"vite": "^7.1.2", "vite": "^7.1.6",
"vite-plugin-solid": "^2.11.8" "vite-plugin-solid": "^2.11.8"
}, },
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748" "packageManager": "pnpm@10.17.0+sha512.fce8a3dd29a4ed2ec566fb53efbb04d8c44a0f05bc6f24a73046910fb9c3ce7afa35a0980500668fa3573345bd644644fa98338fa168235c80f4aa17aa17fbef"
} }
+357 -330
View File
File diff suppressed because it is too large Load Diff
+324 -293
View File
@@ -14,15 +14,17 @@ 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 { openFile } from "./util/oshelper";
import { getTemporaryFilePath } from "./util/path"; import { getTemporaryFilePath, getVencoderFolder } from "./util/path";
import { generateRandomString } from "./util/string"; import { generateRandomString } from "./util/string";
import "./css/icons.css"; import "./css/icons.css";
import BreezeIcon from "./components/BreezeIcon"; import BreezeIcon from "./components/BreezeIcon";
@@ -32,7 +34,7 @@ import DNxHDOptions from "./components/DNxHDOptions";
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]); const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
interface RunningProcessInfo { interface RunningProcessInfo {
process: Neutralino.os.SpawnedProcess; process: Neutralino.SpawnedProcess;
file: string; file: string;
length: number; length: number;
} }
@@ -41,6 +43,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(
@@ -56,8 +59,12 @@ function App() {
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 [audioCodec, setAudioCodec] = createSignal("copy");
const [audioEncoder, setAudioEncoder] = createSignal("");
const [pixelFormatList, setPixelFormatList] = createSignal([] as string[]);
const [pixelFormat, setPixelFormat] = createSignal("");
const logs: { [id: number]: string[] } = {}; const logs: { [id: number]: string[] } = {};
let supportedCodecs: CodecInfo[] = []; let supportedCodecs: CodecList = { vcodecs: [], acodecs: [] };
let ffmpegParams: FFmpegParams = { let ffmpegParams: FFmpegParams = {
vcodec: "", vcodec: "",
useropts: { useropts: {
@@ -127,6 +134,7 @@ function App() {
supportedCodecs = await getAvailableCodecs(); supportedCodecs = await getAvailableCodecs();
filterDisplayedCodecs(); filterDisplayedCodecs();
setAudioCodecList(supportedCodecs.acodecs);
const firstCodec = displayedCodecs()[0]; const firstCodec = displayedCodecs()[0];
@@ -134,6 +142,8 @@ 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(() => {
@@ -180,12 +190,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) {
@@ -222,6 +234,23 @@ function App() {
setSelectedEncoder(encoder); setSelectedEncoder(encoder);
} }
function getAudioEncoders() {
const codec = audioCodec();
let encoders = audioCodecList().find(
(v) => v.shortName === codec,
)?.encoders;
if (encoders) {
setAudioEncoder(encoders[0]);
}
if (encoders instanceof Array && encoders.length === 0) {
encoders = undefined;
}
return encoders;
}
function onParametersChanged(key: string, value: any) { function onParametersChanged(key: string, value: any) {
// @ts-ignore // @ts-ignore
ffmpegParams[key] = value; ffmpegParams[key] = value;
@@ -245,10 +274,18 @@ function App() {
encoder = undefined; encoder = undefined;
} }
let acodec = audioEncoder();
if (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 +300,7 @@ function App() {
input: inputopts(), input: inputopts(),
output: outputopts(), output: outputopts(),
}, },
pixelFormat: pixFmt === "" ? undefined : pixFmt,
}; };
setOutputCommand(generateOutputCommand(ffmpegParams)); setOutputCommand(generateOutputCommand(ffmpegParams));
@@ -282,14 +320,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 +339,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") {
@@ -401,289 +432,289 @@ function App() {
} }
return ( return (
<main class="row flex-col"> <main class="row">
<div class="container" style={{ flex: "1" }}> <div class="row flex-col h-full">
<div class="row h-full"> <header
<div class="row flex-col h-full"> class={`k-page-header k-rborder ${windowFocused() ? "" : "window-blur"}`}
<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> <BreezeIcon
</header> icon="b b-trash-empty"
<div alt="Remove Selected Video"
class="row flex-col gap2 k-white-sidebar k-rborder h-full" />
style={{ padding: "8px" }} </button>
<button
disabled={selectedClip() === ""}
onclick={playBtnClicked}
class="icon-button k-button"
> >
<ul class="k-list-view bordered col"> <BreezeIcon
<For each={fileList()}> icon="playback-start"
{(item, _) => ( alt="Preview Selected Video"
<li />
class={ </button>
item == selectedClip() <button
? "selected" class="icon-button k-button"
: "" onclick={settingsBtnPressed}
}
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"}`}
> >
<div class="page-title">Conversion Settings</div> <BreezeIcon icon="configure" alt="Configure" />
</header> </button>
<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>
</div> </div>
</div> </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>
<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">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> </main>
); );
} }
+10
View File
@@ -92,3 +92,13 @@ 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;
}
+27 -14
View File
@@ -1,4 +1,5 @@
import { events, os, storage } from "@neutralinojs/lib"; import { getVencoderFolder } from "@/util/path";
import { events, os, storage, type SpawnedProcess } from "@neutralinojs/lib";
import { createSignal, onMount, onCleanup, Show, Index } from "solid-js"; import { createSignal, onMount, onCleanup, Show, Index } from "solid-js";
interface TargetFile { interface TargetFile {
@@ -29,7 +30,7 @@ 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 [finished, setFinished] = createSignal(false); const [finished, setFinished] = createSignal(false);
const [fileInfo, setFileInfo] = createSignal<TargetFile[]>([]); const [fileInfo, setFileInfo] = createSignal<TargetFile[]>([]);
@@ -131,6 +132,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" }}>
@@ -182,20 +191,24 @@ function ProgressPage() {
)} )}
</Index> </Index>
</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
class="k-button"
disabled={isCancelling()}
onclick={cancelBtnClicked}
>
Cancel
</button>
}
> >
<button <button class="k-button" onclick={openFolder}>
class="k-button" Open Folder
disabled={isCancelling()}
onclick={cancelBtnClicked}
>
Cancel
</button> </button>
</footer> </Show>
</Show> </footer>
</div> </div>
</main> </main>
); );
+93 -40
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") {
flags, vcodecs.push({
shortName, flags,
description, shortName,
encoders, 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) { 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
*/ */
@@ -117,6 +155,14 @@ 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"
@@ -133,41 +179,48 @@ 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)) {
outputopts += ` -${key} ${params.outputopts[key]}`.trimEnd(); outputopts += ` -${key} ${params.outputopts[key]}`.trimEnd();
} }
} }
if (params.twopass) { if (params.encoder === "vp9_qsv") {
const commonOpts = `${globalopts}${inputopts} -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec} -b:v ${ return quickSyncVp9Command(params, {
params.vbitrate ?? DEFAULT_BITRATE global: globalopts,
}k${faststart}${ input: inputopts,
params.preset === undefined ? "" : ` -preset ${params.preset}` output: outputopts
} -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}${ if (params.twopass) {
params.crf === undefined ? "" : ` -crf ${params.crf}` 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}`
params.vbitrate === undefined ? "" : ` -b:v ${params.vbitrate}` } -progress -${outputopts}`;
}${faststart}${
params.preset === undefined ? "" : ` -preset ${params.preset}` return `ffmpeg ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=1" : "-pass 1"} ${params.doNotUseAn ? "-vsync cfr" : "-an"
} -c:a ${params.acodec ?? "copy"}${ } -f null ${NULL_LOCATION} &&
params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k` ffmpeg ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=2" : "-pass 2"
}${ } -c:a ${params.acodec ?? "copy"
params.speed === undefined ? "" : ` -speed ${params.speed}` }${params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`} "${params.outputFile ?? "{output}"}"`;
} -progress -${outputopts} "${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}`
}${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) { export async function getLengthMicroseconds(target: string) {
+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\\`;
}
}