Web worker
Export without blocking the UI.
exportProjectInWorker runs exportProject in a dedicated worker. The total work is the same, but the page keeps rendering and responding while it runs.
import { exportProjectInWorker } from "@miraiclip/renderer";
const bytes = await exportProjectInWorker(project, {
format: "mp4",
quality: "standard",
onProgress: ({ framesDone, totalFrames }) => console.log(`${framesDone}/${totalFrames}`),
});It spawns the bundled worker with new Worker(new URL("./export.worker.js", import.meta.url), { type: "module" }), a pattern Vite, webpack and Rollup bundle. The worker is terminated when the export ends. For a full button with progress and cancel, see Export UI.
Options
The same as exportProject, minus the ones that are functions:
| Option | Notes |
|---|---|
format, quality, fps, width, height, range, audioChunkSeconds | Same meaning and defaults |
signal | Rejects with ExportAbortedError |
onProgress | Same ExportProgress events, relayed from the worker |
target | Supported. The stream stays open: close it yourself |
worker | Use this Worker instead of spawning one. It is not terminated |
workerUrl | Spawn a module worker from this URL instead of the bundled entry |
factories, openDemuxer, createDecoder, openAudio | Not available: functions can't cross into a worker |
Bring your own worker
If your bundler doesn't resolve the default URL, load the worker entry yourself. The package exports it as @miraiclip/renderer/export-worker.
// Vite
import ExportWorker from "@miraiclip/renderer/export-worker?worker";
import { exportViaWorker } from "@miraiclip/renderer";
const worker = new ExportWorker();
try {
const bytes = await exportViaWorker(worker, project, { format: "webm" });
} finally {
worker.terminate(); // exportViaWorker never terminates your worker
}| Function | Spawns a worker | Terminates it |
|---|---|---|
exportProjectInWorker(project, options) | Yes, unless you pass worker | Only the one it spawned |
exportViaWorker(worker, project, options) | No | No |
With workerUrl, point at a URL that serves the export-worker entry, for example one your bundler emits.
Stream to disk
import { exportProjectInWorker } from "@miraiclip/renderer";
declare function showSaveFilePicker(options?: { suggestedName?: string }): Promise<FileSystemFileHandle>;
const handle = await showSaveFilePicker({ suggestedName: "export.mp4" });
const writable = await handle.createWritable();
try {
await exportProjectInWorker(project, { format: "mp4", target: writable });
await writable.close(); // the worker export leaves the stream open
} catch (error) {
await writable.abort(); // discard the partial file
throw error;
}Encoded chunks pass from the worker through the main thread into your stream, because a FileSystemWritableFileStream can't be transferred. Each chunk waits for its write to finish, so the stream's backpressure reaches the encoders.
What runs where
| Work | Thread |
|---|---|
| Decoding, compositing, encoding | Worker |
| Fonts, captions, effects, transitions | Worker |
| Audio mix | Main. OfflineAudioContext is window-only; the mix is sent to the worker as raw PCM |
| HTML clips | Main. They rasterize through the DOM. Static ones render before the export starts; animated ones render per frame on request |
Writes into target | Main |
So a worker export still uses some main-thread time for audio and HTML clips.
Limits
| Limit | Why |
|---|---|
| Built-in clip kinds only | Custom-kind factories are functions and can't cross into a worker. Use exportProject on the main thread for custom clip kinds |
Media src must be reachable from a worker | http(s): and blob: URLs and Blob/File objects work |
| A worker that fails to load | Rejects with export worker failed: … |