Batch rendering
Many videos from CSV or NDJSON rows.
renderTemplateBatch hydrates the template once per data row and exports each result to a file. It runs in Node and uses server export: one headless Chrome per worker, kept open across rows.
npm install @miraiclip/templates @miraiclip/server-exportimport { parseTemplate, rowsFromCsv } from "@miraiclip/templates";
import { renderTemplateBatch } from "@miraiclip/templates/render";
import { readFile } from "node:fs/promises";
const template = parseTemplate(JSON.parse(await readFile("promo.miraiclip-template.json", "utf8")));
const rows = rowsFromCsv(template, await readFile("leads.csv", "utf8"));
const { rows: results, failed } = await renderTemplateBatch(template, rows, {
out: "out/{index}-{name}.mp4",
format: "mp4",
quality: "high",
assetsDir: "./media",
concurrency: 2,
onProgress: ({ rowsDone, totalRows, row }) =>
console.log(`[${rowsDone}/${totalRows}]`, row.ok ? row.filePath : row.error),
});
if (failed.length > 0) process.exitCode = 1;@miraiclip/templates/render is a separate entry point, so browser bundles that import @miraiclip/templates never load Playwright. @miraiclip/server-export is a peer dependency.
Options
| Option | Default | What it does |
|---|---|---|
out | required | Output path per row: a pattern or a function (below) |
format | required | "mp4" or "webm" |
quality, fps, width, height, range, audioChunkSeconds | as in server export | Applied to every row |
assets, assetsDir, browser | as in server export | Where media comes from; which Chrome to launch |
concurrency | 1 | Browsers working the rows in parallel. Never more than the number of rows |
stopOnError | false | Stop starting new rows after the first failure |
onProgress | none | Called after each row with { rowsDone, totalRows, row } |
signal | none | AbortSignal. Stops the batch; see Cancel |
Output paths
A string pattern substitutes {field} and {index}:
| Placeholder | Becomes |
|---|---|
{index} | The row's position, starting at 0 |
{field} | That field's value from the row data. Characters / \ : * ? " < > | and whitespace become - |
A field the row doesn't set, or an object value (asset { src }) | Left as-is, e.g. {accent} |
Defaults aren't used in paths, and rows can map to the same path. Include {index} to keep names unique. For full control, pass a function:
import { renderTemplateBatch } from "@miraiclip/templates/render";
import type { Template, TemplateData } from "@miraiclip/templates";
declare const template: Template;
declare const rows: TemplateData[];
await renderTemplateBatch(template, rows, {
format: "webm",
out: (data, index) => `out/${String(index).padStart(4, "0")}-${String(data["name"]).toLowerCase()}.webm`,
});Parent folders are created.
Results
The promise resolves with every row, in input order, plus the failed ones:
| Field | Meaning |
|---|---|
rows | One BatchRow per input row |
failed | The rows with ok: false. Empty means every row rendered |
BatchRow field | Set when | Meaning |
|---|---|---|
index, data, ok | always | Position, the input row, success |
filePath, bytesWritten | ok | The file written and its size |
issues | the data was invalid | [{ path, message }] from tryHydrate |
error | !ok | One line: invalid data: data.name: required field is missing, the export error, or not attempted (stopOnError) / not attempted (aborted) |
A row with invalid data is never rendered. A failed row doesn't stop the others unless stopOnError is set. With stopOnError, rows already running still finish.
Cancel
import { renderTemplateBatch } from "@miraiclip/templates/render";
import type { Template, TemplateData } from "@miraiclip/templates";
declare const template: Template;
declare const rows: TemplateData[];
const controller = new AbortController();
process.on("SIGINT", () => controller.abort());
const { failed } = await renderTemplateBatch(template, rows, {
format: "mp4",
out: "out/{index}.mp4",
signal: controller.signal,
});
// aborted rows are in `failed`; the promise still resolvesAborting cancels the exports in progress and leaves the remaining rows unattempted. The browsers are closed when the batch ends, whether it succeeds, fails or is cancelled.
Notes
| Topic | Detail |
|---|---|
| MP4 | Needs Google Chrome (H.264). Chromium renders WebM only. See The browser |
concurrency | Each unit launches its own Chrome, so memory use grows with it |
| Asset fields | A row's asset value is resolved like any src: an http(s): URL, or a path under assetsDir |
| Your own loop | createExportSession from @miraiclip/server-export gives you the same warm browser, if you need custom scheduling |
From the command line: CLI.