VideoFlowcodeGitHubTry itCoreRenderersReact Video EditorPlaygroundExamplesDocscodeGitHubTry it
Getting started
InstallationQuick startCore conceptsYour first video
Builder
Builder APITime formatsParallel & wait
Layers
TextImageVideoAudioCaptionsShapeGroups
Animation
Animate & keyframesEasing functionsTransitionsEffects
Renderers
Browser rendererServer rendererDOM preview
React Video Editor
QuickstartThemingUploadsCustom panelsHooks & commandsKeyboard shortcuts
API reference
Overview@videoflow/core@videoflow/renderer-browser@videoflow/renderer-server@videoflow/renderer-dom@videoflow/react-video-editor
API reference

@videoflow/renderer-browser

Encode VideoJSON to an MP4 Blob inside the browser via WebCodecs.

Classes

class

BrowserRenderer

Methods

constructor
(videoJSON: VideoJSON): BrowserRenderer
Parameters
NameTypeDescription
videoJSONVideoJSON
Returns
BrowserRenderer
captureFrame
(frame: number): Promise<OffscreenCanvas>

Capture a single frame onto the shared render canvas. 1. Ensure all layer DOMs reflect this frame (via `renderFrame`). 2. For each enabled, in-range layer, pick the cheapest path: - **Fast path (tier-1 + no effects):** paint the layer's `$element` directly onto the final canvas via `drawDirectInto` — skips the per-layer surface copy entirely. - **Normal path:** ask the `LayerRasterizer` for a project-sized bitmap (cached when the layer's final props match the previous render), pipe through the WebGL effect compositor if the layer declares effects, then `drawImage` onto the final canvas.

Parameters
NameTypeDescription
framenumber
Returns
Promise<OffscreenCanvas>
compositeLayerInto
(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, layer: RuntimeBaseLayer, frame?: number): Promise<void>

