Really basic functionality
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
import { events } from "@neutralinojs/lib";
|
||||
import {
|
||||
createEffect,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
onCleanup,
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
type Signal,
|
||||
} from "solid-js";
|
||||
import {
|
||||
generateOutputCommand,
|
||||
getAvailableCodecs,
|
||||
playFile,
|
||||
videoFileExtensions,
|
||||
type CodecInfo,
|
||||
type FFmpegParams,
|
||||
} from "./util/ffmpeg";
|
||||
import Neutralino from "@neutralinojs/lib";
|
||||
import H264Options from "./components/H264Options";
|
||||
import Configure from "./assets/breeze/actions/16/configure.svg";
|
||||
import PlaybackStart from "./assets/breeze/actions/16/media-playback-start.svg";
|
||||
import TrashEmpty from "./assets/breeze/actions/16/trash-empty.svg";
|
||||
|
||||
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
|
||||
|
||||
function App() {
|
||||
const [windowFocused, setWindowFocused] = createSignal(true);
|
||||
const [displayedCodecs, setDisplayedCodecs]: Signal<CodecInfo[]> =
|
||||
createSignal([] as CodecInfo[]);
|
||||
const [fileList, setFileList] = createSignal([
|
||||
"/home/satakunu/Videos/litetask_demo.mkv",
|
||||
]);
|
||||
const [selectedClip, setSelectedClip] = createSignal("");
|
||||
const [outputCommand, setOutputCommand] = createSignal(
|
||||
"ffmpeg -i {filename}",
|
||||
);
|
||||
const [showCommonCodecs, setShowCommonCodecs] = createSignal(true);
|
||||
const [selectedCodec, setSelectedCodec] = createSignal<CodecInfo>();
|
||||
const [selectedEncoder, setSelectedEncoder] = createSignal("");
|
||||
const [runningProcess, setRunningProcess] = createSignal<{
|
||||
process: Neutralino.os.SpawnedProcess;
|
||||
params: FFmpegParams;
|
||||
}>();
|
||||
let supportedCodecs: CodecInfo[] = [];
|
||||
let ffmpegParams: FFmpegParams = { vcodec: "" };
|
||||
let successfulCount = 0;
|
||||
let unsuccessfulCount = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
function windowIsFocused() {
|
||||
setWindowFocused(true);
|
||||
}
|
||||
|
||||
function windowUnfocused() {
|
||||
setWindowFocused(false);
|
||||
}
|
||||
|
||||
function handleSpawnedProcessEvents(evt: CustomEvent) {
|
||||
if (runningProcess()?.process.id !== evt.detail.id) return;
|
||||
|
||||
switch (evt.detail.action) {
|
||||
case "stdOut":
|
||||
console.log(evt.detail.data);
|
||||
break;
|
||||
case "stdErr":
|
||||
break;
|
||||
case "exit":
|
||||
if (evt.detail.data === 0) {
|
||||
successfulCount += 1;
|
||||
} else {
|
||||
unsuccessfulCount += 1;
|
||||
Neutralino.os.showNotification(
|
||||
"File Encoding Failed",
|
||||
`Encoding for file "${runningProcess()?.params.inputFile}" failed. Exit code ${evt.detail.data}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (successfulCount + unsuccessfulCount === totalCount) {
|
||||
Neutralino.os.showNotification(
|
||||
"File(s) encoded.",
|
||||
`${successfulCount} files encoded successfully. ${unsuccessfulCount} failed.`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`FFmpeg exited with code: ${evt.detail.data}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
events.on("windowFocus", windowIsFocused);
|
||||
events.on("windowBlur", windowUnfocused);
|
||||
events.on("spawnedProcess", handleSpawnedProcessEvents);
|
||||
|
||||
supportedCodecs = await getAvailableCodecs();
|
||||
filterDisplayedCodecs();
|
||||
|
||||
const firstCodec = displayedCodecs()[0];
|
||||
|
||||
setSelectedCodec(firstCodec);
|
||||
setSelectedEncoder(firstCodec.encoders[0]);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
events.off("windowFocus", windowIsFocused);
|
||||
events.off("windowBlur", windowUnfocused);
|
||||
events.off("spawnedProcess", handleSpawnedProcessEvents);
|
||||
});
|
||||
|
||||
function removeBtnClicked() {
|
||||
if (selectedClip() === "") return;
|
||||
|
||||
const list = fileList();
|
||||
const targetClip = selectedClip();
|
||||
setFileList(list.filter((v) => v !== targetClip));
|
||||
setSelectedClip("");
|
||||
}
|
||||
|
||||
function removeAllBtnClicked() {
|
||||
setFileList([]);
|
||||
setSelectedClip("");
|
||||
}
|
||||
|
||||
function playBtnClicked() {
|
||||
playFile(selectedClip());
|
||||
}
|
||||
|
||||
async function openBtnClicked() {
|
||||
const filePaths = await Neutralino.os.showOpenDialog("Select Videos", {
|
||||
multiSelections: true,
|
||||
filters: [
|
||||
{
|
||||
extensions: ["mp4", "mkv", "mov", "webm"],
|
||||
name: "Common Video Files",
|
||||
},
|
||||
{
|
||||
extensions: ["*"],
|
||||
name: "All Files",
|
||||
},
|
||||
],
|
||||
});
|
||||
setFileList(Array.from(new Set([...fileList(), ...filePaths])));
|
||||
}
|
||||
|
||||
function filterDisplayedCodecs() {
|
||||
if (showCommonCodecs()) {
|
||||
setDisplayedCodecs(
|
||||
supportedCodecs.filter((v) => commonCodecs.has(v.shortName)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setDisplayedCodecs(supportedCodecs);
|
||||
}
|
||||
|
||||
function showCommonCodecsChanged(e: InputEvent) {
|
||||
const newValue = (e.target as HTMLInputElement).checked;
|
||||
setShowCommonCodecs(newValue);
|
||||
filterDisplayedCodecs();
|
||||
}
|
||||
|
||||
function selectedCodecsChanged(e: InputEvent) {
|
||||
const newValue = (e.target as HTMLInputElement).value;
|
||||
const codecObj = displayedCodecs().find(
|
||||
(v) => v.shortName === newValue,
|
||||
);
|
||||
|
||||
if (newValue !== "h264" && newValue !== "hevc") {
|
||||
ffmpegParams.twopass = false;
|
||||
}
|
||||
|
||||
setSelectedCodec(codecObj);
|
||||
let encoder = newValue;
|
||||
if (codecObj?.encoders.length !== 0) {
|
||||
encoder = codecObj?.encoders[0] ?? "";
|
||||
}
|
||||
setSelectedEncoder(encoder);
|
||||
}
|
||||
|
||||
function onParametersChanged(key: string, value: any) {
|
||||
// @ts-ignore
|
||||
ffmpegParams[key] = value;
|
||||
setOutputCommand(generateOutputCommand(ffmpegParams));
|
||||
}
|
||||
|
||||
function settingsBtnPressed() {
|
||||
Neutralino.window.create(`${window.location.href}settings`, {
|
||||
width: 800,
|
||||
height: 600,
|
||||
x: 120,
|
||||
y: 120,
|
||||
injectGlobals: true,
|
||||
});
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
let encoder: string | undefined = selectedEncoder();
|
||||
|
||||
if (encoder === "") {
|
||||
encoder = undefined;
|
||||
}
|
||||
|
||||
ffmpegParams = {
|
||||
vcodec: selectedCodec()?.shortName ?? "",
|
||||
encoder,
|
||||
acodec: ffmpegParams.acodec,
|
||||
abitrate: ffmpegParams.abitrate,
|
||||
crf: ffmpegParams.crf,
|
||||
doNotUseAn: ffmpegParams.doNotUseAn,
|
||||
faststart: ffmpegParams.faststart,
|
||||
hwaccel: ffmpegParams.hwaccel,
|
||||
inputFile: undefined,
|
||||
preset: ffmpegParams.preset,
|
||||
twopass: ffmpegParams.twopass,
|
||||
vbitrate: ffmpegParams.vbitrate,
|
||||
};
|
||||
|
||||
setOutputCommand(generateOutputCommand(ffmpegParams));
|
||||
});
|
||||
|
||||
async function convertClip(clip: string) {
|
||||
ffmpegParams.inputFile = clip;
|
||||
|
||||
const fileName = (await Neutralino.filesystem.getPathParts(clip)).stem;
|
||||
|
||||
const fileExt =
|
||||
videoFileExtensions[selectedCodec()?.shortName ?? ""] ?? "";
|
||||
|
||||
switch (window.NL_OS) {
|
||||
case "Linux":
|
||||
ffmpegParams.outputFile = `${await Neutralino.os.getEnv("HOME")}/Vencoder/${fileName}.${fileExt}`;
|
||||
break;
|
||||
case "Windows":
|
||||
ffmpegParams.outputFile = `${await Neutralino.os.getEnv("HOMEPATH")}\\Vencoder\\${fileName}.${fileExt}`;
|
||||
break;
|
||||
}
|
||||
|
||||
const outputDir = (
|
||||
await Neutralino.filesystem.getPathParts(
|
||||
ffmpegParams.outputFile ?? "",
|
||||
)
|
||||
).parentPath;
|
||||
try {
|
||||
await Neutralino.filesystem.getStats(outputDir);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
await Neutralino.filesystem.createDirectory(outputDir);
|
||||
}
|
||||
|
||||
try {
|
||||
await Neutralino.filesystem.getStats(ffmpegParams.outputFile ?? "");
|
||||
const userAnswer = await Neutralino.os.showMessageBox(
|
||||
"File already exists",
|
||||
`A file at ${ffmpegParams.outputFile} already exists. Would you like to overwrite it?`,
|
||||
Neutralino.os.MessageBoxChoice.YES_NO,
|
||||
Neutralino.os.Icon.QUESTION,
|
||||
);
|
||||
|
||||
if (userAnswer === "NO") {
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
setRunningProcess({
|
||||
process: await Neutralino.os.spawnProcess(
|
||||
generateOutputCommand(ffmpegParams),
|
||||
),
|
||||
params: { ...ffmpegParams },
|
||||
});
|
||||
}
|
||||
|
||||
async function convertAllClicked() {
|
||||
const list = fileList();
|
||||
|
||||
totalCount = list.length;
|
||||
|
||||
for (const clip of list) {
|
||||
convertClip(clip);
|
||||
}
|
||||
}
|
||||
|
||||
function convertSelectedClicked() {
|
||||
convertClip(selectedClip());
|
||||
}
|
||||
|
||||
return (
|
||||
<main class="row flex-col">
|
||||
<div class="container" style={{ flex: "1" }}>
|
||||
<div class="row h-full">
|
||||
<div class="row flex-col h-full">
|
||||
<header
|
||||
class={`k-page-header k-rborder ${windowFocused() ? "" : "window-blur"}`}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<img
|
||||
src={TrashEmpty}
|
||||
alt="Remove Selected Video"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedClip() === ""}
|
||||
onclick={playBtnClicked}
|
||||
class="icon-button k-button"
|
||||
>
|
||||
<img
|
||||
src={PlaybackStart}
|
||||
alt="Preview Selected Video"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="icon-button k-button"
|
||||
onclick={settingsBtnPressed}
|
||||
>
|
||||
<img
|
||||
src={Configure}
|
||||
alt="Configure Vencoder"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-col h-full" style={{ width: "100%" }}>
|
||||
<header
|
||||
class={`k-page-header ${windowFocused() ? "" : "window-blur"}`}
|
||||
>
|
||||
<div class="page-title">Conversion Settings</div>
|
||||
</header>
|
||||
<div
|
||||
class="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>
|
||||
<Show
|
||||
when={
|
||||
selectedCodec()?.encoders.length !==
|
||||
0
|
||||
}
|
||||
>
|
||||
<label>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>
|
||||
</Switch>
|
||||
</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>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user