Show FFmpeg logs after error, Improve icons in dark mode, Improve "Convert Selected" button
@@ -21,9 +21,11 @@ import {
|
||||
} 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";
|
||||
import { openFile } from "./util/oshelper";
|
||||
import { getTemporaryFilePath } from "./util/path";
|
||||
import { generateRandomString } from "./util/string";
|
||||
import "./css/icons.css";
|
||||
import BreezeIcon from "./components/BreezeIcon";
|
||||
|
||||
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
|
||||
|
||||
@@ -50,6 +52,7 @@ function App() {
|
||||
const [runningProcesses, setRunningProcesses] = createSignal<
|
||||
RunningProcessInfo[]
|
||||
>([]);
|
||||
const logs: { [id: number]: string[] } = {};
|
||||
let supportedCodecs: CodecInfo[] = [];
|
||||
let ffmpegParams: FFmpegParams = { vcodec: "" };
|
||||
let successfulCount = 0;
|
||||
@@ -65,32 +68,45 @@ function App() {
|
||||
}
|
||||
|
||||
function handleSpawnedProcessEvents(evt: CustomEvent) {
|
||||
if (evt.detail.action !== "exit") return;
|
||||
switch (evt.detail.action) {
|
||||
case "stdErr":
|
||||
logs[evt.detail.id].push(evt.detail.data);
|
||||
break;
|
||||
case "exit":
|
||||
if (evt.detail.data === 0) {
|
||||
successfulCount += 1;
|
||||
} else {
|
||||
unsuccessfulCount += 1;
|
||||
|
||||
if (evt.detail.data === 0) {
|
||||
successfulCount += 1;
|
||||
} else {
|
||||
unsuccessfulCount += 1;
|
||||
// If the exit code isn't 255 (the exit code of the program exiting because of cancellation)
|
||||
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 (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}.`,
|
||||
);
|
||||
}
|
||||
const tempFilename = `${getTemporaryFilePath()}/vencoder-ffmpeg-${generateRandomString(8)}.log`;
|
||||
Neutralino.filesystem.writeFile(
|
||||
tempFilename,
|
||||
logs[evt.detail.id].join("\n"),
|
||||
);
|
||||
openFile(tempFilename);
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
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 () => {
|
||||
@@ -291,6 +307,8 @@ function App() {
|
||||
|
||||
setRunningProcesses(processes);
|
||||
|
||||
processes.forEach((v) => (logs[v.process.id] = []));
|
||||
|
||||
await Neutralino.storage.setData(
|
||||
"filesBeingProcessed",
|
||||
JSON.stringify(
|
||||
@@ -315,9 +333,37 @@ function App() {
|
||||
async function convertSelectedClicked() {
|
||||
const result = await convertClip(selectedClip());
|
||||
|
||||
if (result !== undefined) {
|
||||
setRunningProcesses([]);
|
||||
if (result === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(result);
|
||||
|
||||
totalCount = 1;
|
||||
|
||||
setRunningProcesses([result]);
|
||||
|
||||
logs[result.process.id] = [];
|
||||
|
||||
await Neutralino.storage.setData(
|
||||
"filesBeingProcessed",
|
||||
JSON.stringify([
|
||||
{
|
||||
id: result.process.id,
|
||||
in: result.file,
|
||||
len: result.length,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await Neutralino.window.create(`${window.location.href}progress`, {
|
||||
width: 600,
|
||||
height: 400,
|
||||
x: 120,
|
||||
y: 120,
|
||||
injectGlobals: true,
|
||||
maximizable: false,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -370,8 +416,8 @@ function App() {
|
||||
onclick={removeBtnClicked}
|
||||
class="icon-button k-button"
|
||||
>
|
||||
<img
|
||||
src={TrashEmpty}
|
||||
<BreezeIcon
|
||||
icon="b b-trash-empty"
|
||||
alt="Remove Selected Video"
|
||||
/>
|
||||
</button>
|
||||
@@ -380,8 +426,8 @@ function App() {
|
||||
onclick={playBtnClicked}
|
||||
class="icon-button k-button"
|
||||
>
|
||||
<img
|
||||
src={PlaybackStart}
|
||||
<BreezeIcon
|
||||
icon="playback-start"
|
||||
alt="Preview Selected Video"
|
||||
/>
|
||||
</button>
|
||||
@@ -389,9 +435,9 @@ function App() {
|
||||
class="icon-button k-button"
|
||||
onclick={settingsBtnPressed}
|
||||
>
|
||||
<img
|
||||
src={Configure}
|
||||
alt="Configure Vencoder"
|
||||
<BreezeIcon
|
||||
icon="configure"
|
||||
alt="Configure"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
|
||||
</defs>
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 10 2.5 C 9.0680195 2.5 8.2844627 3.1373007 8.0625 4 L 2 4 L 2 5 L 8.0625 5 C 8.2844627 5.8626993 9.0680195 6.5 10 6.5 C 10.931981 6.5 11.715537 5.8626993 11.9375 5 L 14 5 L 14 4 L 11.9375 4 C 11.715537 3.1373007 10.931981 2.5 10 2.5 z M 5 9.5 C 4.0680191 9.5 3.2844626 10.137301 3.0625 11 L 2 11 L 2 12 L 3.0625 12 C 3.2844626 12.862699 4.0680191 13.5 5 13.5 C 5.9319809 13.5 6.7155374 12.862699 6.9375 12 L 7 12 L 9 12 L 14 12 L 14 11 L 9 11 L 7 11 L 6.9375 11 C 6.7155374 10.137301 5.9319809 9.5 5 9.5 z M 5 10.5 C 5.55228 10.5 6 10.94772 6 11.5 C 6 12.05228 5.55228 12.5 5 12.5 C 4.44772 12.5 4 12.05228 4 11.5 C 4 10.94772 4.44772 10.5 5 10.5 z " class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 983 B |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
|
||||
<g class="ColorScheme-Text" fill="currentColor" fill-rule="evenodd">
|
||||
<path d="m8 2a6 6 0 0 0 -6 6 6 6 0 0 0 6 6 6 6 0 0 0 6-6 6 6 0 0 0 -6-6zm0 1a5 5 0 0 1 5 5 5 5 0 0 1 -5 5 5 5 0 0 1 -5-5 5 5 0 0 1 5-5z"/>
|
||||
<path d="m7 4h2v2h-2z"/>
|
||||
<path d="m7 7h2v5h-2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 502 B |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
|
||||
<path d="m2 2v12l12-6z" class="ColorScheme-Text" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 282 B |
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
|
||||
</defs>
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="m5 2v2h1v-1h4v1h1v-2h-5zm-3 3v1h2v8h8v-8h2v-1zm3 1h6v7h-6z" class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 390 B |
@@ -1,13 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="M 10 2.5 C 9.0680195 2.5 8.2844627 3.1373007 8.0625 4 L 2 4 L 2 5 L 8.0625 5 C 8.2844627 5.8626993 9.0680195 6.5 10 6.5 C 10.931981 6.5 11.715537 5.8626993 11.9375 5 L 14 5 L 14 4 L 11.9375 4 C 11.715537 3.1373007 10.931981 2.5 10 2.5 z M 5 9.5 C 4.0680191 9.5 3.2844626 10.137301 3.0625 11 L 2 11 L 2 12 L 3.0625 12 C 3.2844626 12.862699 4.0680191 13.5 5 13.5 C 5.9319809 13.5 6.7155374 12.862699 6.9375 12 L 7 12 L 9 12 L 14 12 L 14 11 L 9 11 L 7 11 L 6.9375 11 C 6.7155374 10.137301 5.9319809 9.5 5 9.5 z M 5 10.5 C 5.55228 10.5 6 10.94772 6 11.5 C 6 12.05228 5.55228 12.5 5 12.5 C 4.44772 12.5 4 12.05228 4 11.5 C 4 10.94772 4.44772 10.5 5 10.5 z "
|
||||
class="ColorScheme-Text"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 989 B |
@@ -1,12 +0,0 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
<g class="ColorScheme-Text" fill="currentColor" fill-rule="evenodd">
|
||||
<path d="m8 2a6 6 0 0 0 -6 6 6 6 0 0 0 6 6 6 6 0 0 0 6-6 6 6 0 0 0 -6-6zm0 1a5 5 0 0 1 5 5 5 5 0 0 1 -5 5 5 5 0 0 1 -5-5 5 5 0 0 1 5-5z"/>
|
||||
<path d="m7 4h2v2h-2z"/>
|
||||
<path d="m7 7h2v5h-2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 495 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
<path d="m2 2v12l12-6z" class="ColorScheme-Text" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 275 B |
@@ -1,13 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="m5 2v2h1v-1h4v1h1v-2h-5zm-3 3v1h2v8h8v-8h2v-1zm3 1h6v7h-6z"
|
||||
class="ColorScheme-Text"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 394 B |
@@ -0,0 +1,3 @@
|
||||
export default function BreezeIcon(props: { icon: string; alt: string }) {
|
||||
return <i class={`b b-${props.icon}`} role="img" aria-label={props.alt} />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import type { CodecInfo, FFmpegParams } from "../util/ffmpeg";
|
||||
import HelpAbout from "../assets/breeze/actions/16/help-about.svg";
|
||||
import { os } from "@neutralinojs/lib";
|
||||
import BreezeIcon from "./BreezeIcon";
|
||||
|
||||
const information = {
|
||||
h264: {
|
||||
@@ -21,6 +21,10 @@ function H264Options(props: {
|
||||
onParamChanged: (key: string, value: any) => void;
|
||||
}) {
|
||||
const [twopass, setTwopass] = createSignal(false);
|
||||
const defaultCrf =
|
||||
props.codec?.shortName === "h264"
|
||||
? information.h264.defaultCrf
|
||||
: information.hevc.defaultCrf;
|
||||
|
||||
return (
|
||||
<section id="commonLossyOptions">
|
||||
@@ -52,7 +56,7 @@ function H264Options(props: {
|
||||
}
|
||||
title="This will use the two-pass rate control mode instead of relying on a Constant Rate Factor (CRF) value."
|
||||
>
|
||||
<img src={HelpAbout} />
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
<label>Preset</label>
|
||||
@@ -91,7 +95,9 @@ function H264Options(props: {
|
||||
type="number"
|
||||
name="crf"
|
||||
id="crf"
|
||||
value={props.params.crf ?? "23"}
|
||||
min="0"
|
||||
max="51"
|
||||
value={props.params.crf ?? defaultCrf}
|
||||
oninput={(e) => {
|
||||
props.params.crf = parseInt(e.target.value);
|
||||
props.onParamChanged(
|
||||
@@ -149,7 +155,7 @@ function H264Options(props: {
|
||||
}
|
||||
title="This will move some information to the beginning of your file and allow the video to begin playing before it is completely downloaded by the viewer, recommended for web videos. Click for more information."
|
||||
>
|
||||
<img src={HelpAbout} />
|
||||
<BreezeIcon icon="help-about" alt="Help" />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
.b {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.b-trash-empty {
|
||||
background-image: url("/breeze/actions/16/trash-empty.svg");
|
||||
}
|
||||
|
||||
.b-configure {
|
||||
background-image: url("/breeze/actions/16/configure.svg");
|
||||
}
|
||||
|
||||
.b-playback-start {
|
||||
background-image: url("/breeze/actions/16/media-playback-start.svg");
|
||||
}
|
||||
|
||||
.b-help-about {
|
||||
background-image: url("/breeze/actions/16/help-about.svg");
|
||||
}
|
||||
|
||||
@media screen and (prefers-color-scheme: dark) {
|
||||
.b-trash-empty {
|
||||
background-image: url("/breeze-dark/actions/16/trash-empty.svg");
|
||||
}
|
||||
|
||||
.b-configure {
|
||||
background-image: url("/breeze-dark/actions/16/configure.svg");
|
||||
}
|
||||
|
||||
.b-playback-start {
|
||||
background-image: url("/breeze-dark/actions/16/media-playback-start.svg");
|
||||
}
|
||||
|
||||
.b-help-about {
|
||||
background-image: url("/breeze-dark/actions/16/help-about.svg");
|
||||
}
|
||||
}
|
||||
@@ -75,12 +75,14 @@ h2 {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
img,
|
||||
i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:disabled img {
|
||||
&:disabled img,
|
||||
&:disabled i {
|
||||
filter: invert(50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +64,17 @@ function ProgressPage() {
|
||||
percentage: (parseInt(info.out_time_us) / file.len) * 100,
|
||||
};
|
||||
|
||||
if (Number.isNaN(progressObject[evt.detail.id].percentage)) {
|
||||
progressObject[evt.detail.id].percentage = 0;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { os } from "@neutralinojs/lib";
|
||||
|
||||
const openFilePrograms: { [os: string]: string } = {
|
||||
Linux: "xdg-open",
|
||||
Windows: "explorer",
|
||||
Darwin: "open",
|
||||
};
|
||||
|
||||
export function openFile(path: string) {
|
||||
let program = openFilePrograms[window.NL_OS] ?? "";
|
||||
|
||||
return os.execCommand(`${program} ${path}`);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function getTemporaryFilePath() {
|
||||
switch (window.NL_OS) {
|
||||
case "Windows":
|
||||
return "%temp%";
|
||||
case "Linux":
|
||||
return "/tmp";
|
||||
default:
|
||||
return ".";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Code originally from: https://stackoverflow.com/a/1349426/14512055
|
||||
export function generateRandomString(length: number) {
|
||||
let result = "";
|
||||
let characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let charactersLength = characters.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(
|
||||
Math.floor(Math.random() * charactersLength),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||