Audio Codecs & Slightly Improve UX
Build / build (push) Successful in 1m28s

This commit is contained in:
2025-09-21 10:56:22 +07:00
parent 68d919ab1e
commit 74cebbf180
5 changed files with 330 additions and 315 deletions
+58 -92
View File
@@ -17,12 +17,13 @@ import {
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";
@@ -41,6 +42,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 +58,9 @@ 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 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 +130,7 @@ function App() {
supportedCodecs = await getAvailableCodecs(); supportedCodecs = await getAvailableCodecs();
filterDisplayedCodecs(); filterDisplayedCodecs();
setAudioCodecList(supportedCodecs.acodecs);
const firstCodec = displayedCodecs()[0]; const firstCodec = displayedCodecs()[0];
@@ -180,12 +184,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) {
@@ -248,7 +254,7 @@ function App() {
ffmpegParams = { ffmpegParams = {
vcodec: selectedCodec()?.shortName ?? "", vcodec: selectedCodec()?.shortName ?? "",
encoder, encoder,
acodec: ffmpegParams.acodec, acodec: audioCodec(),
abitrate: ffmpegParams.abitrate, abitrate: ffmpegParams.abitrate,
crf: ffmpegParams.crf, crf: ffmpegParams.crf,
doNotUseAn: ffmpegParams.doNotUseAn, doNotUseAn: ffmpegParams.doNotUseAn,
@@ -282,14 +288,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(
@@ -401,9 +400,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 +416,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 +426,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 +456,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 +467,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,32 +512,19 @@ 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>
@@ -568,10 +532,8 @@ function App() {
<Switch fallback={<div></div>}> <Switch fallback={<div></div>}>
<Match <Match
when={ when={
selectedCodec()?.shortName === selectedCodec()?.shortName === "h264" ||
"h264" || selectedCodec()?.shortName === "hevc"
selectedCodec()?.shortName ===
"hevc"
} }
> >
<H264Options <H264Options
@@ -580,11 +542,7 @@ function App() {
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 +551,7 @@ function App() {
/> />
</Match> </Match>
<Match <Match
when={ when={selectedCodec()?.shortName === "dnxhd"}
selectedCodec()?.shortName ===
"dnxhd"
}
> >
<DNxHDOptions <DNxHDOptions
codec={selectedCodec()} codec={selectedCodec()}
@@ -605,6 +560,27 @@ 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="targetCodec">Codec</label>
<select
class="k-dropdown"
id="targetCodec"
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>
</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 +590,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 +614,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 +628,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 +647,9 @@ function App() {
> >
Convert Selected Convert Selected
</button> </button>
</div>
</footer> </footer>
</div> </div>
</div>
</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;
}
+19 -6
View File
@@ -1,3 +1,4 @@
import { getVencoderFolder } from "@/util/path";
import { events, os, storage } from "@neutralinojs/lib"; import { events, os, storage } from "@neutralinojs/lib";
import { createSignal, onMount, onCleanup, Show, Index } from "solid-js"; import { createSignal, onMount, onCleanup, Show, Index } from "solid-js";
@@ -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,11 +191,10 @@ 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 <button
class="k-button" class="k-button"
disabled={isCancelling()} disabled={isCancelling()}
@@ -194,8 +202,13 @@ function ProgressPage() {
> >
Cancel Cancel
</button> </button>
</footer> }
>
<button class="k-button" onclick={openFolder}>
Open Folder
</button>
</Show> </Show>
</footer>
</div> </div>
</main> </main>
); );
+23 -8
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,27 @@ 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 function playFile(path: string) { export function playFile(path: 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\\`;
}
}