Build an editor
Export UI
An export button with progress and cancel.
exportProjectInWorker runs the export in a web worker, so the page stays responsive. Pass an AbortSignal to cancel it.
import { ExportAbortedError, exportProjectInWorker } from "@miraiclip/renderer";
import { project } from "../shared/project";
import { download } from "../shared/download";
const button = document.querySelector<HTMLButtonElement>("#export")!;
let controller: AbortController | null = null;
button.addEventListener("click", async () => {
if (controller) return controller.abort(); // second click cancels
controller = new AbortController();
try {
const bytes = await exportProjectInWorker(project, {
format: "mp4",
signal: controller.signal,
onProgress: ({ framesDone, totalFrames }) => {
button.textContent = `Cancel (${Math.round((100 * framesDone) / (totalFrames || 1))}%)`;
},
});
download(bytes, "video.mp4");
} catch (error) {
if (!(error instanceof ExportAbortedError)) throw error;
} finally {
controller = null;
button.textContent = "Export MP4";
}
});import { useRef, useState } from "react";
import type { Project } from "@miraiclip/core";
import { ExportAbortedError, exportProjectInWorker } from "@miraiclip/renderer";
import { download } from "../shared/download";
export function ExportButton({ project }: { project: Project }) {
const [progress, setProgress] = useState<number | null>(null);
const controller = useRef<AbortController | null>(null);
async function start() {
controller.current = new AbortController();
setProgress(0);
try {
const bytes = await exportProjectInWorker(project, {
format: "mp4",
signal: controller.current.signal,
onProgress: ({ framesDone, totalFrames }) => setProgress(framesDone / (totalFrames || 1)),
});
download(bytes, "video.mp4");
} catch (error) {
if (!(error instanceof ExportAbortedError)) throw error;
} finally {
setProgress(null);
}
}
return progress === null ? (
<button onClick={start}>Export MP4</button>
) : (
<button onClick={() => controller.current?.abort()}>
Cancel ({Math.round(progress * 100)}%)
</button>
);
}<script setup lang="ts">
import { ref } from "vue";
import type { Project } from "@miraiclip/core";
import { ExportAbortedError, exportProjectInWorker } from "@miraiclip/renderer";
import { download } from "../shared/download";
const { project } = defineProps<{ project: Project }>();
const progress = ref<number | null>(null);
let controller: AbortController | null = null;
async function start() {
controller = new AbortController();
progress.value = 0;
try {
const bytes = await exportProjectInWorker(project, {
format: "mp4",
signal: controller.signal,
onProgress: ({ framesDone, totalFrames }) => (progress.value = framesDone / (totalFrames || 1)),
});
download(bytes, "video.mp4");
} catch (error) {
if (!(error instanceof ExportAbortedError)) throw error;
} finally {
progress.value = null;
}
}
</script>
<template>
<button v-if="progress === null" @click="start">Export MP4</button>
<button v-else @click="controller?.abort()">Cancel ({{ Math.round(progress * 100) }}%)</button>
</template><script lang="ts">
import type { Project } from "@miraiclip/core";
import { ExportAbortedError, exportProjectInWorker } from "@miraiclip/renderer";
import { download } from "../shared/download";
let { project }: { project: Project } = $props();
let progress = $state<number | null>(null);
let controller: AbortController | null = null;
async function start() {
controller = new AbortController();
progress = 0;
try {
const bytes = await exportProjectInWorker(project, {
format: "mp4",
signal: controller.signal,
onProgress: ({ framesDone, totalFrames }) => (progress = framesDone / (totalFrames || 1)),
});
download(bytes, "video.mp4");
} catch (error) {
if (!(error instanceof ExportAbortedError)) throw error;
} finally {
progress = null;
}
}
</script>
{#if progress === null}
<button onclick={start}>Export MP4</button>
{:else}
<button onclick={() => controller?.abort()}>Cancel ({Math.round(progress * 100)}%)</button>
{/if}download is a small helper:
export function download(bytes: Uint8Array, filename: string, type = "video/mp4") {
const url = URL.createObjectURL(new Blob([bytes as BlobPart], { type }));
const a = Object.assign(document.createElement("a"), { href: url, download: filename });
a.click();
URL.revokeObjectURL(url);
}Options
| Option | Default | What it does |
|---|---|---|
format | required | "mp4" (H.264 + AAC) or "webm" (VP9 + Opus) |
quality | "standard" | "draft", "standard", "high" or { videoBitrate } |
width, height, fps | project settings | Output size and frame rate |
range | whole timeline | { startUs, endUs } |
signal | none | Cancel; the promise rejects with ExportAbortedError |
onProgress | none | { phase, framesDone, totalFrames } |