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 { try {
await libav.ffprobe([ await libav.ffprobe([
'-v', 'quiet', //'-v', 'quiet',
'-print_format', 'json', '-print_format', 'json',
'-show_format', '-show_format',
'-show_streams', '-show_streams',

View file

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

View file

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