Miraiclip SDK
Export

Browser

MP4 or WebM export in the page.

exportProject renders the project frame by frame and encodes it with WebCodecs. It uses the same compositor as the preview, so the file matches what the player shows. It runs as fast as decode and encode allow, not in real time.

import { exportProject } from "@miraiclip/renderer";

const bytes = await exportProject(project, {
  format: "mp4",
  quality: "standard",
  onProgress: ({ phase, framesDone, totalFrames }) => console.log(phase, framesDone, totalFrames),
});
const file = new Blob([bytes as BlobPart], { type: "video/mp4" });

exportProject runs on the calling thread. To keep the page responsive, run the same export in a worker: Web worker. For a ready-made button with progress and cancel, see Export UI.

Options

OptionDefaultWhat it does
formatrequired"mp4" (H.264 + AAC) or "webm" (VP9 + Opus)
quality"standard""draft", "standard", "high", or { videoBitrate } in bits per second. Changes bitrate only, not size or frame rate
fpsproject frame rateA number (29.97 is read as 30000/1001) or { num, den }
width, heightproject sizeOutput size in pixels
range0 to the end of the last clip{ startUs, endUs }. Output timestamps start at startUs
targetnoneA WritableStream to stream the file into. See Stream to disk
audioChunkSeconds60Audio is mixed in chunks of this length. Use whole seconds
signalnoneAbortSignal. The promise rejects with ExportAbortedError
onProgressnoneCalled with an ExportProgress (below)
factoriesbuilt-in kindsScene-node factories for custom clip kinds. Pass the same ones as the player
openDemuxer, createDecoder, openAudiomediabunny + WebCodecsMedia adapters

Video decode is capped at 2× the output's longest side. For uncapped decoding, pass createDecoder: createWebCodecsDecoder (exported by @miraiclip/renderer).

An empty project (no clips) throws nothing to export: the composition is empty.

Progress

FieldWhenMeaning
phasealways"audio", "video" or "finalizing"
framesDone, totalFramesalwaysVideo frames encoded so far, out of the total
audioMixedUs, audioTotalUs"audio" phaseTimeline µs mixed so far, out of the range
import { exportProject } from "@miraiclip/renderer";

const bytes = await exportProject(project, {
  format: "webm",
  quality: "high",
  fps: 30,
  width: 1280,
  height: 720,
  range: { startUs: 5_000_000, endUs: 20_000_000 },
  onProgress: ({ phase, framesDone, totalFrames, audioMixedUs, audioTotalUs }) => {
    if (phase === "audio") console.log(`audio ${audioMixedUs}/${audioTotalUs} µs`);
    else console.log(`${phase} ${framesDone}/${totalFrames}`);
  },
});

Cancel

import { ExportAbortedError, exportProject } from "@miraiclip/renderer";

const controller = new AbortController();
// cancelButton.onclick = () => controller.abort();

try {
  const bytes = await exportProject(project, { format: "mp4", signal: controller.signal });
} catch (error) {
  if (!(error instanceof ExportAbortedError)) throw error;
  // cancelled: encoders are released
}

Stream to disk

Without a target, the whole file is held in memory until the promise resolves. For long exports, stream it into a file instead. Each chunk is { type: "write", data, position }, which FileSystemWritableFileStream.write accepts as-is.

import { exportProject } from "@miraiclip/renderer";

// The File System Access API (Chromium). Not in TypeScript's DOM types yet.
declare function showSaveFilePicker(options?: { suggestedName?: string }): Promise<FileSystemFileHandle>;

const handle = await showSaveFilePicker({ suggestedName: "export.webm" });
const writable = await handle.createWritable();

const bytes = await exportProject(project, { format: "webm", target: writable });
bytes.length; // 0: the file went to disk
BehaviorDetail
Return valueAn empty Uint8Array
Write positionsMay go backwards: the container patches its header at the end
BackpressureA slow stream slows the encoders, so memory stays bounded
ClosingexportProject closes the stream when it finishes. Don't call writable.close() after it
CancellingThe stream is closed too, so the file may hold partial output

Worker exports are different

With exportProjectInWorker / exportViaWorker, the stream is left open. Close it yourself. See Web worker.

Codec support

Before the first frame, exportProject checks that the browser can encode the format. If it can't, it rejects with UnsupportedMediaError. Its codec names what's missing: "avc" or "aac" for MP4, "vp9" or "opus" for WebM. Some Chromium builds have no H.264 encoder. Fall back to WebM:

import { UnsupportedMediaError, exportProject, type ExportFormat } from "@miraiclip/renderer";

async function exportWithFallback(): Promise<{ bytes: Uint8Array; format: ExportFormat }> {
  try {
    return { bytes: await exportProject(project, { format: "mp4" }), format: "mp4" };
  } catch (error) {
    if (!(error instanceof UnsupportedMediaError)) throw error;
    return { bytes: await exportProject(project, { format: "webm" }), format: "webm" };
  }
}
HelperReturns
isWebCodecsSupported()true when the browser has the WebCodecs decode pipeline
hasHardwareVideoEncoder(format, width, height)true when the format's video codec has a hardware encoder at that size. false doesn't mean export fails: a software encoder may still work

Memory

SourceBounded by
Encoded filetarget. Without one, the whole file is in memory
Audio mixaudioChunkSeconds. About 23 MB of PCM per minute at 48 kHz stereo
Frames in flightA small fixed window between render and encode
Decoded videoThe 2× decode cap above

For hour-long exports, set a target. To render on a server instead, see Server (Node).

On this page