Skip to content

Rendering

View .mdllms.txt
Step Call Cost
1 tsc --noEmit, comp.lint(), CompositorProgramSchema.safeParse(comp.toSpec()) no GPU, instant
2 comp.renderFrame({ frame }) one frame
3 comp.renderFrameGrid({ frames }) a handful of frames
4 Vitest pixel probes per test
5 comp.renderVideo({ outputPath, qa: true }) the whole film
import { CompositorProgramSchema } from "framefields";
const parsed = CompositorProgramSchema.safeParse(comp.toSpec());
if (!parsed.success) throw new Error(JSON.stringify(parsed.error.format()));
for (const d of comp.lint()) console.log(`${d.severity} ${d.code}: ${d.message}${d.layerId ? ` (${d.layerId})` : ""}`);

lint() finds what renders without error but not as intended: unparseable colors and gradients, low-contrast text, off-canvas layers, truncated animations, PBR props without an environment. Codes are in QA codes.

import fs from "node:fs/promises";
import { HeadlessMediaRenderer } from "framefields";
const renderer = new HeadlessMediaRenderer(); // reuse across frames
const png = await comp.renderFrame({ frame: 45, renderer }); // PNG Buffer
await fs.writeFile("output/f0045.png", png);

Render the key moments — entrance, peak, exit — and look at them.

const grid = await comp.renderFrameGrid({
frames: [0, 6, 12, 18, 24, 30, 36, 42], // or startFrame / endFrame / stepFrames, or timesMs
columns: 4,
cellWidth: 480, // 120–960, default 320
showLabels: true, // frame + time badge per cell (default)
outputPath: "output/intro-grid.png", // optional: also write it
});

A grid shows easing hitches, wrap jumps, wrong z-order and missing cuts in one image. Default cap is 64 frames (maxFrames, hard max 120). Not for vision layers — see Vision.

src/film.test.ts
import { describe, expect, it } from "vitest";
import { HeadlessMediaRenderer } from "framefields";
import { buildFilm } from "./film.js";
describe("hero", () => {
it("draws the title by frame 30", async () => {
const comp = await buildFilm();
const png = await comp.renderFrame({ frame: 30, renderer: new HeadlessMediaRenderer() });
expect(png.length).toBeGreaterThan(1000);
// decode with skia-canvas and sample pixels at known coordinates
});
});
import { formatQaReport } from "framefields";
const result = await comp.renderVideo({
outputPath: "output/film.mp4",
quality: "high", // "very_low" | "low" | "high" | "very_high" (default)
codec: "h264", // default, MP4; "vp9" | "vp8" → WebM
concurrency: 4,
qa: { targetLufs: -14 }, // or true
});
console.log(formatQaReport(result.qa!));
if (!result.qa!.passed) process.exitCode = 1;
process.exit();
Option Notes
outputPath written there; result.filePath is the final path
quality bitrate preset
codec, audioCodec h264 → MP4 + AAC; vp9/vp8 → WebM + Opus; aac/mp3/opus alone → audio-only
concurrency frames rendered in parallel
ringCapacity GPU→encoder staging depth, 2–8 (or FRAMEFIELDS_RING_CAPACITY)
qa measure frames and audio as they encode; returns result.qa
renderer share a HeadlessMediaRenderer

QA thresholds: minBlackSec (0.5), minFrozenSec (2), minSilenceSec (2), silenceDbfs (−60), targetLufs, lufsTolerance (1). qa.passed is false on any error or warning. qa.audio.integratedLufs is BS.1770 loudness (streaming normalizes to about −14).

Terminal window
ffprobe -v error -show_entries stream=codec_name,width,height,duration,r_frame_rate output/film.mp4
ffmpeg -i output/film.mp4 -vf "blackdetect=d=0.5:pix_th=0.10" -f null -
ffmpeg -i output/film.mp4 -af volumedetect -f null -

A worker uses roughly 200–400 MB of RAM (vs 2–4 GB for headless Chromium), which suits GPU instances and job queues. The framefields repo ships a Dockerfile.renderer for a renderer service.