diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index a0899bb..0000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -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']
diff --git a/.gitignore b/.gitignore
index 4a3dc9d..949575a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ neutralino.js
# Neutralinojs related files
.storage
*.log
+.tmp
diff --git a/neutralino.config.json b/neutralino.config.json
index 88248ae..b8f0163 100644
--- a/neutralino.config.json
+++ b/neutralino.config.json
@@ -17,6 +17,7 @@
"filesystem.getPathParts",
"filesystem.createDirectory",
"filesystem.getStats",
+ "storage.*",
"debug.log"
],
"modes": {
diff --git a/solid-src/index.html b/solid-src/index.html
index b237359..e22958f 100644
--- a/solid-src/index.html
+++ b/solid-src/index.html
@@ -5,7 +5,7 @@
-
+
Vencoder
diff --git a/solid-src/src/App.tsx b/solid-src/src/App.tsx
index b47ca4c..c9c9961 100644
--- a/solid-src/src/App.tsx
+++ b/solid-src/src/App.tsx
@@ -13,6 +13,7 @@ import {
import {
generateOutputCommand,
getAvailableCodecs,
+ getLengthMicroseconds,
playFile,
videoFileExtensions,
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"]);
+interface RunningProcessInfo {
+ process: Neutralino.os.SpawnedProcess;
+ file: string;
+ length: number;
+}
+
function App() {
const [windowFocused, setWindowFocused] = createSignal(true);
const [displayedCodecs, setDisplayedCodecs]: Signal =
@@ -40,10 +47,9 @@ function App() {
const [showCommonCodecs, setShowCommonCodecs] = createSignal(true);
const [selectedCodec, setSelectedCodec] = createSignal();
const [selectedEncoder, setSelectedEncoder] = createSignal("");
- const [runningProcess, setRunningProcess] = createSignal<{
- process: Neutralino.os.SpawnedProcess;
- params: FFmpegParams;
- }>();
+ const [runningProcesses, setRunningProcesses] = createSignal<
+ RunningProcessInfo[]
+ >([]);
let supportedCodecs: CodecInfo[] = [];
let ffmpegParams: FFmpegParams = { vcodec: "" };
let successfulCount = 0;
@@ -59,35 +65,32 @@ function App() {
}
function handleSpawnedProcessEvents(evt: CustomEvent) {
- if (runningProcess()?.process.id !== evt.detail.id) return;
+ if (evt.detail.action !== "exit") 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 (evt.detail.data === 0) {
+ successfulCount += 1;
+ } else {
+ unsuccessfulCount += 1;
- 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;
+ if (evt.detail.data !== 255) {
+ Neutralino.os.showNotification(
+ "File Encoding Failed",
+ `Encoding for file "${runningProcesses()?.find((v) => v.process.id == evt.detail.id)?.file}" failed. Exit code ${evt.detail.data}.`,
+ );
+ }
}
+
+ 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 () => {
@@ -221,7 +224,9 @@ function App() {
setOutputCommand(generateOutputCommand(ffmpegParams));
});
- async function convertClip(clip: string) {
+ async function convertClip(
+ clip: string,
+ ): Promise {
ffmpegParams.inputFile = clip;
const fileName = (await Neutralino.filesystem.getPathParts(clip)).stem;
@@ -264,12 +269,15 @@ function App() {
}
} catch (e) {}
- setRunningProcess({
+ const length = await getLengthMicroseconds(clip);
+
+ return {
process: await Neutralino.os.spawnProcess(
generateOutputCommand(ffmpegParams),
),
- params: { ...ffmpegParams },
- });
+ file: clip,
+ length,
+ };
}
async function convertAllClicked() {
@@ -277,13 +285,39 @@ function App() {
totalCount = list.length;
- for (const clip of list) {
- convertClip(clip);
- }
+ const processes = (await Promise.all(list.map(convertClip))).filter(
+ (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() {
- convertClip(selectedClip());
+ async function convertSelectedClicked() {
+ const result = await convertClip(selectedClip());
+
+ if (result !== undefined) {
+ setRunningProcesses([]);
+ }
}
return (
diff --git a/solid-src/src/css/Kirigami.css b/solid-src/src/css/Kirigami.css
index 38f74c5..9eb8d76 100644
--- a/solid-src/src/css/Kirigami.css
+++ b/solid-src/src/css/Kirigami.css
@@ -206,3 +206,7 @@ input[type="checkbox"] {
.p-medium {
padding: var(--k-medium-spacing);
}
+
+.p-grid {
+ padding: var(--k-grid-unit);
+}
diff --git a/solid-src/src/css/index.css b/solid-src/src/css/index.css
index d8353be..4d10e6c 100644
--- a/solid-src/src/css/index.css
+++ b/solid-src/src/css/index.css
@@ -62,6 +62,10 @@ h2 {
margin: 0 0 0.25em 0;
}
+.w-full {
+ width: 100%;
+}
+
.h-full {
height: 100%;
}
diff --git a/solid-src/src/index.tsx b/solid-src/src/index.tsx
index b86d5c3..8ac3ce2 100644
--- a/solid-src/src/index.tsx
+++ b/solid-src/src/index.tsx
@@ -8,6 +8,7 @@ import { clamp } from "./util/math.ts";
import convert, { type RGB } from "color-convert";
import { Route, Router } from "@solidjs/router";
import Settings from "./pages/Settings.tsx";
+import ProgressPage from "./pages/ProgressPage.tsx";
const root = document.getElementById("root");
@@ -44,6 +45,7 @@ render(
+
),
root!,
diff --git a/solid-src/src/pages/ProgressPage.tsx b/solid-src/src/pages/ProgressPage.tsx
new file mode 100644
index 0000000..991ad8d
--- /dev/null
+++ b/solid-src/src/pages/ProgressPage.tsx
@@ -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([]);
+ const progressObject: {
+ [key: string]: ProgressInfo;
+ } = {};
+ const [progressList, setProgressList] = createSignal([]);
+ 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 (
+
+
+
+
+
+ Processes finished. You can close this window.
+
+
+ {(item, _) => (
+
+
{item().filename}
+
+
+
+ {Math.min(
+ Math.round(item().percentage),
+ 100,
+ )}
+ %
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+export default ProgressPage;
diff --git a/solid-src/src/util/ffmpeg.ts b/solid-src/src/util/ffmpeg.ts
index 5a7196b..b8f13be 100644
--- a/solid-src/src/util/ffmpeg.ts
+++ b/solid-src/src/util/ffmpeg.ts
@@ -127,3 +127,18 @@ ffmpeg -y -hwaccel auto ${commonOpts} ${
params.abitrate === undefined ? "" : ` -b:a ${params.abitrate}k`
} -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);
+}