npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fr0/renderer-node

v0.1.0

Published

Render a fr0 Timeline to MP4/WebM/GIF via @napi-rs/canvas + ffmpeg.

Downloads

135

Readme

@fr0/renderer-node

Render a fr0 Timeline to an H.264 MP4 file from Node.js via headless Chromium.

Status

✅ Phase γ (MVP) — done. ✅ Phase δ-6 (canvas-direct via renderer-canvas) — done.

Runs the same renderTimelineToVideo orchestrator from @fr0/renderer-browser inside Playwright-driven Chromium. Phase δ-5 rewired that orchestrator to use @fr0/renderer-canvas directly (no html2canvas, no React DOM in the encoder path), so the bundle this package ships to the browser is now significantly smaller and the per-frame work is significantly faster.

Purpose

Same MP4 as the Vite web demo produces, but callable from a Node script / CLI / CI pipeline. Useful for:

  • Automated regression tests that verify the full render pipeline end-to-end
  • Batch processing (render many templates in a loop)
  • Server-side rendering without pushing work to a user's browser

The output is byte-identical to what the Vite Export button would download, so a regression in the preview-time renderer cannot silently diverge from the Node render path.

Quick start

import { renderTimelineToFile } from '@fr0/renderer-node';
import { buildSample } from '@fr0/templates';

const timeline = buildSample('titleCard', { title: 'Hello' });

const result = await renderTimelineToFile(timeline, {
  outputPath: 'title-card.mp4',
  onComplete: (bytes) => console.log(`wrote ${bytes} bytes`),
});

console.log(result);
// {
//   outputPath: 'title-card.mp4',
//   bytes: 351630,
//   frames: 120,
//   width: 1920,
//   height: 1080,
//   fps: 30,
// }

See packages/examples/cli-render for a CLI wrapper.

How it works

renderTimelineToFile(timeline, { outputPath })
    ├─ esbuild bundles src/headless/entry.ts in-memory (cached per process).
    │  The bundle includes renderer-browser (encoder + DOM components for
    │  embed, kept around as the "second renderer"), renderer-canvas
    │  (Phase δ-5 — the actual draw path), and mp4-muxer. The headless
    │  entry exposes `window.__fr0Render` on load.
    │  React / react-dom are still bundled because the renderer-browser
    │  package re-exports DOM components even though the encoder path no
    │  longer touches them.
    │
    ├─ Writes the bundle into a minimal HTML wrapper, saves it to a temp
    │  file, and points Playwright at the file:// URL. (file:// is a secure
    │  context in Chrome, so WebCodecs VideoEncoder is available — about:blank
    │  / page.setContent is not.)
    │
    ├─ Launches a headless Playwright Chromium with --use-gl=swiftshader
    │  and no-GPU software rendering args, suitable for CI / containers.
    │
    ├─ page.goto(file://…/host.html) waits for the bundle to run.
    │
    ├─ page.evaluate((tl) => window.__fr0Render(tl), timeline) triggers
    │  the renderTimelineToVideo orchestrator inside Chrome (Phase δ-5):
    │    offscreen <canvas> → frame loop → drawTimeline (renderer-canvas)
    │      → new VideoFrame(canvas) → WebCodecs VideoEncoder (H.264)
    │      → mp4-muxer
    │    Returns the MP4 bytes as a plain number[] (Playwright cannot
    │    serialize ArrayBuffer through evaluate).
    │
    ├─ Reassembles the number[] into a Node Buffer and fs.writeFile() it
    │  to outputPath.
    │
    └─ Closes the browser + cleans the temp directory.

Public API

| Export | Purpose | |---|---| | renderTimelineToFile(timeline, options) | Top-level entry point. Returns { outputPath, bytes, frames, width, height, fps }. | | RenderTimelineToFileOptions | outputPath (required), bitrate?, onComplete?, customizeLaunch? hook for injecting your own Playwright launch options. | | buildHeadlessBundle() | Low-level: return the bundled browser entry as a string. Cached per process. Useful if you want to host the bundle from your own dev server. | | clearBundleCache() | Reset the bundle cache. Mainly for tests that mock esbuild. |

Requirements

  • Node.js 20+
  • Playwright Chromium. Downloaded on pnpm install (~200 MB binary). Re-run pnpm exec playwright install chromium if the download was skipped.
  • WebCodecs VideoEncoder — available in the full Chromium channel Playwright ships. The stripped chrome-headless-shell variant does NOT expose VideoEncoder, which is why renderTimelineToFile forces a file:// URL (secure context) and uses the full chromium binary.

Known limitations / trade-offs

  • First render per process is slow because esbuild has to bundle the encoder + its dependencies. Subsequent calls reuse the cached bundle.
  • Software H.264 encoder. Headless Chromium ships the software encoder only; hardware acceleration is advisory. For batch workloads, consider pooling browsers.
  • Profile selection. Only Constrained Baseline is used (avc1.42E0..) because the software encoder in headless Chromium does not reliably accept High Profile. Baseline handles up to 4K fine; interop with old players is a bonus.
  • Custom layer registry empty. Phase δ-5 dropped the React COMPONENTS registry from the encoder path. Sample timelines that use custom-layer components (Counter / ProgressBar / GradientText) render those layers as no-ops in the canvas pipeline. Phase δ-9 promotes the existing components to first-class IR primitives so the escape hatch goes away entirely.
  • No audio layer yet. When audio support lands in core, the encoder path and this wrapper will need an AudioEncoder branch.
  • Video layer in Node is browser-mediated. Because the encoder runs inside Playwright Chromium, video layers Just Work via <video> + currentTime seek. There is no separate Node-side video decoder.

Testing

pnpm --filter @fr0/renderer-node test

Six tests across two files:

  • bundle.test.ts (4): real esbuild call, cache identity, clearBundleCache, ensures renderTimelineToVideo symbol ends up in the bundle
  • render-to-file.smoke.test.ts (2): renders a tiny 320×240 / 5-frame Timeline through real Playwright Chromium, verifies file size + MP4 ftyp magic bytes + onComplete. Skipped automatically when chromium.executablePath() does not point at an installed binary.

The full sample-template integration check lives under packages/examples/cli-render:

pnpm --filter @fr0/example-cli-render render --sample titleCard
pnpm --filter @fr0/example-cli-render render --sample kineticTypography
pnpm --filter @fr0/example-cli-render render --sample shapeShowcase

Outputs land under packages/examples/cli-render/out/.

What this package is NOT

  • Not a general-purpose video processing library (no trimming, concat, transcoding)
  • Not FFmpeg-based — we reuse the browser encoder instead
  • Not tied to any cloud provider (runs anywhere Node + Chromium run)