Loading...
Loading...
Render, inspect, test, and export high-performance WebGPU video and frame grids with the gitframes headless engine. Use when verifying compositions, diagnosing blank frames, profiling GPU pipelines, running visual frame grids, or exporting final MP4 video in gitframes.
npx skill4agent add gatewai-dev/gitframes gitframes-rendergitframesgitframesgitframesimport { CompositorProgramSchema } from "gitframes";
// 1. Compile spec
const spec = comp.toSpec();
// 2. Validate against strict schema
const parsed = CompositorProgramSchema.safeParse(spec);
if (!parsed.success) {
console.error("Schema validation failed:", parsed.error.format());
process.exit(1);
}npx tsc --noEmit -p .renderFrameimport fs from "node:fs/promises";
import path from "node:path";
import { HeadlessMediaRenderer } from "gitframes";
const renderer = new HeadlessMediaRenderer();
// Render frame 45 (1.5s at 30fps)
const pngBuffer = await comp.renderFrame({
frame: 45,
renderer,
});
await fs.writeFile("output/frame-45.png", pngBuffer);renderFrameGridimport fs from "node:fs/promises";
import { renderFrameGrid } from "gitframes";
// Samples frames across the intro sweep (frames 0 to 60)
const gridPng = await comp.renderFrameGrid({
frames: [0, 6, 12, 18, 24, 30, 36, 42, 48, 54, 60],
columns: 4,
cellWidth: 480, // Downscaled thumbnail size for rapid visual scanning
showLabels: true, // frame number and timestamp under each cell
});
await fs.writeFile("output/intro-grid.png", gridPng);import { describe, it, expect } from "vitest";
import { HeadlessMediaRenderer } from "gitframes";
import { buildFilm } from "../film.js";
describe("Hero Scene Conformance", () => {
it("renders non-black title text on frame 30", async () => {
const comp = await buildFilm();
const renderer = new HeadlessMediaRenderer();
const png = await comp.renderFrame({ frame: 30, renderer });
expect(png.length).toBeGreaterThan(1000);
// Use skia-canvas or pixel probes to assert color at target coordinates
});
});previewstartPreviewimport { startPreview } from "gitframes";
const session = await startPreview(
{ entry: new URL("./film.ts", import.meta.url), export: "buildFilm" },
{ title: "Launch film" },
);
console.log(`Preview at ${session.url}`);
await session.closed; // keep the process alive while the user watchesCompositionCompositionnode:pathfileURLToPath(import.meta.url)fs.gitpackage.jsonrootstartPreviewopen: truerenderVideoaudio: falserenderVideoimport path from "node:path";
const result = await comp.renderVideo({
outputPath: "output/final-video.mp4",
quality: "high", // "draft" | "medium" | "high" | "lossless"
codec: "avc", // "avc" (H.264) | "hevc" (H.265) | "vp9" | "av1"
audioCodec: "aac",
concurrency: 4, // Parallel frame encoding threads
});
// Always invoke cleanup to release temporary frame cache and file handles
await result.cleanup?.();qa: trueresult.qasrc/render.ts// src/render.ts
import fs from "node:fs/promises";
import path from "node:path";
import { HeadlessMediaRenderer, startPreview } from "gitframes";
import { buildFilm } from "./film.js";
const OUT = path.resolve(import.meta.dirname, "../output");
const [mode = "video", ...args] = process.argv.slice(2);
if (mode === "preview") {
// The browser runs film.ts itself; no need to build the film here.
const session = await startPreview({ entry: new URL("./film.ts", import.meta.url), export: "buildFilm" });
console.log(`Preview at ${session.url}`);
await session.closed; // serves until its tab closes or the next preview replaces it
process.exit(0);
}
const comp = await buildFilm();
await fs.mkdir(OUT, { recursive: true });
if (mode === "frames") {
const renderer = new HeadlessMediaRenderer();
await fs.mkdir(path.join(OUT, "frames"), { recursive: true });
for (const f of args.map(Number)) {
const png = await comp.renderFrame({ frame: f, renderer });
const file = path.join(OUT, "frames", `f${String(f).padStart(4, "0")}.png`);
await fs.writeFile(file, png);
console.log(`Rendered frame: ${file}`);
}
} else {
const started = Date.now();
const result = await comp.renderVideo({
outputPath: path.join(OUT, "output.mp4"),
quality: "high",
});
await result.cleanup?.();
console.log(`Exported video: ${result.filePath} in ${((Date.now() - started) / 1000).toFixed(1)}s`);
}
// CRITICAL: Always explicitly exit process to prevent native WebGPU/mediabunny thread hangs
process.exit(0);# Render specific keyframes for instant review:
tsx src/render.ts frames 0 18 36 72
# Live player for the user (run in the background; re-run after changes and the open tab reloads):
tsx src/render.ts preview
# Render full master video:
tsx src/render.tsrenderVideo({ qa })import { formatQaReport } from "gitframes";
const result = await comp.renderVideo({
outputPath: "output/final-video.mp4",
qa: { targetLufs: -14 }, // or `qa: true` for defaults (no loudness target)
});
console.log(formatQaReport(result.qa!));
if (!result.qa!.passed) process.exitCode = 1; // fail CI / the agent loopresult.qa | Severity | Meaning |
|---|---|---|
| error | Fewer frames encoded than the composition has |
| warning | ≥ |
| warning | Samples at or beyond full scale |
| warning | Integrated loudness more than |
| warning | Keyframes past a layer's clip window never play; has |
| info | Picture unchanged for ≥ |
| info | ≥ |
| info | The whole track is silent |
passedqa.audio.integratedLufsqa.audio.peakDbfsffmpegffprobe# 1. Check video and audio stream properties
ffprobe -v error -show_entries stream=codec_name,width,height,duration,r_frame_rate output/final-video.mp4
# 2. Detect unexpected black frames or dead transitions
ffmpeg -i output/final-video.mp4 -vf "blackdetect=d=0.5:pix_th=0.10" -f null -
# 3. Verify audio loudness levels (prevent clipping / silence)
ffmpeg -i output/final-video.mp4 -af "volumedetect" -f null -| Symptom | Root Cause | Solution |
|---|---|---|
| Entire frame renders black | Missing font registration or | Ensure every font used in |
| Media layer is invisible / blank | Bad input handle binding or zero opacity | Check that the media path exists and |
| Audio cuts out early | Missing | Explicitly assign |
Vision cutout / mask is empty on a single | Vision reads the previous rendered frame; frame 0 has none | Render two or more consecutive frames with the same |
| Ghosted double of the subject in a vision cutout | Frames rendered out of order (frame grid, or jumping from frame 3 to 48), so the cutout uses pixels from another frame | Verify vision layers with sequential frames or the exported MP4, not |
| Script hangs after rendering | Background worker thread pool held open | Always include |
| Text looks misaligned or truncated | Layout node box smaller than text string | Set explicit wrapping |
| Colors appear washed out / dark edges | Unpremultiplied alpha blending | The engine operates with premultiplied alpha. Ensure custom shaders use |