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

@videoflow/renderer-server

v1.2.2

Published

Server-side video renderering for VideoFlow — renders JSON videos into MP4/WebM videos directly in the browser — Open Source Remotion alternative

Readme

@videoflow/renderer-server

npm license

Render VideoFlow videos to MP4 on Node.js. Drives a headless Chromium via Playwright so the server reuses the exact same rendering pipeline as the browser — pixel-for-pixel identical output to @videoflow/renderer-browser and @videoflow/renderer-dom.

Renderers docs: videoflow.dev/renderers · Live playground: videoflow.dev/playground


Why use this package?

  • Server-side video generation. Accept a VideoJSON payload, return an MP4 — perfect for APIs, batch jobs, and background workers.
  • WebCodecs-accelerated by default. The headless browser encodes the entire video in-process via BrowserRenderer.exportVideo(); the finished MP4 is POSTed back to Node — no per-frame screenshot, no JPEG re-encode. Significantly faster than pipelining through ffmpeg.
  • ffmpeg fallback. When you need ffmpeg-specific flags downstream, switch to the alternative pipeline with { ffmpeg: true } — VideoFlow renders frames as JPEG and pipes them to ffmpeg for x264 + AAC encoding.
  • Same pixels as the browser. Both pipelines run inside a real Chromium, so transitions, GLSL effects, fonts, and mix-blend-mode blends look identical to what your users see in @videoflow/renderer-dom.
  • Cancellable + observable. Every render accepts an AbortSignal and an onProgress callback.

Installation

npm install @videoflow/core @videoflow/renderer-server
npx playwright install chromium

Requirements

  • Node.js 18+
  • Chromium (installed by npx playwright install chromium above)
  • ffmpeg 4.4+only required if you opt into the ffmpeg pipeline ({ ffmpeg: true }). The default pipeline does everything inside Chromium.

Installing ffmpeg (optional fallback)

# macOS
brew install ffmpeg
# Linux
sudo apt-get install ffmpeg
# Windows (Chocolatey)
choco install ffmpeg

Quick Start

import VideoFlow from '@videoflow/core';

const $ = new VideoFlow({ width: 1920, height: 1080, fps: 30 });

const title = $.addText({ text: 'Hello!', fontSize: 6, color: '#fff' });
title.fadeIn('1s');
$.wait('3s');
title.fadeOut('1s');

await $.renderVideo({
  outputType: 'file',
  output: './output.mp4',
  verbose: true,
});

$.renderVideo() auto-detects Node.js and dispatches to @videoflow/renderer-server. You can also import the renderer directly:

import VideoRenderer from '@videoflow/renderer-server';

const json = await $.compile();
await VideoRenderer.render(json, {
  outputType: 'file',
  output: './output.mp4',
});

Encoding pipelines

| Mode | When to use | Encoder | Per-frame screenshot? | | --- | --- | --- | --- | | ffmpeg: false (default) | The fast path | WebCodecs + MediaBunny inside Chromium | No | | ffmpeg: true | When you need ffmpeg flags or a non-MP4 container in the same pipeline | ffmpeg (libx264 + AAC) | Yes (JPEG via Playwright) |

The default pipeline is typically several times faster: it skips the per-frame page.screenshot() round-trip and the JPEG → H.264 re-encode. The ffmpeg pipeline remains available for projects that already build on it or that want to apply ffmpeg-specific filters.

// Force the ffmpeg pipeline (e.g. to use a non-default encoder preset downstream)
await VideoRenderer.render(json, {
  outputType: 'file',
  output: './out.mp4',
  ffmpeg: true,
});

API

VideoRenderer.render(videoJSON, options?)

One-shot static API — boots a Chromium, runs the render, cleans up.

import VideoRenderer from '@videoflow/renderer-server';

await VideoRenderer.render(videoJSON, {
  outputType: 'file',                    // 'file' | 'buffer' (default 'buffer')
  output: './video.mp4',                 // required when outputType: 'file'
  verbose: true,                         // log progress to stdout
  signal: controller.signal,             // AbortSignal — cancel mid-render
  onProgress: (p) => console.log(p),     // 0..1
  ffmpeg: false,                         // default; set true to use the ffmpeg fallback
});

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | outputType | 'file' | 'buffer' | 'buffer' | Where the rendered MP4 ends up | | output | string | — | File path; required when outputType: 'file' | | verbose | boolean | false | Print progress / pipeline info to stdout | | signal | AbortSignal | — | Cancel the in-flight render | | onProgress | (p: number) => void | — | Called with 0..1 | | ffmpeg | boolean | false | Pick the ffmpeg fallback instead of the default browser-export path |

