Server (Node)
The same export from Node, plus a CLI.
@miraiclip/server-export exports a project document from Node. It launches headless Chrome, loads the document into a page, and runs the same exportProject the browser uses.
npm install @miraiclip/server-exportimport { exportProjectFile } from "@miraiclip/server-export";
import { readFile } from "node:fs/promises";
const doc = JSON.parse(await readFile("project.json", "utf8"));
const { filePath, bytesWritten } = await exportProjectFile(doc, {
format: "mp4",
quality: "high",
out: "render/final.mp4",
assets: { interview: "./media/interview.mp4" },
onProgress: (p) => console.log(p.phase, p.framesDone, "/", p.totalFrames),
});The document is what project.toJSON() returns. Server export renders built-in clip kinds only: custom clip kinds need their factories, which can't cross into the page.
Options
| Option | Default | What it does |
|---|---|---|
format | required | "mp4" or "webm" |
quality | "standard" | "draft", "standard", "high" or { videoBitrate } |
fps | project fps | Output frame rate (a number) |
width, height | project size | Output size |
range | whole composition | { startUs, endUs } |
out | none | File path to stream into. Parent folders are created |
assets | none | Asset id → local file path |
assetsDir | process.cwd() | Base folder for relative asset src values |
audioChunkSeconds | 60 | Audio mix chunk length |
signal | none | Rejects with ServerExportAbortedError |
onProgress | none | { phase, framesDone, totalFrames, audioMixedUs?, audioTotalUs?, bytesWritten? } |
browser | see The browser | { executablePath?, args?, swiftshader? } |
Result
With out | Without out |
|---|---|
{ filePath, bytesWritten }. The file streamed to disk as it was encoded | { bytes }. The whole file in memory |
Use out for anything long. Without it, the file crosses from the page to Node in one piece, which briefly costs about twice its size in memory. A failed streamed export deletes the partial file.
bytesWritten in progress events is only set with out. Output is written in chunks of about 16 MiB, so short exports may report 0 until the end. Use framesDone / totalFrames for completion.
Cancel
import { ServerExportAbortedError, exportProjectFile } from "@miraiclip/server-export";
import type { ProjectDocument } from "@miraiclip/core";
declare const doc: ProjectDocument;
const controller = new AbortController();
process.on("SIGINT", () => controller.abort());
try {
await exportProjectFile(doc, { format: "webm", out: "out.webm", signal: controller.signal });
} catch (error) {
if (!(error instanceof ServerExportAbortedError)) throw error;
}Assets
Each asset's media must be readable on the server. Per asset, in order:
| Rule | Example |
|---|---|
An assets entry for the asset id | assets: { intro: "/data/intro.mp4" } |
An http(s): or data: src is fetched as-is | src: "https://cdn.example.com/intro.mp4" |
Any other src is a path relative to assetsDir | src: "media/intro.mp4" |
A local file that doesn't exist fails the export before the browser starts.
The browser
The package depends on playwright-core and downloads no browser. It launches the first of:
browser.executablePath- The
MIRAICLIP_BROWSERenvironment variable - The installed Google Chrome
| Browser | MP4 (H.264 + AAC) | WebM (VP9 + Opus) |
|---|---|---|
| Google Chrome | Yes | Yes |
| Chromium (including Playwright's download and most Docker images) | No: fails the codec check with a clear error | Yes |
On a server without Chrome, npx playwright install chrome installs it.
browser option | Default | What it does |
|---|---|---|
executablePath | none | Path to any Chrome or Chromium binary |
args | none | Extra Chrome flags, added after the defaults |
swiftshader | true on Linux, false elsewhere | Render WebGL in software. Servers rarely have a GPU |
Warm sessions
exportProjectFile launches a browser on every call. createExportSession keeps one browser open, so each export skips the launch. Calls on one session run one at a time, in order. A failed call doesn't break the session.
import { createExportSession } from "@miraiclip/server-export";
import type { ProjectDocument } from "@miraiclip/core";
declare const docA: ProjectDocument;
declare const docB: ProjectDocument;
const session = await createExportSession({ assetsDir: "./media" });
try {
await session.exportFile(docA, { format: "mp4", out: "out/a.mp4" });
await session.exportFile(docB, { format: "mp4", out: "out/b.mp4" });
} finally {
await session.close();
}| Function | Session options | Per call |
|---|---|---|
createExportSession(options?) | assets, assetsDir, browser | exportFile(doc, options): every exportProjectFile option except those three |
createRenderSession(options?) | assets, assetsDir, browser | renderStill(doc, { timeUs, width?, height? }): a PNG. See Stills |
For many videos from one template, Batch rendering runs rows through export sessions for you.
CLI
npx miraiclip-export project.json --out final.mp4 --quality high --asset interview=./media/interview.mp4It prints progress to stderr and <file> (<MB> MB, <seconds>s) to stdout. Ctrl-C cancels the export.
| Flag | Default | What it does |
|---|---|---|
<project.json> | required | The project document |
--out <file>, -o <file> | required | Output path |
--format mp4|webm | from the --out extension: .webm → WebM, anything else → MP4 | Container |
--quality draft|standard|high | standard | Quality preset |
--fps <n> | project fps | Output frame rate |
--width <px>, --height <px> | project size | Output size |
--start <s>, --end <s> | whole composition | Range in seconds. Give both or neither |
--asset <id>=<path> | none | Map an asset id to a local file. Repeatable |
--assets-dir <dir> | the project file's folder | Base folder for relative asset paths |
--browser <path> | see The browser | Chrome or Chromium binary |
--quiet | off | No progress output |
The CLI has no --help flag. An unknown flag exits with unknown flag ….