cobalt / web /src /routes /remux /+page.svelte
playingapi's picture
Upload 376 files
43a06dc verified
<script lang="ts">
import mime from "mime";
import LibAVWrapper from "$lib/libav";
import { beforeNavigate, goto } from "$app/navigation";
import { t } from "$lib/i18n/translations";
import { createDialog } from "$lib/state/dialogs";
import { downloadFile } from "$lib/download";
import Skeleton from "$components/misc/Skeleton.svelte";
import DropReceiver from "$components/misc/DropReceiver.svelte";
import FileReceiver from "$components/misc/FileReceiver.svelte";
import BulletExplain from "$components/misc/BulletExplain.svelte";
import IconRepeat from "@tabler/icons-svelte/IconRepeat.svelte";
import IconDevices from "@tabler/icons-svelte/IconDevices.svelte";
import IconInfoCircle from "@tabler/icons-svelte/IconInfoCircle.svelte";
let draggedOver = false;
let file: File | undefined;
let totalDuration: number | undefined;
let processedDuration: number | undefined;
let speed: number | undefined;
let progress: 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;
}
if (progress.speed) {
speed = progress.speed;
}
});
ff.init();
const render = async () => {
if (!file || processing) return;
await ff.init();
let dialogOpened;
try {
progress = undefined;
speed = undefined;
processing = true;
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);
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 (!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;
}
totalDuration = 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);
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 (!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) => {
if (processing && !wentAway) {
event.cancel();
const path = event.to?.route?.id;
if (path) {
return createDialog({
id: "remux-ongoing",
type: "small",
icon: "warn-red",
title: $t("dialog.processing.title.ongoing"),
bodyText: $t("dialog.processing.ongoing"),
buttons: [
{
text: $t("button.no"),
main: false,
action: () => {},
},
{
text: $t("button.yes"),
main: true,
color: "red",
action: async () => {
await ff.terminate();
wentAway = true;
goto(path);
},
},
],
});
}
}
});
$: if (file) {
render();
}
</script>
<svelte:head>
<title>{$t("tabs.remux")} ~ {$t("general.cobalt")}</title>
<meta
property="og:title"
content="{$t('tabs.remux')} ~ {$t('general.cobalt')}"
/>
</svelte:head>
<DropReceiver
bind:file
bind:draggedOver
id="remux-container"
classes={processing ? "processing" : ""}
>
<div
id="remux-open"
class:processing
tabindex="-1"
data-first-focus
data-focus-ring-hidden
>
<div id="remux-receiver">
<FileReceiver
bind:draggedOver
bind:file
acceptTypes={["video/*", "audio/*"]}
acceptExtensions={[
"mp4",
"webm",
"mp3",
"ogg",
"opus",
"wav",
"m4a",
]}
/>
</div>
<div id="remux-bullets">
<BulletExplain
title={$t("remux.bullet.purpose.title")}
description={$t("remux.bullet.purpose.description")}
icon={IconRepeat}
/>
<BulletExplain
title={$t("remux.bullet.explainer.title")}
description={$t("remux.bullet.explainer.description")}
icon={IconInfoCircle}
/>
<BulletExplain
title={$t("remux.bullet.privacy.title")}
description={$t("remux.bullet.privacy.description")}
icon={IconDevices}
/>
</div>
</div>
<div id="remux-processing" class:processing aria-hidden={!processing}>
<div id="processing-status">
{#if processing}
{#if progress && speed}
<div class="progress-bar">
<Skeleton
width="{progress}%"
height="20px"
class="elevated"
/>
</div>
<div class="progress-text">
processing ({progress}%, {speed}x)...
</div>
{:else}
processing...
{/if}
{:else}
done!
{/if}
</div>
</div>
</DropReceiver>
<style>
:global(#remux-container) {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
#remux-open {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
text-align: center;
gap: 48px;
transition:
transform 0.2s,
opacity 0.2s;
}
#remux-processing {
position: absolute;
display: flex;
flex-direction: column;
opacity: 0;
transform: scale(0.9);
transition:
transform 0.2s,
opacity 0.2s;
pointer-events: none;
}
#remux-processing.processing {
opacity: 1;
transform: none;
}
#remux-open.processing {
transform: scale(0.9);
opacity: 0;
pointer-events: none;
}
#processing-status {
display: flex;
flex-direction: column;
padding: var(--padding);
gap: var(--padding);
justify-content: center;
}
.progress-bar {
height: 20px;
width: 400px;
max-width: 400px;
border-radius: 6px;
background: var(--button);
}
.progress-text {
font-size: 14px;
text-align: center;
}
#remux-receiver {
max-width: 450px;
}
#remux-bullets {
display: flex;
flex-direction: column;
gap: 18px;
max-width: 450px;
}
@media screen and (max-width: 920px) {
#remux-open {
flex-direction: column;
gap: var(--padding);
}
#remux-bullets {
padding: var(--padding);
}
}
@media screen and (max-width: 535px) {
.progress-bar {
width: 350px;
}
#remux-bullets {
gap: var(--padding);
}
}
@media screen and (max-height: 750px) and (max-width: 535px) {
:global(#remux-container:not(.processing)) {
justify-content: start;
align-items: start;
padding-top: var(--padding);
}
}
</style>