Progress page
This commit is contained in:
@@ -1,12 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: shalithasuranga
|
|
||||||
patreon: shalithasuranga
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
@@ -11,3 +11,4 @@ neutralino.js
|
|||||||
# Neutralinojs related files
|
# Neutralinojs related files
|
||||||
.storage
|
.storage
|
||||||
*.log
|
*.log
|
||||||
|
.tmp
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"filesystem.getPathParts",
|
"filesystem.getPathParts",
|
||||||
"filesystem.createDirectory",
|
"filesystem.createDirectory",
|
||||||
"filesystem.getStats",
|
"filesystem.getStats",
|
||||||
|
"storage.*",
|
||||||
"debug.log"
|
"debug.log"
|
||||||
],
|
],
|
||||||
"modes": {
|
"modes": {
|
||||||
|
|||||||
@@ -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:37503/__neutralino_globals.js"></script>
|
<script src="http://localhost:35731/__neutralino_globals.js"></script>
|
||||||
<title>Vencoder</title>
|
<title>Vencoder</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
+73
-39
@@ -13,6 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
generateOutputCommand,
|
generateOutputCommand,
|
||||||
getAvailableCodecs,
|
getAvailableCodecs,
|
||||||
|
getLengthMicroseconds,
|
||||||
playFile,
|
playFile,
|
||||||
videoFileExtensions,
|
videoFileExtensions,
|
||||||
type CodecInfo,
|
type CodecInfo,
|
||||||
@@ -26,6 +27,12 @@ import TrashEmpty from "./assets/breeze/actions/16/trash-empty.svg";
|
|||||||
|
|
||||||
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
|
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
|
||||||
|
|
||||||
|
interface RunningProcessInfo {
|
||||||
|
process: Neutralino.os.SpawnedProcess;
|
||||||
|
file: string;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [windowFocused, setWindowFocused] = createSignal(true);
|
const [windowFocused, setWindowFocused] = createSignal(true);
|
||||||
const [displayedCodecs, setDisplayedCodecs]: Signal<CodecInfo[]> =
|
const [displayedCodecs, setDisplayedCodecs]: Signal<CodecInfo[]> =
|
||||||
@@ -40,10 +47,9 @@ function App() {
|
|||||||
const [showCommonCodecs, setShowCommonCodecs] = createSignal(true);
|
const [showCommonCodecs, setShowCommonCodecs] = createSignal(true);
|
||||||
const [selectedCodec, setSelectedCodec] = createSignal<CodecInfo>();
|
const [selectedCodec, setSelectedCodec] = createSignal<CodecInfo>();
|
||||||
const [selectedEncoder, setSelectedEncoder] = createSignal("");
|
const [selectedEncoder, setSelectedEncoder] = createSignal("");
|
||||||
const [runningProcess, setRunningProcess] = createSignal<{
|
const [runningProcesses, setRunningProcesses] = createSignal<
|
||||||
process: Neutralino.os.SpawnedProcess;
|
RunningProcessInfo[]
|
||||||
params: FFmpegParams;
|
>([]);
|
||||||
}>();
|
|
||||||
let supportedCodecs: CodecInfo[] = [];
|
let supportedCodecs: CodecInfo[] = [];
|
||||||
let ffmpegParams: FFmpegParams = { vcodec: "" };
|
let ffmpegParams: FFmpegParams = { vcodec: "" };
|
||||||
let successfulCount = 0;
|
let successfulCount = 0;
|
||||||
@@ -59,35 +65,32 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSpawnedProcessEvents(evt: CustomEvent) {
|
function handleSpawnedProcessEvents(evt: CustomEvent) {
|
||||||
if (runningProcess()?.process.id !== evt.detail.id) return;
|
if (evt.detail.action !== "exit") return;
|
||||||
|
|
||||||
switch (evt.detail.action) {
|
if (evt.detail.data === 0) {
|
||||||
case "stdOut":
|
successfulCount += 1;
|
||||||
console.log(evt.detail.data);
|
} else {
|
||||||
break;
|
unsuccessfulCount += 1;
|
||||||
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) {
|
if (evt.detail.data !== 255) {
|
||||||
Neutralino.os.showNotification(
|
Neutralino.os.showNotification(
|
||||||
"File(s) encoded.",
|
"File Encoding Failed",
|
||||||
`${successfulCount} files encoded successfully. ${unsuccessfulCount} failed.`,
|
`Encoding for file "${runningProcesses()?.find((v) => v.process.id == evt.detail.id)?.file}" failed. Exit code ${evt.detail.data}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`FFmpeg exited with code: ${evt.detail.data}`);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (successfulCount + unsuccessfulCount === totalCount) {
|
||||||
|
Neutralino.os.showNotification(
|
||||||
|
"File(s) encoded.",
|
||||||
|
`${successfulCount} files encoded successfully. ${unsuccessfulCount} failed or cancelled.`,
|
||||||
|
);
|
||||||
|
successfulCount = 0;
|
||||||
|
unsuccessfulCount = 0;
|
||||||
|
totalCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`FFmpeg exited with code: ${evt.detail.data}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@@ -221,7 +224,9 @@ function App() {
|
|||||||
setOutputCommand(generateOutputCommand(ffmpegParams));
|
setOutputCommand(generateOutputCommand(ffmpegParams));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function convertClip(clip: string) {
|
async function convertClip(
|
||||||
|
clip: string,
|
||||||
|
): Promise<RunningProcessInfo | undefined> {
|
||||||
ffmpegParams.inputFile = clip;
|
ffmpegParams.inputFile = clip;
|
||||||
|
|
||||||
const fileName = (await Neutralino.filesystem.getPathParts(clip)).stem;
|
const fileName = (await Neutralino.filesystem.getPathParts(clip)).stem;
|
||||||
@@ -264,12 +269,15 @@ function App() {
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
setRunningProcess({
|
const length = await getLengthMicroseconds(clip);
|
||||||
|
|
||||||
|
return {
|
||||||
process: await Neutralino.os.spawnProcess(
|
process: await Neutralino.os.spawnProcess(
|
||||||
generateOutputCommand(ffmpegParams),
|
generateOutputCommand(ffmpegParams),
|
||||||
),
|
),
|
||||||
params: { ...ffmpegParams },
|
file: clip,
|
||||||
});
|
length,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertAllClicked() {
|
async function convertAllClicked() {
|
||||||
@@ -277,13 +285,39 @@ function App() {
|
|||||||
|
|
||||||
totalCount = list.length;
|
totalCount = list.length;
|
||||||
|
|
||||||
for (const clip of list) {
|
const processes = (await Promise.all(list.map(convertClip))).filter(
|
||||||
convertClip(clip);
|
(v) => v !== undefined,
|
||||||
}
|
);
|
||||||
|
|
||||||
|
setRunningProcesses(processes);
|
||||||
|
|
||||||
|
await Neutralino.storage.setData(
|
||||||
|
"filesBeingProcessed",
|
||||||
|
JSON.stringify(
|
||||||
|
processes.map((v) => ({
|
||||||
|
id: v.process.id,
|
||||||
|
in: v.file,
|
||||||
|
len: v.length,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Neutralino.window.create(`${window.location.href}progress`, {
|
||||||
|
width: 600,
|
||||||
|
height: 400,
|
||||||
|
x: 120,
|
||||||
|
y: 120,
|
||||||
|
injectGlobals: true,
|
||||||
|
maximizable: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertSelectedClicked() {
|
async function convertSelectedClicked() {
|
||||||
convertClip(selectedClip());
|
const result = await convertClip(selectedClip());
|
||||||
|
|
||||||
|
if (result !== undefined) {
|
||||||
|
setRunningProcesses([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -206,3 +206,7 @@ input[type="checkbox"] {
|
|||||||
.p-medium {
|
.p-medium {
|
||||||
padding: var(--k-medium-spacing);
|
padding: var(--k-medium-spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-grid {
|
||||||
|
padding: var(--k-grid-unit);
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ h2 {
|
|||||||
margin: 0 0 0.25em 0;
|
margin: 0 0 0.25em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.h-full {
|
.h-full {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { clamp } from "./util/math.ts";
|
|||||||
import convert, { type RGB } from "color-convert";
|
import convert, { type RGB } from "color-convert";
|
||||||
import { Route, Router } from "@solidjs/router";
|
import { Route, Router } from "@solidjs/router";
|
||||||
import Settings from "./pages/Settings.tsx";
|
import Settings from "./pages/Settings.tsx";
|
||||||
|
import ProgressPage from "./pages/ProgressPage.tsx";
|
||||||
|
|
||||||
const root = document.getElementById("root");
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ render(
|
|||||||
<Router>
|
<Router>
|
||||||
<Route path="/" component={App} />
|
<Route path="/" component={App} />
|
||||||
<Route path="/settings" component={Settings} />
|
<Route path="/settings" component={Settings} />
|
||||||
|
<Route path="/progress" component={ProgressPage} />
|
||||||
</Router>
|
</Router>
|
||||||
),
|
),
|
||||||
root!,
|
root!,
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { events, os, storage } from "@neutralinojs/lib";
|
||||||
|
import { createSignal, onMount, onCleanup, Show, Index } from "solid-js";
|
||||||
|
|
||||||
|
interface TargetFile {
|
||||||
|
id: string;
|
||||||
|
in: string;
|
||||||
|
len: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgressInfo {
|
||||||
|
filename: string;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FFmpegProgressInfo {
|
||||||
|
bitrate: string;
|
||||||
|
drop_frames: string;
|
||||||
|
dup_frames: string;
|
||||||
|
fps: string;
|
||||||
|
frame: string;
|
||||||
|
out_time: string;
|
||||||
|
out_time_ms: string;
|
||||||
|
out_time_us: string;
|
||||||
|
progress: string;
|
||||||
|
speed: string;
|
||||||
|
total_size: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProgressPage() {
|
||||||
|
const [windowFocused, setWindowFocused] = createSignal(true);
|
||||||
|
const [runningProcesses, setRunningProcesses] = createSignal<
|
||||||
|
os.SpawnedProcess[]
|
||||||
|
>([]);
|
||||||
|
const [finished, setFinished] = createSignal(false);
|
||||||
|
const [fileInfo, setFileInfo] = createSignal<TargetFile[]>([]);
|
||||||
|
const progressObject: {
|
||||||
|
[key: string]: ProgressInfo;
|
||||||
|
} = {};
|
||||||
|
const [progressList, setProgressList] = createSignal<ProgressInfo[]>([]);
|
||||||
|
const [isCancelling, setIsCancelling] = createSignal(false);
|
||||||
|
|
||||||
|
function windowIsFocused() {
|
||||||
|
setWindowFocused(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowUnfocused() {
|
||||||
|
setWindowFocused(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSpawnedProcessEvents(evt: CustomEvent) {
|
||||||
|
switch (evt.detail.action) {
|
||||||
|
case "stdOut":
|
||||||
|
const info: FFmpegProgressInfo = Object.fromEntries(
|
||||||
|
(evt.detail.data as string)
|
||||||
|
.split("\n")
|
||||||
|
.map((v) => v.split("=")),
|
||||||
|
);
|
||||||
|
const file = fileInfo().find((v) => v.id === evt.detail.id);
|
||||||
|
|
||||||
|
if (file === undefined) return;
|
||||||
|
|
||||||
|
progressObject[evt.detail.id] = {
|
||||||
|
filename: file.in,
|
||||||
|
percentage: (parseInt(info.out_time_us) / file.len) * 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
setProgressList(Object.values(progressObject));
|
||||||
|
break;
|
||||||
|
case "stdErr":
|
||||||
|
break;
|
||||||
|
case "exit":
|
||||||
|
console.log(`FFmpeg exited with code: ${evt.detail.data}`);
|
||||||
|
os.getSpawnedProcesses().then((processes) => {
|
||||||
|
if (processes.length === 0) {
|
||||||
|
setFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRunningProcesses(processes);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
events.on("windowFocus", windowIsFocused);
|
||||||
|
events.on("windowBlur", windowUnfocused);
|
||||||
|
events.on("spawnedProcess", handleSpawnedProcessEvents);
|
||||||
|
|
||||||
|
const processes = await os.getSpawnedProcesses();
|
||||||
|
setRunningProcesses(processes);
|
||||||
|
|
||||||
|
const storedFileInfo: TargetFile[] = JSON.parse(
|
||||||
|
await storage.getData("filesBeingProcessed"),
|
||||||
|
);
|
||||||
|
setFileInfo(storedFileInfo);
|
||||||
|
|
||||||
|
for (let i = 0; i < processes.length; i++) {
|
||||||
|
progressObject[processes[i].id] = {
|
||||||
|
filename: storedFileInfo[i].in,
|
||||||
|
percentage: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgressList(Object.values(progressObject));
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
events.off("windowFocus", windowIsFocused);
|
||||||
|
events.off("windowBlur", windowUnfocused);
|
||||||
|
events.off("spawnedProcess", handleSpawnedProcessEvents);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function cancelBtnClicked() {
|
||||||
|
setIsCancelling(true);
|
||||||
|
|
||||||
|
const processes = runningProcesses();
|
||||||
|
|
||||||
|
for (const process of processes) {
|
||||||
|
try {
|
||||||
|
await os.updateSpawnedProcess(process.id, "exit");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main class="row flex-col">
|
||||||
|
<div class="container row flex-col" style={{ flex: "1" }}>
|
||||||
|
<header
|
||||||
|
class={`k-page-header ${windowFocused() ? "" : "window-blur"}`}
|
||||||
|
>
|
||||||
|
<div class="page-title" role="heading">
|
||||||
|
Progress
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
class="p-grid col row flex-col"
|
||||||
|
style={{ overflow: "scroll" }}
|
||||||
|
>
|
||||||
|
<Show when={finished()}>
|
||||||
|
Processes finished. You can close this window.
|
||||||
|
</Show>
|
||||||
|
<Index each={progressList()}>
|
||||||
|
{(item, _) => (
|
||||||
|
<div
|
||||||
|
class="row flex-col"
|
||||||
|
style={{
|
||||||
|
"padding-bottom": "var(--k-grid-unit)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label>{item().filename}</label>
|
||||||
|
<div
|
||||||
|
class="grid"
|
||||||
|
style={{
|
||||||
|
"grid-template-columns": "90% 10%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="row justify-content-center align-items-center">
|
||||||
|
<progress
|
||||||
|
class="col"
|
||||||
|
value={item().percentage}
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-content-center align-items-center">
|
||||||
|
{Math.min(
|
||||||
|
Math.round(item().percentage),
|
||||||
|
100,
|
||||||
|
)}
|
||||||
|
%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Index>
|
||||||
|
</div>
|
||||||
|
<Show when={!finished()}>
|
||||||
|
<footer
|
||||||
|
class="p-medium row"
|
||||||
|
style={{ "align-items": "end" }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="k-button"
|
||||||
|
disabled={isCancelling()}
|
||||||
|
onclick={cancelBtnClicked}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProgressPage;
|
||||||
@@ -127,3 +127,18 @@ ffmpeg -y -hwaccel auto ${commonOpts} ${
|
|||||||
params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`
|
params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`
|
||||||
} -progress - "${params.outputFile ?? "{output}"}"`;
|
} -progress - "${params.outputFile ?? "{output}"}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getLengthMicroseconds(target: string) {
|
||||||
|
const result = await Neutralino.os.execCommand(
|
||||||
|
`ffprobe -v quiet -of json=c=1 -show_entries format=duration -sexagesimal "${target}"`,
|
||||||
|
);
|
||||||
|
const rawDuration = JSON.parse(result.stdOut)["format"]["duration"].split(
|
||||||
|
":",
|
||||||
|
) as string[];
|
||||||
|
|
||||||
|
const hours = parseInt(rawDuration[0]);
|
||||||
|
const minutes = hours * 60 + parseInt(rawDuration[1]);
|
||||||
|
const seconds = minutes * 60 + parseFloat(rawDuration[2]);
|
||||||
|
|
||||||
|
return Math.trunc(seconds * 1000000);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user