Compare commits
8 Commits
01306a9688
..
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
37568aa0d1
|
|||
|
466a7cedca
|
|||
|
77e91fde1c
|
|||
|
74cebbf180
|
|||
|
68d919ab1e
|
|||
|
d586a8f222
|
|||
|
4cad43abe1
|
|||
|
3921c003bb
|
@@ -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
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ A tool to interactively (re-)encode videos using FFmpeg.
|
|||||||
Uses Neutralino.js and Solid.js.
|
Uses Neutralino.js and Solid.js.
|
||||||
|
|
||||||
This app _tries_ to imitate KDE's Kirigami UI framework, and also makes use of
|
This app _tries_ to imitate KDE's Kirigami UI framework, and also makes use of
|
||||||
Breeze icons
|
Breeze icons (Located in `./solid-src/public/breeze[-dark]`)
|
||||||
|
|
||||||
- `./solid-src/src/assets/breeze[-dark]`: Icons used by TSX files
|
|
||||||
- `./solid-src/public/breeze[-dark]`: Icons used by CSS files
|
|
||||||
|
|
||||||
Vencoder is tested with FFmpeg 7.1.1, should be compatible with older versions
|
Vencoder is tested with FFmpeg 7.1.1, should be compatible with older versions
|
||||||
but is not guaranteed.
|
but is not guaranteed.
|
||||||
@@ -56,7 +53,7 @@ encoders supported by your FFmpeg install will show up.
|
|||||||
- [ ] av1_nvenc
|
- [ ] av1_nvenc
|
||||||
- [ ] av1_qsv
|
- [ ] av1_qsv
|
||||||
- [ ] av1_vaapi
|
- [ ] av1_vaapi
|
||||||
- [ ] DNxHD
|
- [x] DNxHD (Does not provide options to deal with its pickiness yet)
|
||||||
- [ ] H.264
|
- [ ] H.264
|
||||||
- [x] libx264
|
- [x] libx264
|
||||||
- [x] libx264rgb (Untested, but _should_ work)
|
- [x] libx264rgb (Untested, but _should_ work)
|
||||||
@@ -81,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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+357
-330
File diff suppressed because it is too large
Load Diff
+202
-86
@@ -14,24 +14,27 @@ 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";
|
||||||
import AV1Options from "./components/AV1Options";
|
import AV1Options from "./components/AV1Options";
|
||||||
|
import DNxHDOptions from "./components/DNxHDOptions";
|
||||||
|
|
||||||
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
|
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
|
||||||
|
|
||||||
interface RunningProcessInfo {
|
interface RunningProcessInfo {
|
||||||
process: Neutralino.os.SpawnedProcess;
|
process: Neutralino.SpawnedProcess;
|
||||||
file: string;
|
file: string;
|
||||||
length: number;
|
length: number;
|
||||||
}
|
}
|
||||||
@@ -40,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(
|
||||||
@@ -52,9 +56,23 @@ function App() {
|
|||||||
RunningProcessInfo[]
|
RunningProcessInfo[]
|
||||||
>([]);
|
>([]);
|
||||||
const [customFileExt, setCustomFileExt] = createSignal("");
|
const [customFileExt, setCustomFileExt] = createSignal("");
|
||||||
|
const [globalopts, setGlobalopts] = createSignal("");
|
||||||
|
const [inputopts, setInputopts] = 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 = { vcodec: "" };
|
let ffmpegParams: FFmpegParams = {
|
||||||
|
vcodec: "",
|
||||||
|
useropts: {
|
||||||
|
global: "",
|
||||||
|
input: "",
|
||||||
|
output: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
let successfulCount = 0;
|
let successfulCount = 0;
|
||||||
let unsuccessfulCount = 0;
|
let unsuccessfulCount = 0;
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
@@ -116,6 +134,7 @@ function App() {
|
|||||||
|
|
||||||
supportedCodecs = await getAvailableCodecs();
|
supportedCodecs = await getAvailableCodecs();
|
||||||
filterDisplayedCodecs();
|
filterDisplayedCodecs();
|
||||||
|
setAudioCodecList(supportedCodecs.acodecs);
|
||||||
|
|
||||||
const firstCodec = displayedCodecs()[0];
|
const firstCodec = displayedCodecs()[0];
|
||||||
|
|
||||||
@@ -123,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(() => {
|
||||||
@@ -169,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) {
|
||||||
@@ -195,6 +218,11 @@ function App() {
|
|||||||
|
|
||||||
ffmpegParams = {
|
ffmpegParams = {
|
||||||
vcodec: codecObj?.shortName ?? "",
|
vcodec: codecObj?.shortName ?? "",
|
||||||
|
useropts: {
|
||||||
|
global: "",
|
||||||
|
input: "",
|
||||||
|
output: "",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let encoder = newValue;
|
let encoder = newValue;
|
||||||
@@ -206,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;
|
||||||
@@ -229,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,
|
||||||
@@ -242,6 +295,12 @@ function App() {
|
|||||||
preset: ffmpegParams.preset,
|
preset: ffmpegParams.preset,
|
||||||
twopass: ffmpegParams.twopass,
|
twopass: ffmpegParams.twopass,
|
||||||
vbitrate: ffmpegParams.vbitrate,
|
vbitrate: ffmpegParams.vbitrate,
|
||||||
|
useropts: {
|
||||||
|
global: globalopts(),
|
||||||
|
input: inputopts(),
|
||||||
|
output: outputopts(),
|
||||||
|
},
|
||||||
|
pixelFormat: pixFmt === "" ? undefined : pixFmt,
|
||||||
};
|
};
|
||||||
|
|
||||||
setOutputCommand(generateOutputCommand(ffmpegParams));
|
setOutputCommand(generateOutputCommand(ffmpegParams));
|
||||||
@@ -261,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(
|
||||||
@@ -287,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") {
|
||||||
@@ -380,9 +432,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"}`}
|
||||||
@@ -398,13 +448,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>
|
||||||
@@ -412,16 +458,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
|
||||||
@@ -448,10 +488,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>
|
||||||
@@ -462,14 +499,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"
|
||||||
@@ -514,41 +544,44 @@ 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>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>}>
|
<Switch fallback={<div></div>}>
|
||||||
<Match
|
<Match
|
||||||
when={
|
when={
|
||||||
selectedCodec()?.shortName ===
|
selectedCodec()?.shortName === "h264" ||
|
||||||
"h264" ||
|
selectedCodec()?.shortName === "hevc"
|
||||||
selectedCodec()?.shortName ===
|
|
||||||
"hevc"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<H264Options
|
<H264Options
|
||||||
@@ -557,11 +590,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()}
|
||||||
@@ -569,23 +598,111 @@ function App() {
|
|||||||
onParamChanged={onParametersChanged}
|
onParamChanged={onParametersChanged}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
<Match
|
||||||
</div>
|
when={selectedCodec()?.shortName === "dnxhd"}
|
||||||
<div class="row flex-col p-medium">
|
|
||||||
<label for="outputCommand">Command</label>
|
|
||||||
<pre
|
|
||||||
id="outputCommand"
|
|
||||||
class="k-text-field w-full col"
|
|
||||||
>
|
>
|
||||||
|
<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()}
|
{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
|
||||||
@@ -595,10 +712,9 @@ function App() {
|
|||||||
>
|
>
|
||||||
Convert Selected
|
Convert Selected
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { os } from "@neutralinojs/lib";
|
||||||
|
import { type CodecInfo, type FFmpegParams } from "../util/ffmpeg";
|
||||||
|
import BreezeIcon from "./BreezeIcon";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for H.264/H.265 codecs
|
||||||
|
*/
|
||||||
|
function DNxHDOptions(props: {
|
||||||
|
codec: CodecInfo | undefined;
|
||||||
|
params: FFmpegParams;
|
||||||
|
onParamChanged: (key: string, value: any) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section id="commonLossyOptions">
|
||||||
|
<div class="row flex-col align-items-center">
|
||||||
|
<h3 class="k-form-section-title">Encoder Options</h3>
|
||||||
|
</div>
|
||||||
|
<div class="k-form">
|
||||||
|
<label>Help</label>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
onclick={() =>
|
||||||
|
os.open("https://askubuntu.com/a/907515")
|
||||||
|
}
|
||||||
|
title="DNxHD is a picky encoder."
|
||||||
|
>
|
||||||
|
<BreezeIcon icon="help-about" alt="Help" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label for="profile">Profile</label>
|
||||||
|
<select
|
||||||
|
class="k-dropdown"
|
||||||
|
name="profile"
|
||||||
|
id="profile"
|
||||||
|
value={props.params.outputopts?.profile ?? "dnxhd"}
|
||||||
|
oninput={(e) => {
|
||||||
|
props.onParamChanged("outputopts", {
|
||||||
|
profile: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="dnxhd">DNxHD</option>
|
||||||
|
<option value="dnxhr_444">DNxHR 444</option>
|
||||||
|
<option value="dnxhr_hqx">DNxHR HQX</option>
|
||||||
|
<option value="dnxhr_hq">DNxHR HQ</option>
|
||||||
|
<option value="dnxhr_sq">DNxHR SQ</option>
|
||||||
|
<option value="dnxhr_lb">DNxHR LB</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DNxHDOptions;
|
||||||
@@ -63,7 +63,7 @@ function H264Options(props: {
|
|||||||
<BreezeIcon icon="help-about" alt="Help" />
|
<BreezeIcon icon="help-about" alt="Help" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label>Preset</label>
|
<label for="encodingPreset">Preset</label>
|
||||||
<select
|
<select
|
||||||
class="k-dropdown"
|
class="k-dropdown"
|
||||||
name="encodingPreset"
|
name="encodingPreset"
|
||||||
|
|||||||
@@ -64,10 +64,12 @@ function LibaomOptions(props: {
|
|||||||
<BreezeIcon icon="help-about" alt="Help" />
|
<BreezeIcon icon="help-about" alt="Help" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label>Rate-control modes</label>
|
<label for="rateControlMode">Rate-control modes</label>
|
||||||
<select
|
<select
|
||||||
class="k-dropdown"
|
class="k-dropdown"
|
||||||
onchange={(e) => setRateControlMode(e.target.value)}
|
onchange={(e) => setRateControlMode(e.target.value)}
|
||||||
|
name="rateControlMode"
|
||||||
|
id="rateControlMode"
|
||||||
>
|
>
|
||||||
<option value="Constant">Constant Quality</option>
|
<option value="Constant">Constant Quality</option>
|
||||||
<option value="Constrained">Constrained Quality</option>
|
<option value="Constrained">Constrained Quality</option>
|
||||||
@@ -80,7 +82,7 @@ function LibaomOptions(props: {
|
|||||||
rateControlMode() === "Constrained"
|
rateControlMode() === "Constrained"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<label>CRF</label>
|
<label for="crf">CRF</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
name="crf"
|
name="crf"
|
||||||
@@ -97,12 +99,13 @@ function LibaomOptions(props: {
|
|||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={rateControlMode() !== "Constant"}>
|
<Show when={rateControlMode() !== "Constant"}>
|
||||||
<label>Bitrate</label>
|
<label for="bitrate">Bitrate</label>
|
||||||
<div class="row gap2">
|
<div class="row gap2 align-items-center">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
name="bitrate"
|
name="bitrate"
|
||||||
id="bitrate"
|
id="bitrate"
|
||||||
|
aria-label="Kbps"
|
||||||
value={props.params.vbitrate ?? DEFAULT_BITRATE}
|
value={props.params.vbitrate ?? DEFAULT_BITRATE}
|
||||||
oninput={(e) => {
|
oninput={(e) => {
|
||||||
props.onParamChanged(
|
props.onParamChanged(
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { type CodecInfo, type FFmpegParams } from "@/util/ffmpeg";
|
import {
|
||||||
|
DEFAULT_BITRATE,
|
||||||
|
type CodecInfo,
|
||||||
|
type FFmpegParams,
|
||||||
|
} from "@/util/ffmpeg";
|
||||||
import { os } from "@neutralinojs/lib";
|
import { os } from "@neutralinojs/lib";
|
||||||
import BreezeIcon from "@/components/BreezeIcon";
|
import BreezeIcon from "@/components/BreezeIcon";
|
||||||
import { onMount } from "solid-js";
|
import { onMount } from "solid-js";
|
||||||
@@ -10,7 +14,10 @@ function Librav1eOptions(props: {
|
|||||||
}) {
|
}) {
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
props.onParamChanged("crf", undefined);
|
props.onParamChanged("crf", undefined);
|
||||||
props.onParamChanged("vbitrate", undefined);
|
props.onParamChanged(
|
||||||
|
"vbitrate",
|
||||||
|
props.params.vbitrate ?? DEFAULT_BITRATE,
|
||||||
|
);
|
||||||
props.onParamChanged("speed", 5);
|
props.onParamChanged("speed", 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,6 +53,19 @@ function Librav1eOptions(props: {
|
|||||||
props.onParamChanged("speed", e.target.value)
|
props.onParamChanged("speed", e.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<label for="bitrate">Bitrate</label>
|
||||||
|
<div class="row gap2 align-items-center">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="bitrate"
|
||||||
|
id="bitrate"
|
||||||
|
value={props.params.vbitrate ?? DEFAULT_BITRATE}
|
||||||
|
oninput={(e) =>
|
||||||
|
props.onParamChanged("vbitrate", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>Kbps</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,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>
|
||||||
);
|
);
|
||||||
|
|||||||
+115
-31
@@ -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) {
|
||||||
@@ -72,6 +109,12 @@ export const videoFileExtensions: { [key: string]: string } = {
|
|||||||
vp9: "mkv",
|
vp9: "mkv",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ExtraFFmpegArguments {
|
||||||
|
global: string;
|
||||||
|
input: string;
|
||||||
|
output: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FFmpegParams {
|
export interface FFmpegParams {
|
||||||
inputFile?: string;
|
inputFile?: string;
|
||||||
outputFile?: string;
|
outputFile?: string;
|
||||||
@@ -93,6 +136,15 @@ export interface FFmpegParams {
|
|||||||
faststart?: boolean;
|
faststart?: boolean;
|
||||||
doNotUseAn?: boolean;
|
doNotUseAn?: boolean;
|
||||||
speed?: number;
|
speed?: number;
|
||||||
|
pixelFormat?: string;
|
||||||
|
/**
|
||||||
|
* Extra parameters defined by users
|
||||||
|
*/
|
||||||
|
useropts: ExtraFFmpegArguments;
|
||||||
|
/**
|
||||||
|
* Extra output parameters defined by Vencoder
|
||||||
|
*/
|
||||||
|
outputopts?: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
const NULL_LOCATION = window.NL_OS === "Windows" ? "NUL" : "/dev/null";
|
const NULL_LOCATION = window.NL_OS === "Windows" ? "NUL" : "/dev/null";
|
||||||
@@ -103,40 +155,72 @@ 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"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
if (params.twopass) {
|
let globalopts = "-hwaccel auto -y";
|
||||||
const commonOpts = `-i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec} -b:v ${
|
let inputopts =
|
||||||
params.vbitrate ?? DEFAULT_BITRATE
|
params.useropts.input !== "" ? " " + params.useropts.input : "";
|
||||||
}k${faststart}${
|
let outputopts =
|
||||||
params.preset === undefined ? "" : ` -preset ${params.preset}`
|
params.useropts.output !== "" ? " " + params.useropts.output : "";
|
||||||
} -progress -`;
|
|
||||||
|
|
||||||
return `ffmpeg -hwaccel auto -y ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=1" : "-pass 1"} ${
|
if (params.useropts.global !== "") {
|
||||||
params.doNotUseAn ? "-vsync cfr" : "-an"
|
globalopts += " " + params.useropts.global;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.pixelFormat) {
|
||||||
|
if (params.outputopts === undefined) {
|
||||||
|
params.outputopts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
params.outputopts = {
|
||||||
|
"pix_fmt": params.pixelFormat
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.outputopts !== undefined) {
|
||||||
|
for (const key of Object.keys(params.outputopts)) {
|
||||||
|
outputopts += ` -${key} ${params.outputopts[key]}`.trimEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.encoder === "vp9_qsv") {
|
||||||
|
return quickSyncVp9Command(params, {
|
||||||
|
global: globalopts,
|
||||||
|
input: inputopts,
|
||||||
|
output: outputopts
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.twopass) {
|
||||||
|
const commonOpts = `${globalopts}${inputopts} -i "${params.inputFile ?? "{fileName}"}" -c:v ${params.encoder ?? params.vcodec} -b:v ${params.vbitrate ?? DEFAULT_BITRATE
|
||||||
|
}k${faststart}${params.preset === undefined ? "" : ` -preset ${params.preset}`
|
||||||
|
} -progress -${outputopts}`;
|
||||||
|
|
||||||
|
return `ffmpeg ${commonOpts} ${params.vcodec === "hevc" ? "-x265-params pass=1" : "-pass 1"} ${params.doNotUseAn ? "-vsync cfr" : "-an"
|
||||||
} -f null ${NULL_LOCATION} &&
|
} -f null ${NULL_LOCATION} &&
|
||||||
ffmpeg -y -hwaccel auto ${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 -y -hwaccel auto -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}`
|
||||||
}${
|
}${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}`
|
} -progress -${outputopts} "${params.outputFile ?? "{output}"}"`;
|
||||||
} -c:a ${params.acodec ?? "copy"}${
|
|
||||||
params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`
|
|
||||||
}${
|
|
||||||
params.speed === undefined ? "" : ` -speed ${params.speed}`
|
|
||||||
} -progress - "${params.outputFile ?? "{output}"}"`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLengthMicroseconds(target: string) {
|
export async function getLengthMicroseconds(target: string) {
|
||||||
|
|||||||
@@ -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\\`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user