web/remux: convert to a web worker (wip)

This commit is contained in:
wukko 2025-01-15 22:11:08 +06:00
parent 28d8927c08
commit 1ed2eef65a
No known key found for this signature in database
GPG key ID: 3E30B3F26C7B4AA2
5 changed files with 198 additions and 130 deletions

View file

@ -39,7 +39,7 @@ export default class LibAVWrapper {
try {
await libav.ffprobe([
'-v', 'quiet',
//'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams',

View file

@ -32,7 +32,7 @@ export const maskImage = async (source: Blob, mask: RawImage) => {
return canvas;
}
export const removeImageBackground = async (file: File) => {
const removeImageBackground = async (file: File) => {
const originalImageBlob = new Blob([file]);
const image = await RawImage.fromBlob(originalImageBlob);

View file

@ -0,0 +1,89 @@
import mime from "mime";
import LibAVWrapper from "$lib/libav";
const error = (code: string) => {
return {
error: `error.${code}`,
}
}
const ff = new LibAVWrapper((progress) => {
self.postMessage({
progress: {
durationProcessed: progress.out_time_sec,
speed: progress.speed,
}
})
});
ff.init();
const remux = async (file: File) => {
if (!file) return;
await ff.init();
try {
const file_info = await ff.probe(file).catch((e) => {
if (e?.message?.toLowerCase().includes("out of memory")) {
console.error("uh oh! out of memory");
console.error(e);
self.postMessage(error("remux.out_of_resources"));
}
});
if (!file_info?.format) {
self.postMessage(error("remux.corrupted"));
return;
}
self.postMessage({
progressInfo: {
duration: Number(file_info.format.duration),
}
});
if (file instanceof File && !file.type) {
file = new File([file], file.name, {
type: mime.getType(file.name) ?? undefined,
});
}
const render = await ff
.render({
blob: file,
args: ["-c", "copy", "-map", "0"],
})
.catch((e) => {
console.error("uh-oh! render error");
console.error(e);
self.postMessage(error("remux.out_of_resources"));
});
if (!render) {
return console.log("not a valid file");
}
const filenameParts = file.name.split(".");
const filenameExt = filenameParts.pop();
const filename = `${filenameParts.join(".")} (remux).${filenameExt}`;
self.postMessage({
render,
filename
});
} catch (e) {
console.log(e);
}
}
self.onmessage = async (event: MessageEvent) => {
console.log(event.data);
if (event.data.file) {
await remux(event.data.file);
}
}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import mime from "mime";
import LibAVWrapper from "$lib/libav";
import RemuxWorker from "$lib/workers/remux?worker";
import { beforeNavigate, goto } from "$app/navigation";
import { t } from "$lib/i18n/translations";
@ -19,145 +19,88 @@
let draggedOver = false;
let file: File | undefined;
let totalDuration: number | undefined;
let processedDuration: number | undefined;
let speed: number | undefined;
let progress: string | undefined;
let currentProgress: string | undefined;
let wentAway = false;
$: {
if (totalDuration && processedDuration) {
const percentage = Math.max(
0,
Math.min(100, (processedDuration / totalDuration) * 100)
).toFixed(2);
progress = percentage;
} else if (processing) {
progress = undefined;
speed = undefined;
} else {
progress = undefined;
speed = undefined;
}
}
let processing = false;
const ff = new LibAVWrapper((progress) => {
if (progress.out_time_sec) {
processedDuration = progress.out_time_sec;
}
let worker: Worker;
if (progress.speed) {
speed = progress.speed;
}
});
const remux = async () => {
worker = new RemuxWorker();
ff.init();
speed = undefined;
progress = undefined;
currentProgress = undefined;
const render = async () => {
if (!file || processing) return;
await ff.init();
worker.postMessage({ file });
processing = true;
let dialogOpened;
try {
progress = undefined;
speed = undefined;
processing = true;
worker.onerror = (e) => {
console.error("remux worker exploded:", e);
worker.terminate();
};
const file_info = await ff.probe(file).catch((e) => {
if (e?.message?.toLowerCase().includes("out of memory")) {
console.error("uh oh! out of memory");
console.error(e);
worker.onmessage = (event) => {
console.log(event.data);
dialogOpened = true;
return createDialog({
id: "remux-error",
type: "small",
meowbalt: "error",
bodyText: $t("error.remux.out_of_resources"),
buttons: [
{
text: $t("button.gotit"),
main: true,
action: () => {},
},
],
});
if (event.data.progress) {
let eprogress = event.data.progress;
if (eprogress?.speed) {
speed = eprogress.speed;
}
});
if (!file_info?.format) {
if (!dialogOpened)
return createDialog({
id: "remux-error",
type: "small",
meowbalt: "error",
bodyText: $t("error.remux.corrupted"),
buttons: [
{
text: $t("button.gotit"),
main: true,
action: () => {},
},
],
});
return;
if (eprogress?.durationProcessed) {
currentProgress = eprogress.durationProcessed;
}
if (eprogress?.duration && eprogress?.durationProcessed) {
progress = Math.max(
0,
Math.min(
100,
(eprogress.durationProcessed / eprogress.duration) *
100
)
).toFixed(2);
}
console.log(eprogress, progress, speed, currentProgress);
}
totalDuration = Number(file_info.format.duration);
if (file instanceof File && !file.type) {
file = new File([file], file.name, {
type: mime.getType(file.name) ?? undefined,
if (event.data.render) {
processing = false;
worker.terminate();
return downloadFile({
file: new File([event.data.render], event.data.filename, {
type: event.data.render.type,
}),
});
}
const render = await ff
.render({
blob: file,
args: ["-c", "copy", "-map", "0"],
})
.catch((e) => {
console.error("uh-oh! render error");
console.error(e);
return createDialog({
id: "remux-error",
type: "small",
meowbalt: "error",
bodyText: $t("error.remux.out_of_resources"),
buttons: [
{
text: $t("button.gotit"),
main: true,
action: () => {},
},
],
});
if (event.data.error) {
processing = false;
worker.terminate();
return createDialog({
id: "remux-error",
type: "small",
meowbalt: "error",
bodyText: $t(event.data.error),
buttons: [
{
text: $t("button.gotit"),
main: true,
action: () => {},
},
],
});
if (!render) {
return console.log("not a valid file");
}
const filenameParts = file.name.split(".");
const filenameExt = filenameParts.pop();
const filename = `${filenameParts.join(".")} (remux).${filenameExt}`;
return await downloadFile({
file: new File([render], filename, {
type: render.type,
}),
});
} finally {
processing = false;
file = undefined;
progress = undefined;
speed = undefined;
}
};
};
beforeNavigate((event) => {
@ -183,7 +126,7 @@
main: true,
color: "red",
action: async () => {
await ff.terminate();
worker.terminate();
wentAway = true;
goto(path);
},
@ -193,10 +136,6 @@
}
}
});
$: if (file) {
render();
}
</script>
<svelte:head>
@ -235,6 +174,19 @@
"m4a",
]}
/>
{#if file}
<div class="button-row">
<button on:click={remux}>remux</button>
<button
on:click={() => {
file = undefined;
}}
>
clear imported files
</button>
</div>
{/if}
</div>
<div id="remux-bullets">
@ -272,11 +224,22 @@
<div class="progress-text">
processing ({progress}%, {speed}x)...
</div>
{:else if currentProgress && speed}
<div class="progress-text">
processing ({currentProgress}s, {speed}x)...
</div>
{:else}
processing...
<div class="progress-text">processing...</div>
{/if}
{:else}
done!
<button
on:click={() => {
file = undefined;
processing = false;
worker?.terminate();
}}
>
cancel
</button>
{/if}
</div>
</div>
@ -311,7 +274,6 @@
transition:
transform 0.2s,
opacity 0.2s;
pointer-events: none;
}
#remux-processing.processing {
@ -331,6 +293,7 @@
padding: var(--padding);
gap: var(--padding);
justify-content: center;
align-items: center;
}
.progress-bar {
@ -348,6 +311,9 @@
#remux-receiver {
max-width: 450px;
display: flex;
flex-direction: column;
gap: var(--padding);
}
#remux-bullets {
@ -357,6 +323,18 @@
max-width: 450px;
}
.button-row {
display: flex;
flex-direction: row;
gap: 6px;
}
button {
padding: 12px 24px;
border-radius: 200px;
width: fit-content;
}
@media screen and (max-width: 920px) {
#remux-open {
flex-direction: column;

View file

@ -0,0 +1 @@
export const ssr = false;