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-browser

v1.2.1

Published

Browser-side video renderer for VideoFlow

Readme

@videoflow/renderer-browser

npm license

Render VideoFlow videos to MP4 entirely in the browser — no server, no upload, no backend. Built on WebCodecs, MediaBunny, and a per-layer rasterization pipeline that pushes encoding to a Web Worker so the page stays responsive.

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


Why use this package?

  • Real MP4 files generated client-side — H.264 video + AAC (or Opus) audio, muxed by MediaBunny.
  • Zero server cost. The user's device does the work; you keep their footage on-device.
  • Worker-based encoding. Frame rasterization stays on the main thread (SVG <foreignObject> decode requires DOM), but encoding/muxing run in a dedicated Worker — UI stays smooth.
  • Tier-based per-layer rasterization. Static / simple-transform layers paint with a direct drawImage fast path; complex layers go through a cached SVG rasterizer.
  • WebGL effect compositor. Layers with effects are piped through a ping-pong shader pipeline.
  • Audio sub-mixes for groups. A $.group(...) whose children produce audio is rendered as its own buffer first, then placed on the parent timeline — group-level volume / pan / pitch / mute / fade transitions apply to the sub-mix as a whole.

This is the export renderer. For interactive playback / scrubbing in the same browser, pair it with @videoflow/renderer-dom — they share the same transition + effect registries, so live preview and exported MP4 always agree.


Installation

npm install @videoflow/core @videoflow/renderer-browser

Quick Start

import VideoFlow from '@videoflow/core';
import VideoRenderer from '@videoflow/renderer-browser';

// 1. Define a video
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');

// 2. Compile to JSON, render to an MP4 Blob
const json = await $.compile();
const blob = await VideoRenderer.render(json);

// 3. Download
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'hello.mp4';
a.click();
URL.revokeObjectURL(a.href);

You can also bypass compile() and let VideoFlow auto-detect the environment:

const blob = await $.renderVideo();   // → Blob in the browser

API

VideoRenderer.render(videoJSON, options?)

Static one-shot — creates a renderer, exports an MP4, and tears everything down.

const blob = await VideoRenderer.render(videoJSON, {
  signal: controller.signal,            // AbortSignal — cancel mid-encode
  onProgress: (p) => console.log((p * 100).toFixed(1) + '%'),
  worker: true,                         // default; pass false to encode on the main thread
});

Options

| Option | Type | Description | | --- | --- | --- | | signal | AbortSignal | Cancel the encode mid-flight | | onProgress | (p: number) => void | Called with 0..1 during encode | | worker | boolean | Encode in a dedicated Worker (default true). Set false for environments without Worker support |

Returns: Blob (video/mp4).

VideoRenderer.renderFrame(videoJSON, frame)

const canvas = await VideoRenderer.renderFrame(videoJSON, 30);  // OffscreenCanvas

VideoRenderer.renderAudio(videoJSON)

const audioBuffer = await VideoRenderer.renderAudio(videoJSON); // AudioBuffer | null

Returns null when the project has no audio layers.

Instance API

For long-lived previews (re-rendering many frames, sharing one rasterizer cache, …) use the constructor + captureFrame() / exportVideo():

import BrowserRenderer from '@videoflow/renderer-browser';

const renderer = new BrowserRenderer(videoJSON);
try {
  for (let f = 0; f < total; f++) {
    const offscreen = await renderer.captureFrame(f);
    // …consume the OffscreenCanvas…
  }
} finally {
  renderer.destroy();
}

How it works

  1. Layer mounting. Each layer becomes a DOM element in an off-screen [data-renderer] container. CSS handles transforms, filters, blend modes, fonts, and mix-blend-mode blending.
  2. Per-frame property pass. Every layer's interpolated properties at the current frame are written as inline CSS / custom properties.
  3. Tier-based rasterization (LayerRasterizer):
    • Tier 1 — simple transform + no filters/borders/shadows → straight drawImage from the layer's source bitmap onto the destination canvas.
    • Tier 3 — anything else (rotation, 3D, filters, text, shapes, effects-bearing layers) → rasterized through an SVG <foreignObject>, cached per layer until the resolved props change.
  4. Effect pipeline. Layers with effects are piped through a WebGLEffectCompositor (ping-pong FBOs) before composite.
  5. Composite onto the final canvas. Layers paint in sorted track order with their blendMode applied via globalCompositeOperation. Groups composite their children onto a private project-sized surface first, then drop that surface onto the parent.
  6. Audio mix. An OfflineAudioContext mixes every audio-bearing layer (recursing through groups). volume/pan keyframes drive AudioParam automation; pitch is decoupled from speed via an offline granular pitch shifter; mute short-circuits the source.
  7. Encode + mux. Frames and audio are fed into a Worker-resident MediaBunny pipeline (WebCodecs VideoEncoder / AudioEncoder), which produces the final MP4 buffer.