Composite one layer onto `ctx` (the group's flatten canvas, or any other caller-owned target sized to the project). - **Fast path:** when the layer is tier-1 and has no effects, paints the layer's `$element` straight onto `ctx` via `drawDirectInto` — no per-layer surface, no extra blit. - **Normal path:** rasterizes the layer (cached when possible), runs the WebGL effect compositor if effects are declared, then `drawImage`s the result onto `ctx`. Used by `RuntimeGroupLayer.renderFrame` to flatten each child onto the group's canvas — the same path that `captureFrame` uses for top-level layers.

Parameters
NameTypeDescription
ctxCanvasRenderingContext2D | OffscreenCanvasRenderingContext2D
layerRuntimeBaseLayer
frame optionalnumber
Returns
Promise<void>
createRuntimeLayer
(layerJSON: LayerJSON): RuntimeBaseLayer

Instantiate the runtime layer registered for `layerJSON.type`, wired to this renderer and this project's fps / dimensions. Used for top-level layers and — via `ILayerRenderer` — by groups for their descendants. Throws for a type that isn't registered on this renderer.

Parameters
NameTypeDescription
layerJSONLayerJSON
Returns
RuntimeBaseLayer
destroy
(): void

Tear down the renderer: destroy every runtime layer (releases media elements, decoders, audio buffers), remove the hidden `<div data-renderer>` from the DOM, drop the shared render canvas, and dispose the rasterizer's per-layer surfaces and the WebGL effect compositor's GL context. Call this after the last `captureFrame` / `exportVideo` to avoid leaking GL contexts across repeated renders.

enableElementCapture
(scale: number, mode: "auto" | "force"): Promise<boolean>

Opt into compositing each frame with a single `drawElementImage()` call instead of rasterizing every layer through an SVG `<foreignObject>`. `drawElementImage` (Chromium's "HTML in canvas", behind `--enable-blink-features=CanvasDrawElement`) paints a live element straight into a 2D context. Because the browser composites the whole project in one pass, the per-frame cost collapses to Blink's own incremental layout+paint of whatever actually changed. Measured across the example projects at 1080p, composite time per frame: ``` basic text 49.8 → 10.8 ms transitions 76.7 → 32.1 ms image bg 334.1 → 11.8 ms effects 494.9 → 32.5 ms keyframes 81.5 → 19.7 ms groups 272.7 → 0.3 ms ``` with output pixel-identical to the rasterizer path on every one of those scenes (the effect-overlay canvases are inside the container, so WebGL effects are captured too). ## Requirements, and why this is opt-in - The API only exists behind a Chromium launch flag, so it cannot be relied on in a normal browser tab. Returns `false` when unavailable and the renderer keeps using the rasterizer. - The container **must be on-screen**: `drawElementImage` silently produces a blank frame for an element parked off to the left, which is where this renderer normally hides it. Enabling therefore moves the container to the top-left of the page, where it is visible. That is fine for a headless export; in a live page the caller must accept the project being briefly painted over the document. - One layer type is not caught by this path: a layer that changes *canvas pixels* (video) needs one extra lifecycle tick before its paint record catches up, which captureFrameViaElement accounts for. Getting that wrong exports the previous video frame. ## `scale` — why capturing bigger than the frame is not a luxury `drawElementImage` paints the live DOM, so it inherits Blink's paint-time text quantisation: glyph advances land on a QUARTER of a device pixel. Under a slow tween — a 3% "life push" over 3-4s moves a headline's edges ~0.1px/frame — the type therefore holds still for several frames and then jumps a quarter pixel, which reads as judder. This is the same defect LayerRasterizer's scale/position latch exists to remove, and element capture bypasses that latch entirely because there is no per-layer raster to blit. It cannot be fixed in CSS. Measured under `drawElementImage`, on an 84px headline under `scale: 1 -> 1.03`, mean frame-to-frame jerk of the sub-pixel left ink edge: ``` plain 0.221 px frozen 65/119 frames will-change: transform 0.221 px 65/119 contain: paint 0.221 px 65/119 opacity: 0.999 0.221 px 65/119 filter: opacity(1) 0.221 px 65/119 text-rendering: geometricPrecision 0.222 px 62/119 <svg><text> 0.221 px 65/119 ``` Nothing moves it, because the quantum is defined in DEVICE pixels — so the one thing that does move it is making a device pixel smaller. Drawing the container into a host `scale`x larger and downsampling divides the quantum by `scale`: ``` 1x jerk 0.208 px frozen 36/89 4x jerk 0.004 px 0/89 2x jerk 0.050 px 0/89 3x jerk 0.026 px 0/89 ``` That is a 4x improvement for 4x the capture pixels, and it clears the HORIZONTAL axis outright. It does not clear the vertical one: the vertical quantum is a WHOLE device pixel, so 2x still leaves half-pixel steps, and an html component (a full-frame subtree scaled about the frame centre) drifts its off-centre content at only ~0.09 px/frame under a 3% push. Matching the latched rasterizer there would take ~16x. Supersampling is therefore a mitigation, not a cure — mode `'auto'` is the cure. Which is why it is **off by default**: it never fully solved the problem it was aimed at, and it cost more than element capture saves (see DEFAULT_ELEMENT_CAPTURE_SCALE for the wall-clock table). Raise it per-render when a deliverable's whole point is very slow type motion and you would rather pay the pixels than fall back. Idempotent; safe to call before or after the first render.

Parameters
NameTypeDescription
scalenumbersupersample factor for the capture host (default 1 — no supersampling; clamped to 1-4 and rounded). Also tightens/loosens the `'auto'` decline threshold, which is `1 / scale` px per frame.
mode"auto" | "force"`'auto'` (the default) declines element capture for projects that contain DOM motion slower than the paint grid, where the rasterizer's latch is worth more than the speed — see subGridDomMotion. `'force'` takes it regardless.
Returns
Promise<boolean>
exportVideo
(options: RenderOptions): Promise<Blob>

Export the full video as an MP4 blob using MediaBunny. When `options.worker` is not `false` (the default), SVG rasterisation and MediaBunny encoding are offloaded to a dedicated Web Worker so that the main thread stays responsive. Set `worker: false` to encode entirely on the main thread (useful when Workers are unavailable).

Parameters
NameTypeDescription
optionsRenderOptionsRendering options including abort signal.
Returns
Promise<Blob>
getLayerType
(type: string): LayerTypeDescriptor | undefined

The descriptor registered for `type` on this renderer, if any.

Parameters
NameTypeDescription
typestring
Returns
LayerTypeDescriptor | undefined
getPropertyDefinition
(layerType: string): Record<string, PropertyDefinition> | undefined

Look up the full property definitions for a layer type, or a single property. Resolved through this renderer's layer-type registry, so an overridden type gets its overriding property definitions too.

Parameters
NameTypeDescription
layerTypestring
Returns
Record<string, PropertyDefinition> | undefined
(layerType: string, prop: string): PropertyDefinition | undefined

Look up the full property definitions for a layer type, or a single property. Resolved through this renderer's layer-type registry, so an overridden type gets its overriding property definitions too.

Parameters
NameTypeDescription
layerTypestring
propstring
Returns
PropertyDefinition | undefined
getVirtualLayerHost
(): Node

Where group layers should park their hidden child host. For the export renderer we use `document.body` directly so the children's CSS resolves against the page's layout context (the same context the main `$canvas` uses).

Returns
Node
listLayerTypes
(): string[]

Every layer type registered on this renderer, built-ins included.

Returns
string[]
loadFont
(fontName: string): Promise<void>

Load a Google Font by name. Constructs the exact CSS2 URL for the font using the bundled Google Fonts registry (axis ranges, italic support, weight variants) so the request always matches what the API expects — preventing 400 errors that arise from requesting unsupported axis combinations.

Parameters
NameTypeDescription
fontNamestring
Returns
Promise<void>
registerLayerType
(type: string, descriptor: LayerTypeDescriptor): void

Register (or replace) a layer type on **this** renderer. ```ts const renderer = new BrowserRenderer(videoJSON); renderer.registerLayerType('custom', { runtime: RuntimeCustomLayer, propertiesDefinition: CustomLayer.propertiesDefinition, }); await renderer.exportVideo(); ``` Lifecycle: register after construction and **before** the first `initLayers()` / `renderFrame()` / `captureFrame()` / `renderAudio()` / `exportVideo()` call — those create the runtime layers and close the registration window. Registering a type that already exists (including a built-in) replaces the previous descriptor rather than throwing, which is how you override a built-in type for one renderer instance. The registry belongs to this instance: other renderers are unaffected.

Parameters
NameTypeDescription
typestring
descriptorLayerTypeDescriptor
renderAudio
(): Promise<AudioBuffer | null>

Render all audio layers (including layers nested inside groups) into a single AudioBuffer using OfflineAudioContext. Group layers are first mixed down to their own intermediate buffer — the group's own `volume` / `pan` / `pitch` / `mute` / transitions are then applied on the parent timeline, mirroring the visual sub-mix model. 48 kHz is the universal sample rate for video AAC encoding — 44.1 kHz fails the WebCodecs `AudioEncoder.isConfigSupported` check on some platforms (notably headless Chrome on Linux).

Returns
Promise<AudioBuffer | null>
renderFrame
(frame: number, force: boolean): Promise<void>

Render a single frame to the DOM. Each enabled layer computes its interpolated properties at the given frame and applies them to its DOM element.

Parameters
NameTypeDescription
framenumber
forceboolean
Returns
Promise<void>
registerEffect
(name: string, glsl: string, params: Record<string, EffectParamDefinition>): void

Register a GLSL effect. `glsl` is the fragment-shader body implementing `vec4 effect(sampler2D tex, vec2 uv, vec2 resolution)`; each entry in `params` becomes a `u_<name>` uniform. Like transitions, the registry is shared with `DomRenderer`.

Parameters
NameTypeDescription
namestring
glslstring
paramsRecord<string, EffectParamDefinition>
registerTransition
(name: string, fn: TransitionFn): void

Register a transition preset. The same registry backs both `BrowserRenderer` and `DomRenderer`, so a transition registered here is immediately usable in live preview too.

Parameters
NameTypeDescription
namestring
fnTransitionFn
render
(videoJSON: VideoJSON, options: RenderOptions): Promise<Blob | ArrayBuffer>

Render a VideoJSON to a video Blob or ArrayBuffer. This is the primary public API. It creates a BrowserRenderer instance, initialises all layers, renders every frame, encodes the result via MediaBunny, and returns the output.

Parameters
NameTypeDescription
videoJSONVideoJSONThe compiled video JSON.
optionsRenderOptionsRendering options (outputType, signal, etc.).
Returns
Promise<Blob | ArrayBuffer>

Properties

NameTypeDescription
currentFramenumberFrame being rendered right now (for dedup / cancellation).
layersRuntimeBaseLayer[]Runtime layer wrappers.
loadedFontsRecord<string, string>Cache of loaded Google Fonts — maps font name → stylesheet URL.
elementCaptureDeclinedReason
elementCaptureSupersample
usesElementCapture

Functions

function

getEffect

(name: string): EffectDefinition | undefined

Retrieve a registered effect.

Parameters
NameTypeDescription
namestring
Returns
EffectDefinition | undefined
function

getTransition

(name: string): TransitionFn | undefined

Look up a previously registered transition's function.

Parameters
NameTypeDescription
namestring
Returns
TransitionFn | undefined
function

listEffects

(): string[]

List registered effect names.

Returns
string[]
function

listTransitions

(): string[]

List registered transition names.

Returns
string[]
function

registerEffect

(name: string, glslOrPasses: string | EffectPass[], params: Record<string, EffectParamDefinition>): void

Register an effect. Pass either: - a string `glsl` body for single-pass effects, or - an array of `EffectPass` for multi-pass effects. Last registration wins.

Parameters
NameTypeDescription
namestring
glslOrPassesstring | EffectPass[]
paramsRecord<string, EffectParamDefinition>
function

registerTransition

(name: string, fn: TransitionFn, options: RegisterTransitionOptions): void

Register a transition preset under `name`. Last registration wins. `options.defaultEasing` sets the easing applied to `p` when the layer's `transitionIn` / `transitionOut` spec does not specify one. Layers can always override it via the `easing` field of their transition spec.

Parameters
NameTypeDescription
namestring
fnTransitionFn
optionsRegisterTransitionOptions

Type aliases

type

EffectDefinition

A registered effect. Has either `glsl` (single-pass) or `passes`.

type EffectDefinition = 
type

EffectParamDefinition

Metadata describing a single effect parameter. `default` is used when the layer's JSON does not override it. For `color`, the value is a CSS colour string (`"#rrggbb"` / `"rgba(..)"`), converted to a `vec4` by the compositor at draw time. For `option`, the value is one of the keys of `fieldConfig.options`; the compositor resolves it to its index and binds it as an `int u_<name>` uniform. `fieldConfig` carries all editor UI hints — step, integer coercion, unit, option labels — and also drives runtime unit conversion (`unit: 'em'` → pixels). See EffectParamFieldConfig.

type EffectParamDefinition = 
type

EffectParamType

Supported GL uniform types for effect parameters.

type EffectParamType = "float" | "int" | "bool" | "vec2" | "vec3" | "vec4" | "color" | "option"
type

RegisterTransitionOptions

Options accepted by registerTransition.

type RegisterTransitionOptions = 
type

TransitionDefinition

Full transition entry as stored in the registry.

type TransitionDefinition = 
type

TransitionFn

A transition implementation. - `p` — signed progress, `-1..+1` as described above (already eased). - `properties` — the layer's resolved, unit-ized properties at this frame. Mutate in place or return a new object; either works. - `params` — free-form per-preset parameters from `LayerTransitionJSON.params`. - `context` — per-call context (e.g. seed for deterministic randomness).

type TransitionFn = (p: number, properties: Record<string, any>, params: Record<string, any>, context: TransitionContext) => Record<string, any>

On this page

ClassesFunctionsType aliases
VideoFlow

Open-source toolkit for composing videos from code.

Product

CoreRenderersReact Video EditorPlayground

Learn

DocsAPI referenceExamplesvs. Remotionvs. FFmpeg

Project

GitHubLicenseContactTermsPrivacy

From the blog

All posts →The 50-Line YouTube Shorts Factory: Programmatic Video with VideoFlowMastering Motion: How to Animate Effect Parameters in VideoFlowMastering Flow: How to Use $.wait, $.parallel, and $.group in VideoFlowParallel vs Sequential: Mastering the VideoFlow Timing ModelOne JSON, Three Renderers: Mastering the VideoFlow LifecycleGenerating Multi-Language Video at Scale: The Localization PlaybookBrowser-Side Video Export: Zero-Server Rendering with WebCodecsHow to Build a Dynamic Video Editor in React in 10 Minutes
© 2026 VideoFlow. Apache-2.0 core.