Miraiclip SDK
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.

main.ts
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";
  }
});
ExportButton.tsx
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>
  );
}
ExportButton.vue
<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>
ExportButton.svelte
<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:

download.ts
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

OptionDefaultWhat it does
formatrequired"mp4" (H.264 + AAC) or "webm" (VP9 + Opus)
quality"standard""draft", "standard", "high" or { videoBitrate }
width, height, fpsproject settingsOutput size and frame rate
rangewhole timeline{ startUs, endUs }
signalnoneCancel; the promise rejects with ExportAbortedError
onProgressnone{ phase, framesDone, totalFrames }

On this page