Transitions

Built-in transition presets (the same library used by @videoflow/renderer-dom) auto-register on import. See the core README → Transitions for the full categorised table and the signed-p contract.

Custom transitions

BrowserRenderer.registerTransition() writes to a registry shared with DomRenderer — register once, works in both export and live preview.

import BrowserRenderer from '@videoflow/renderer-browser';

BrowserRenderer.registerTransition('spinIn', (p, properties, params, ctx) => {
  const t = 1 - Math.abs(p);                     // 0 at edges, 1 at rest
  properties.rotation = (properties.rotation ?? 0) + (1 - t) * (params.angle ?? 360);
  properties.opacity  = (properties.opacity  ?? 1) * t;
  return properties;
}, {
  defaultEasing: 'easeOut',
  layerCategory: 'visual',                       // 'all' | 'visual' | 'audio' | 'textual'
});

The function receives:

  • p — signed progress in [-1, +1], already eased per the layer's easing. -1 is the start of transitionIn, 0 is rest, +1 is the end of transitionOut.
  • properties — the layer's resolved properties at this frame. Mutate in place or return a new object.
  • params — values from the layer's transitionIn.params / transitionOut.params.
  • ctx{ seed, frame, fps, projectWidth, projectHeight } for deterministic per-layer randomness and aspect-aware geometry.

Set injectsEffects: true if your preset pushes synthetic effects onto properties.__effects — the renderer keeps the effect overlay mounted across the layer's lifetime so the WebGL pipeline always engages.


GLSL effects

Built-in effects (chromaticAberration, pixelate, vignette, rgbSplit, invert, bloom, colorCorrection, frostedGlass, lightSweep, gaussianBlur, motionBlur, noiseDissolve, …) are auto-registered on import. Reference them by name from a layer's effects property; animate any param via a dot-path key.

Custom effects

import BrowserRenderer from '@videoflow/renderer-browser';

BrowserRenderer.registerEffect(
  'glitchShift',
  `
vec4 effect(sampler2D tex, vec2 uv, vec2 resolution) {
  vec2 shifted = uv + vec2(u_amount * sin(uv.y * 40.0), 0.0);
  return texture2D(tex, shifted);
}`,
  {
    amount: { type: 'float', default: 0.02, min: 0, max: 0.1, animatable: true },
  },
);

The GLSL snippet defines a single vec4 effect(sampler2D tex, vec2 uv, vec2 resolution). Each declared param becomes a u_<name> uniform. The compositor wraps the snippet with the precision/uniform/varying boilerplate at registration time.

Param types: 'float', 'int', 'bool', 'vec2', 'vec3', 'vec4', 'color' (CSS colour string → vec4).


End-to-end example: an export button

<button id="exportBtn">Export Video</button>
<progress id="prog" max="1" value="0"></progress>

<script type="module">
  import VideoFlow from '@videoflow/core';
  import VideoRenderer from '@videoflow/renderer-browser';

  document.getElementById('exportBtn').addEventListener('click', async () => {
    const $ = new VideoFlow({ width: 1280, height: 720, fps: 30 });

    $.addImage(
      {
        fit: 'cover',
        effects: [
          { effect: 'vignette', params: { strength: 0.6 } },
          { effect: 'chromaticAberration', params: { amount: 0.003 } },
        ],
      },
      { source: './photo.jpg', sourceDuration: '5s' },
    );

    const title = $.addText(
      { text: 'Made with VideoFlow', fontSize: 5, fontWeight: 800 },
      {
        startTime: '0.5s',
        sourceDuration: '4s',
        transitionIn:  { transition: 'slideUp', duration: '600ms' },
        transitionOut: { transition: 'fade',    duration: '500ms' },
      },
    );
    $.wait('5s');

    const json = await $.compile();
    const blob = await VideoRenderer.render(json, {
      onProgress: (p) => { document.getElementById('prog').value = p; },
    });

    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'export.mp4';
    a.click();
    URL.revokeObjectURL(a.href);
  });
</script>

Notes & requirements

  • WebCodecs. Required for video encoding. Available in Chrome / Edge / recent Firefox / Safari 17+. The library probes for AAC support and falls back to Opus when AAC isn't available (notably on Linux Chrome). If neither is available the audio track is dropped and a warning is emitted — the video still encodes.
  • Cross-origin sources. Video / image / audio sources must be CORS-readable for decode() / decodeAudioData() to succeed. Same-origin or blob-URL sources always work.
  • Bundlers. The encoder Worker is bundled inline as a Blob URL, so no special bundler config is needed (esbuild / Vite / webpack all just work).

Related packages

Resources

License

Apache-2.0