Video Frames
Drive the renderer with a time axis and pipe raw frames into ffmpeg or ffplay.
Animated WebP and GIF tap out around a few seconds at 30 fps before the file size gets uncomfortable. For longer clips, higher frame rates, or any output you'd want to embed in a video player, render raw frames and hand them to a real video encoder. Takumi gives you the frames; ffmpeg gives you the codec.
Raw frame loop
Call render with timeMs in a loop, ask for format: "raw", and you get back an RGBA byte buffer per frame. Pipe it anywhere that takes raw pixels — ffmpeg, ffplay, a Sharp/WASM encoder, your own muxer.
import { } from "takumi-js";
const = 30;
const = 4;
const = 1200;
const = 630;
const = * ;
for (let = 0; < ; ++) {
const = await (<Scene />, {
,
,
: "raw",
,
: ( / ) * 1000,
});
// frame is a Uint8Array of width * height * 4 RGBA bytes.
consumer.write();
}Two things to know:
- The same renderer can run frames in parallel. Build an array of promises and await them in order before writing to the encoder; keeps all cores busy without reordering the stream.
devicePixelRatioworks here. Render atdpr: 1.5or2and the output is sharp on high-DPI displays.
ffmpeg pipeline
The full example lives at example/ffmpeg-keyframe-animation. Pared down:
import { } from "bun";
import { } from "takumi-js";
import { , } from "./Scene";
const = 30;
const = 1200;
const = 630;
const = * 4;
const = (
[
"ffmpeg",
"-y",
"-f",
"rawvideo",
"-pixel_format",
"rgba",
"-video_size",
`${}x${}`,
"-framerate",
`${}`,
"-i",
"pipe:0",
"-vf",
"format=yuv420p10le",
"-c:v",
"libx265",
"-crf",
"16",
"-preset",
"medium",
"-tag:v",
"hvc1",
"output.mp4",
],
{ : "pipe", : "ignore", : "ignore" },
);
const = < />;
const = .({ : }, (, ) =>
(, {
,
,
: "raw",
,
: ( / ) * 1000,
}),
);
for (let = 0; < ; ++) {
const = await [];
..();
}
..();
await .;The flags worth knowing:
-pixel_format rgbamatches Takumi's output. No conversion before encoding.-vf format=yuv420p10leconverts to a 10-bit yuv space ffmpeg's encoders prefer.-c:v libx265 -crf 16produces a high-quality H.265 stream.-c:v libx264 -crf 18is the H.264 equivalent if you care about playback compatibility over file size.-tag:v hvc1makes the resulting MP4 play in Safari and QuickTime.
ffplay live preview
The same frame loop drives a live preview via ffplay. Useful for iterating on an animation without re-encoding on every change. Full example: example/ffplay.
import { } from "bun";
import { } from "takumi-js/node";
const = 60;
const = 960;
const = 540;
const = new ();
const = (
[
"ffplay",
"-f",
"rawvideo",
"-pixel_format",
"rgba",
"-video_size",
`${}x${}`,
"-framerate",
`${}`,
"-fflags",
"nobuffer",
"-flags",
"low_delay",
"-framedrop",
"-i",
"pipe:0",
],
{ : "pipe" },
);
let = false;
(async () => {
if () return; // skip frame if last one isn't done
= true;
try {
const = await .(createFrame(.()), {
,
,
: "raw",
});
..();
} finally {
= false;
}
}, 1000 / );nobuffer, low_delay, and framedrop together push ffplay into the lowest-latency mode it has. Without framedrop, a slow frame stalls the whole stream; with it, ffplay drops frames to stay synced.
For frames that depend on outside state — a clock, a websocket message, mouse input — call render each tick and stream the result. Takumi has no opinion about the source of truth.
Last updated on