diff --git a/web/i18n/en/receiver.json b/web/i18n/en/receiver.json index 567e569f..43144ae9 100644 --- a/web/i18n/en/receiver.json +++ b/web/i18n/en/receiver.json @@ -1,5 +1,7 @@ { "title": "drag or select a file", + "title.multiple": "drag or select files", "title.drop": "drop the file here!", + "title.drop.multiple": "drop the files here!", "accept": "supported formats: {{ formats }}." } diff --git a/web/src/components/misc/DropReceiver.svelte b/web/src/components/misc/DropReceiver.svelte index a653067d..4a51407a 100644 --- a/web/src/components/misc/DropReceiver.svelte +++ b/web/src/components/misc/DropReceiver.svelte @@ -3,19 +3,20 @@ export let classes = ""; export let draggedOver = false; - export let file: File | undefined; + export let files: FileList | undefined; const dropHandler = async (ev: DragEvent) => { draggedOver = false; ev.preventDefault(); - if (ev?.dataTransfer?.files.length === 1) { - file = ev.dataTransfer.files[0]; - return file; + if (ev?.dataTransfer?.files && ev?.dataTransfer?.files.length > 0) { + files = ev.dataTransfer.files; + return files; } }; const dragOverHandler = (ev: DragEvent) => { + console.log("dragged over omg") draggedOver = true; ev.preventDefault(); }; @@ -25,6 +26,7 @@ {id} class={classes} role="region" + aria-hidden="true" on:drop={(ev) => dropHandler(ev)} on:dragover={(ev) => dragOverHandler(ev)} on:dragend={() => { diff --git a/web/src/components/misc/FileReceiver.svelte b/web/src/components/misc/FileReceiver.svelte index 59d1b406..d2921be7 100644 --- a/web/src/components/misc/FileReceiver.svelte +++ b/web/src/components/misc/FileReceiver.svelte @@ -5,22 +5,33 @@ import IconFileImport from "@tabler/icons-svelte/IconFileImport.svelte"; import IconUpload from "@tabler/icons-svelte/IconUpload.svelte"; - export let file: File | undefined; + export let files: FileList | undefined; export let draggedOver = false; export let acceptTypes: string[]; export let acceptExtensions: string[]; + export let maxFileNumber: number = 100; + + let selectorStringMultiple = maxFileNumber > 1 ? ".multiple" : ""; let fileInput: HTMLInputElement; + const openFile = async () => { fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = acceptTypes.join(","); + if (maxFileNumber > 1) { + fileInput.multiple = true; + } + fileInput.click(); fileInput.onchange = async () => { - if (fileInput.files?.length === 1) { - file = fileInput.files[0]; - return file; + let userFiles = fileInput?.files; + if (userFiles && userFiles.length >= 1) { + if (userFiles.length > maxFileNumber) { + return alert("too many files, limit is " + maxFileNumber); + } + return files = userFiles; } }; }; @@ -47,9 +58,9 @@
{#if draggedOver} - {$t("receiver.title.drop")} + {$t("receiver.title.drop" + selectorStringMultiple)} {:else} - {$t("receiver.title")} + {$t("receiver.title" + selectorStringMultiple)} {/if}
diff --git a/web/src/components/queue/ProcessingQueue.svelte b/web/src/components/queue/ProcessingQueue.svelte index 326346f0..2e89c500 100644 --- a/web/src/components/queue/ProcessingQueue.svelte +++ b/web/src/components/queue/ProcessingQueue.svelte @@ -2,11 +2,10 @@ import { t } from "$lib/i18n/translations"; import { onNavigate } from "$app/navigation"; - import settings from "$lib/state/settings"; - import { addToQueue, nukeEntireQueue, queue } from "$lib/state/queue"; + import { clearQueue, queue } from "$lib/state/queen-bee/queue"; import type { SvelteComponent } from "svelte"; - import type { QueueItem } from "$lib/types/queue"; + import type { CobaltQueueItem } from "$lib/types/queue"; import SectionHeading from "$components/misc/SectionHeading.svelte"; import PopoverContainer from "$components/misc/PopoverContainer.svelte"; @@ -15,47 +14,24 @@ import ProcessingQueueStub from "$components/queue/ProcessingQueueStub.svelte"; import IconX from "@tabler/icons-svelte/IconX.svelte"; - import IconPlus from "@tabler/icons-svelte/IconPlus.svelte"; - - import IconGif from "@tabler/icons-svelte/IconGif.svelte"; - import IconMovie from "@tabler/icons-svelte/IconMovie.svelte"; - import IconMusic from "@tabler/icons-svelte/IconMusic.svelte"; - import IconPhoto from "@tabler/icons-svelte/IconPhoto.svelte"; - import IconVolume3 from "@tabler/icons-svelte/IconVolume3.svelte"; + import { currentTasks } from "$lib/state/queen-bee/current-tasks"; let popover: SvelteComponent; $: expanded = false; - $: queueItems = Object.entries($queue) as [id: string, item: QueueItem][]; + $: queueItems = Object.entries($queue) as [ + id: string, + item: CobaltQueueItem, + ][]; $: queueLength = Object.keys($queue).length; + $: completedQueueItems = queueItems.filter(([id, item]) => { + return item.state === "done" + }).length; + + // TODO: toggle this only when progress is unknown $: indeterminate = false; - const itemIcons = { - video: IconMovie, - video_mute: IconVolume3, - audio: IconMusic, - audio_convert: IconMusic, - image: IconPhoto, - gif: IconGif, - }; - - const addFakeQueueItem = () => { - return addToQueue({ - id: crypto.randomUUID(), - status: "waiting", - type: "video", - filename: "test.mp4", - files: [ - { - type: "video", - url: "https://", - }, - ], - processingSteps: [], - }); - }; - const popoverAction = async () => { expanded = !expanded; }; @@ -66,7 +42,7 @@
- +
{#if queueLength > 0} - {/if} - - {#if $settings.advanced.debug} - - {/if}
{#each queueItems as [id, item]} {/each} {#if queueLength === 0} diff --git a/web/src/components/queue/ProcessingQueueItem.svelte b/web/src/components/queue/ProcessingQueueItem.svelte index 6c43a6cf..75d6743b 100644 --- a/web/src/components/queue/ProcessingQueueItem.svelte +++ b/web/src/components/queue/ProcessingQueueItem.svelte @@ -1,41 +1,67 @@
- +
{filename}
-
-
-
-
{id}: {status}
+ {#if state === "running"} +
+
+
+ {/if} +
{id}: {state}
- - + {/if} +
diff --git a/web/src/lib/queen-bee/queue.ts b/web/src/lib/queen-bee/queue.ts new file mode 100644 index 00000000..8e2b3b90 --- /dev/null +++ b/web/src/lib/queen-bee/queue.ts @@ -0,0 +1,36 @@ +import { addItem } from "$lib/state/queen-bee/queue"; +import type { CobaltPipelineItem } from "$lib/types/workers"; + +export const getMediaType = (type: string) => { + const kind = type.split('/')[0]; + + // can't use .includes() here for some reason + if (kind === "video" || kind === "audio" || kind === "image") { + return kind; + } +} + +export const createRemuxPipeline = (file: File) => { + // chopped khia + const parentId = crypto.randomUUID(); + const mediaType = getMediaType(file.type); + + const pipeline: CobaltPipelineItem[] = [{ + worker: "remux", + workerId: crypto.randomUUID(), + parentId, + workerArgs: { + files: [file], + }, + }]; + + if (mediaType) { + addItem({ + id: parentId, + state: "waiting", + pipeline, + filename: file.name, + mediaType, + }) + } +} diff --git a/web/src/lib/queen-bee/run-worker.ts b/web/src/lib/queen-bee/run-worker.ts new file mode 100644 index 00000000..8163865a --- /dev/null +++ b/web/src/lib/queen-bee/run-worker.ts @@ -0,0 +1,60 @@ +import RemuxWorker from "$lib/workers/remux?worker"; +//import RemoveBgWorker from "$lib/workers/removebg?worker"; + +import type { CobaltPipelineItem } from "$lib/types/workers"; +import { itemDone, itemError } from "$lib/state/queen-bee/queue"; + +const workerError = (parentId: string, workerId: string, worker: Worker, error: string) => { + itemError(parentId, workerId, error); + worker.terminate(); +} + +const workerSuccess = (parentId: string, workerId: string, worker: Worker, file: File) => { + itemDone(parentId, workerId, file); + worker.terminate(); +} + +export const runRemuxWorker = async (workerId: string, parentId: string, file: File) => { + const worker = new RemuxWorker(); + + worker.postMessage({ file }); + + worker.onerror = (e) => { + console.error("remux worker exploded:", e); + + // TODO: proper error code + workerError(parentId, workerId, worker, "internal error"); + }; + + worker.onmessage = (event) => { + const eventData = event.data.cobaltRemuxWorker; + if (!eventData) return; + + console.log(eventData); + + // TODO: calculate & use progress again + + if (eventData.render) { + return workerSuccess( + parentId, + workerId, + worker, + new File([eventData.render], eventData.filename, { + type: eventData.render.type, + }) + ); + } + + if (eventData.error) { + return workerError(parentId, workerId, worker, eventData.error); + } + }; +} + +export const startWorker = async ({ worker, workerId, parentId, workerArgs }: CobaltPipelineItem) => { + switch (worker) { + case "remux": + await runRemuxWorker(workerId, parentId, workerArgs.files[0]); + break; + } +} diff --git a/web/src/lib/queen-bee/scheduler.ts b/web/src/lib/queen-bee/scheduler.ts new file mode 100644 index 00000000..468415ae --- /dev/null +++ b/web/src/lib/queen-bee/scheduler.ts @@ -0,0 +1,41 @@ +import { get } from "svelte/store"; +import { itemRunning, queue } from "$lib/state/queen-bee/queue"; +import { startWorker } from "$lib/queen-bee/run-worker"; +import { addWorkerToQueue, currentTasks } from "$lib/state/queen-bee/current-tasks"; + +export const checkTasks = () => { + const queueItems = get(queue); + const ongoingTasks = get(currentTasks) + + if (Object.keys(ongoingTasks).length > 0) return; + + for (const item of Object.keys(queueItems)) { + const task = queueItems[item]; + + if (task.state === "running") { + break; + } + + if (task.state === "waiting") { + for (let i = 0; i < task.pipeline.length; i++) { + // TODO: loop here and pass the file between pipelines + // or schedule several tasks one after another but within + // one parent & pipeline + const pipelineItem = task.pipeline[i]; + + startWorker(pipelineItem); + + addWorkerToQueue({ + id: pipelineItem.workerId, + parentId: task.id, + step: i + 1, + totalSteps: task.pipeline.length, + }); + + itemRunning(task.id, i); + break; + } + break; + } + } +} diff --git a/web/src/lib/state/queen-bee/current-tasks.ts b/web/src/lib/state/queen-bee/current-tasks.ts new file mode 100644 index 00000000..e1c26d85 --- /dev/null +++ b/web/src/lib/state/queen-bee/current-tasks.ts @@ -0,0 +1,40 @@ +import { readable, type Updater } from "svelte/store"; + +import type { CobaltWorkerProgress } from "$lib/types/workers"; +import type { CobaltCurrentTasks, CobaltCurrentTaskItem } from "$lib/types/queen-bee"; + +let update: (_: Updater) => void; + +const currentTasks = readable( + {}, + (_, _update) => { update = _update } +); + +export function addWorkerToQueue(item: CobaltCurrentTaskItem) { + update(tasks => { + tasks[item.id] = item; + return tasks; + }); +} + +export function removeWorkerFromQueue(id: string) { + update(tasks => { + delete tasks[id]; + return tasks; + }); +} + +export function updateWorkerProgress(id: string, progress: CobaltWorkerProgress) { + update(tasks => { + tasks[id].progress = progress; + return tasks; + }); +} + +export function clearQueue() { + update(() => { + return {}; + }); +} + +export { currentTasks }; diff --git a/web/src/lib/state/queen-bee/queue.ts b/web/src/lib/state/queen-bee/queue.ts new file mode 100644 index 00000000..4d784844 --- /dev/null +++ b/web/src/lib/state/queen-bee/queue.ts @@ -0,0 +1,84 @@ +import { readable, type Updater } from "svelte/store"; +import type { CobaltQueue, CobaltQueueItem } from "$lib/types/queue"; +import { checkTasks } from "$lib/queen-bee/scheduler"; +import { removeWorkerFromQueue } from "./current-tasks"; + +let update: (_: Updater) => void; + +const queue = readable( + {}, + (_, _update) => { update = _update } +); + +export function addItem(item: CobaltQueueItem) { + update(queueData => { + queueData[item.id] = item; + return queueData; + }); + + checkTasks(); +} + +export function itemError(id: string, workerId: string, error: string) { + update(queueData => { + if (queueData[id]) { + queueData[id] = { + ...queueData[id], + state: "error", + errorCode: error, + } + } + return queueData; + }); + + removeWorkerFromQueue(workerId); + checkTasks(); +} + +export function itemDone(id: string, workerId: string, file: File) { + update(queueData => { + if (queueData[id]) { + queueData[id] = { + ...queueData[id], + state: "done", + resultFile: file, + } + } + return queueData; + }); + + removeWorkerFromQueue(workerId); + checkTasks(); +} + +export function itemRunning(id: string, step: number) { + update(queueData => { + if (queueData[id]) { + queueData[id] = { + ...queueData[id], + state: "running", + currentStep: step, + } + } + return queueData; + }); + + checkTasks(); +} + +export function removeItem(id: string) { + update(queueData => { + delete queueData[id]; + return queueData; + }); + + checkTasks(); +} + +export function clearQueue() { + update(() => { + return {}; + }); +} + +export { queue }; diff --git a/web/src/lib/state/queue.ts b/web/src/lib/state/queue.ts deleted file mode 100644 index ee431743..00000000 --- a/web/src/lib/state/queue.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { merge } from "ts-deepmerge"; -import { get, readable, type Updater } from "svelte/store"; -import type { OngoingQueueItem, QueueItem } from "$lib/types/queue"; - -type Queue = { - [id: string]: QueueItem; -} - -type OngoingQueue = { - [id: string]: OngoingQueueItem; -} - -let update: (_: Updater) => void; - -const queue = readable( - {}, - (_, _update) => { update = _update } -); - -export function addToQueue(item: QueueItem) { - update(queueData => { - queueData[item.id] = item; - return queueData; - }); -} - -export function removeFromQueue(id: string) { - update(queueData => { - delete queueData[id]; - return queueData; - }); -} - -let updateOngoing: (_: Updater) => void; - -const ongoingQueue = readable( - {}, - (_, _update) => { updateOngoing = _update } -); - -export function updateOngoingQueue(id: string, itemInfo: Partial = {}) { - updateOngoing(queueData => { - if (get(queue)?.id) { - queueData[id] = merge(queueData[id], { - id, - ...itemInfo, - }); - } - - return queueData; - }); -} - -export function removeFromOngoingQueue(id: string) { - updateOngoing(queue => { - delete queue[id]; - return queue; - }); -} - -export function nukeEntireQueue() { - update(() => { - return {}; - }); - updateOngoing(() => { - return {}; - }); -} - -export { queue, ongoingQueue }; diff --git a/web/src/lib/types/queen-bee.ts b/web/src/lib/types/queen-bee.ts new file mode 100644 index 00000000..3684ac5a --- /dev/null +++ b/web/src/lib/types/queen-bee.ts @@ -0,0 +1,13 @@ +import type { CobaltWorkerProgress } from "$lib/types/workers"; + +export type CobaltCurrentTaskItem = { + id: string, + parentId: string, // parent id is queue id to which this pipeline worker belongs to + step: number, + totalSteps: number, + progress?: CobaltWorkerProgress, +} + +export type CobaltCurrentTasks = { + [id: string]: CobaltCurrentTaskItem, +} diff --git a/web/src/lib/types/queue.ts b/web/src/lib/types/queue.ts index 6785b514..91caac90 100644 --- a/web/src/lib/types/queue.ts +++ b/web/src/lib/types/queue.ts @@ -1,34 +1,37 @@ -type ProcessingStep = "mux" | "mux_hls" | "encode"; -type ProcessingPreset = "mp4" | "webm" | "copy"; -type ProcessingState = "completed" | "failed" | "canceled" | "waiting" | "downloading" | "muxing" | "converting"; -type ProcessingType = "video" | "video_mute" | "audio" | "audio_convert" | "image" | "gif"; -type QueueFileType = "video" | "audio" | "image" | "gif"; +import type { CobaltPipelineItem, CobaltPipelineResultFileType } from "$lib/types/workers"; -export type ProcessingStepItem = { - type: ProcessingStep, - preset?: ProcessingPreset, -} +export type CobaltQueueItemState = "waiting" | "running" | "done" | "error"; -export type QueueFile = { - type: QueueFileType, - url: string, -} - -export type QueueItem = { +export type CobaltQueueBaseItem = { id: string, - status: ProcessingState, - type: ProcessingType, + state: CobaltQueueItemState, + pipeline: CobaltPipelineItem[], + // TODO: metadata filename: string, - files: QueueFile[], - processingSteps: ProcessingStepItem[], -} + mediaType: CobaltPipelineResultFileType, +}; -export type OngoingQueueItem = { - id: string, - currentStep?: ProcessingStep, - size?: { - expected: number, - current: number, - }, - speed?: number, -} +export type CobaltQueueItemWaiting = CobaltQueueBaseItem & { + state: "waiting", +}; + +export type CobaltQueueItemRunning = CobaltQueueBaseItem & { + state: "running", + currentStep: number, +}; + +export type CobaltQueueItemDone = CobaltQueueBaseItem & { + state: "done", + resultFile: File, +}; + +export type CobaltQueueItemError = CobaltQueueBaseItem & { + state: "error", + errorCode: string, +}; + +export type CobaltQueueItem = CobaltQueueItemWaiting | CobaltQueueItemRunning | CobaltQueueItemDone | CobaltQueueItemError; + +export type CobaltQueue = { + [id: string]: CobaltQueueItem, +}; diff --git a/web/src/lib/types/workers.ts b/web/src/lib/types/workers.ts new file mode 100644 index 00000000..3bbc79c5 --- /dev/null +++ b/web/src/lib/types/workers.ts @@ -0,0 +1,22 @@ +export const resultFileTypes = ["video", "audio", "image"] as const; + +export type CobaltWorkerType = "remux" | "removebg"; +export type CobaltPipelineResultFileType = typeof resultFileTypes[number]; + +export type CobaltWorkerProgress = { + indeterminate: boolean, + speed?: number, + percentage: number, +} + +export type CobaltWorkerArgs = { + files: File[], + //TODO: args for libav & etc with unique types +} + +export type CobaltPipelineItem = { + worker: CobaltWorkerType, + workerId: string, + parentId: string, + workerArgs: CobaltWorkerArgs, +} diff --git a/web/src/routes/remux/+page.svelte b/web/src/routes/remux/+page.svelte index ff644693..787063cd 100644 --- a/web/src/routes/remux/+page.svelte +++ b/web/src/routes/remux/+page.svelte @@ -1,11 +1,5 @@ @@ -150,7 +38,7 @@ - {#if file} + {#if files}