Returns: Buffer (when outputType: 'buffer') or the absolute output path (when outputType: 'file').

Instance API

For long-running services or pipelines that re-use the same Chromium across multiple operations, construct a ServerRenderer:

import { ServerRenderer } from '@videoflow/renderer-server';

const json = await $.compile();
const renderer = new ServerRenderer(json);

try {
  // Render a single frame to JPEG
  const jpeg = await renderer.renderFrame(30);
  fs.writeFileSync('frame.jpg', jpeg);

  // Render the audio track to a WAV Buffer
  const wav = await renderer.renderAudio();
  if (wav) fs.writeFileSync('audio.wav', wav);
} finally {
  await renderer.cleanup();
}

| Method | Returns | | --- | --- | | renderer.renderFrame(frame) | Buffer (JPEG) | | renderer.renderAudio() | Buffer \| null (WAV bytes, or null if the project has no audio) | | renderer.cleanup() | Tears down the Chromium page and any ffmpeg subprocess |


Example: video-generation API

import express from 'express';
import VideoFlow from '@videoflow/core';

const app = express();
app.use(express.json());

app.post('/api/generate-video', async (req, res, next) => {
  try {
    const { title, subtitle } = req.body;

    const $ = new VideoFlow({ width: 1920, height: 1080, fps: 30 });

    const t = $.addText({ text: title, fontSize: 6, color: '#fff' });
    t.fadeIn('1s'); $.wait('1.5s');

    const s = $.addText({ text: subtitle, fontSize: 3, color: '#94a3b8', position: [0.5, 0.6] });
    s.fadeIn('500ms'); $.wait('3s');

    $.parallel([() => t.fadeOut('500ms'), () => s.fadeOut('500ms')]);

    const buffer = await $.renderVideo();   // outputType defaults to 'buffer'

    res.set('Content-Type', 'video/mp4').send(buffer);
  } catch (err) {
    next(err);
  }
});

app.listen(3000);

Example: batch generation with progress

import VideoFlow from '@videoflow/core';

const items = [
  { text: 'Slide 1', color: '#ef4444' },
  { text: 'Slide 2', color: '#10b981' },
  { text: 'Slide 3', color: '#3b82f6' },
];

for (const [i, item] of items.entries()) {
  const $ = new VideoFlow({ width: 1920, height: 1080, fps: 30 });
  const t = $.addText({ text: item.text, fontSize: 6, color: item.color });
  t.fadeIn('500ms'); $.wait('2s'); t.fadeOut('500ms');

  await $.renderVideo({
    outputType: 'file',
    output: `./output/slide-${i + 1}.mp4`,
    onProgress: (p) => process.stdout.write(`\rslide ${i + 1}: ${(p * 100).toFixed(0)}%`),
  });
  console.log(`  ✓ slide ${i + 1}`);
}

Example: cancelling a render

import VideoFlow from '@videoflow/core';

const $ = new VideoFlow({ width: 1280, height: 720, fps: 30 });
$.addVideo({}, { source: './long-clip.mp4' }, { waitFor: 'finish' });

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);   // cancel after 5s

try {
  await $.renderVideo({
    outputType: 'file',
    output: './out.mp4',
    signal: controller.signal,
  });
} catch (err) {
  if (err.name === 'AbortError') console.log('cancelled');
  else throw err;
}

Notes

  • renderVideo() cleans up after itself. The static render(...) API (and $.renderVideo(...) underneath) tears down the Chromium page on completion or abort. Long-running services should use the instance API + cleanup() to share one browser across requests instead of spawning one per call.
  • Asset URLs. When the project references HTTP(S) URLs, the headless browser fetches them itself, so anything reachable from the server works — including blob URLs you create from in-memory buffers via Playwright's route API.
  • Fonts. Google Font names referenced via fontFamily are auto-resolved through a bundled registry — no setup needed.

Related packages

Resources

License

Apache-2.0