@videoflow/renderer-dom
Live, scrubbable player that paints VideoJSON into a DOM host.
Classes
DomRenderer
Re-exported from `@videoflow/renderer-browser` so an app can build an external layer type against `@videoflow/renderer-dom` alone: ```ts import DomRenderer, { RuntimeVisualLayer, type LayerTypeDescriptor } from '@videoflow/renderer-dom'; class RuntimeCustomLayer extends RuntimeVisualLayer { ... } const renderer = new DomRenderer(host); renderer.registerLayerType('custom', { runtime: RuntimeCustomLayer, propertiesDefinition: CustomLayer.propertiesDefinition, }); await renderer.loadVideo(videoJSON); ```
Methods
constructor
(host: HTMLElement): DomRendererParameters
| Name | Type | Description |
|---|---|---|
host | HTMLElement |
Returns
DomRendereraddLayer
(layerJSON: LayerJSON, index?: number): Promise<void>Insert a new layer into the video at the given index (defaults to end). The new layer is constructed, initialized, and mounted into the existing `$canvas` — other layers' DOM elements are left untouched.
Parameters
| Name | Type | Description |
|---|---|---|
layerJSON | LayerJSON | The layer JSON to add. Must include a unique `id`. |
index optional | number | The insertion index in `this.layers`. Defaults to appending. |
Returns
Promise<void>compositeLayerInto
(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, layer: RuntimeBaseLayer, frame?: number): Promise<void>Rasterize a single layer (cached when possible), pipe it through the WebGL effect compositor if it declares effects, and `drawImage` the result onto `ctx`. Used by `RuntimeGroupLayer.renderFrame` to flatten each child onto the group's canvas. The child's `blendMode` is honoured here via `globalCompositeOperation` so children inside a group blend identically in live preview and export. Without this, group-children blend modes would silently no-op in DomRenderer because the children render on the off-screen `virtualRoot` (their CSS `mix-blend-mode` never has a chance to take effect) — only the group's own composited canvas is on-screen.
Parameters
| Name | Type | Description |
|---|---|---|
ctx | CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | |
layer | RuntimeBaseLayer | |
frame optional | number |
Returns
Promise<void>createRuntimeLayer
(layerJSON: LayerJSON): RuntimeBaseLayerInstantiate the runtime layer registered for `layerJSON.type`, wired to this renderer and the active 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
| Name | Type | Description |
|---|---|---|
layerJSON | LayerJSON |
Returns
RuntimeBaseLayerdestroy
(clearShadow: boolean): voidTear down the renderer: stop playback (also exits smooth-playback mode on every video layer), destroy every runtime layer (releases media elements, decoders, and `requestVideoFrameCallback` subscriptions), clear the per-layer effect canvases, dispose the rasterizer's per-layer surfaces and the WebGL effect compositor's GL context, and finally empty the shadow DOM. Pending queued mutations registered before destroy resolve to no-ops via the `destroyed` guard.
Parameters
| Name | Type | Description |
|---|---|---|
clearShadow | boolean | Whether to wipe the shadow DOM. Default `true`. Pass `false` if the host element is being removed anyway and you want to keep the last rendered frame visible until removal. |
getLayerType
(type: string): LayerTypeDescriptor | undefinedThe descriptor registered for `type` on this renderer, if any.
Parameters
| Name | Type | Description |
|---|---|---|
type | string |
Returns
LayerTypeDescriptor | undefinedgetPropertyDefinition
(layerType: string): Record<string, PropertyDefinition> | undefinedReturn the full propertiesDefinition for a layer type, or a single property's definition. Resolved through this renderer's layer-type registry, so an overridden type gets its overriding property definitions too.
Parameters
| Name | Type | Description |
|---|---|---|
layerType | string |
Returns
Record<string, PropertyDefinition> | undefined(layerType: string, prop: string): PropertyDefinition | undefinedReturn the full propertiesDefinition for a layer type, or a single property's definition. Resolved through this renderer's layer-type registry, so an overridden type gets its overriding property definitions too.
Parameters
| Name | Type | Description |
|---|---|---|
layerType | string | |
prop | string |
Returns
PropertyDefinition | undefinedgetVirtualLayerHost
(): NodeWhere group layers should park their hidden child host. We use the shadow root so renderer CSS (which is shadow-scoped) still applies to the group's children — without this, descendants would lose their `--vw` / `--project-*` context and render at the wrong scale.
Returns
NodelistLayerTypes
(): string[]Every layer type registered on this renderer, built-ins included.
Returns
string[]loadFont
(fontName: string): Promise<void>Load a Google Font and make it available to the document (and shadow DOM).
Parameters
| Name | Type | Description |
|---|---|---|
fontName | string |
Returns
Promise<void>loadVideo
(videoJSON: VideoJSON): Promise<void>Load a compiled VideoJSON into the renderer. Sets up the shadow DOM, creates runtime layers, initialises media, and renders frame 0. Closes the registerLayerType window — synchronously on entry, so the cutoff doesn't depend on when the mutation queue gets around to the actual load.
Parameters
| Name | Type | Description |
|---|---|---|
videoJSON | VideoJSON | Compiled VideoJSON from VideoFlow.compile(). |
Returns
Promise<void>play
(options: { fpsCallback?: (fps: number) => void }): Promise<void>Start real-time playback from the current frame with audio sync. Renders audio to WAV, creates an `Audio` element, and advances frames inside a `requestAnimationFrame` loop while nudging the audio `playbackRate` to keep video and audio in sync. Frame dropping -------------- Targets are computed from wall-clock time (`Date.now() - startTime`) and each `renderFrame` call is fired-and-forget — playback never waits for a render to finish. If the renderer can't keep up (a single frame takes longer than one tick), renderFrame's internal `pendingFrame` slot supersedes the in-flight target with the latest one, so intermediates are skipped rather than serialized. Wall-clock time and audio stay live; the picture catches up to the most recent frame the renderer can produce. Each layer is switched into its smooth-playback path via RuntimeBaseLayer.enterSmoothPlayback at the start of the loop — for video layers this trades the per-frame `currentTime` seek for a native `<video>.play()` plus drift correction, eliminating the seek cost that otherwise dominates live-preview frame budget. The path is reverted on `stop()` / `seek()` so scrubbing stays frame-accurate.
Parameters
| Name | Type | Description |
|---|---|---|
options | { fpsCallback?: (fps: number) => void } | Optional callbacks: - `fpsCallback(fps)` — fired on each successful render with the rolling 1-second count of actually-rendered frames. With frame dropping this can sit below the video's nominal fps; that's the meaningful signal (HUD / diagnostic display). To track frame changes, set the public onFrame property. |
Returns
Promise<void>registerLayerType
(type: string, descriptor: LayerTypeDescriptor): voidRegister (or replace) a layer type on **this** renderer. ```ts const renderer = new DomRenderer(host); renderer.registerLayerType('custom', { runtime: RuntimeCustomLayer, propertiesDefinition: CustomLayer.propertiesDefinition, }); await renderer.loadVideo(videoJSON); ``` Lifecycle: register after construction and **before** the first `loadVideo()`. Registrations stay attached to this instance, so every later `loadVideo()` / `addLayer()` uses them without re-registering. Registering after a video has been loaded throws rather than silently rebuilding the live runtime layers (which would drop their loaded media and mounted DOM) — construct a new DomRenderer instead. Registering a type that already exists (including a built-in) replaces the previous descriptor rather than throwing. The registry belongs to this instance: other renderers are unaffected.
Parameters
| Name | Type | Description |
|---|---|---|
type | string | |
descriptor | LayerTypeDescriptor |
removeLayer
(id: string): Promise<void>Remove a layer from the video. Destroys the layer (releasing its media ref) and detaches its DOM element. Other layers are untouched.
Parameters
| Name | Type | Description |
|---|---|---|
id | string |
Returns
Promise<void>renderAudio
(): Promise<AudioBuffer | null>Render the full audio track as an AudioBuffer. Recurses through groups via the shared mixer — each group renders into its own intermediate buffer so the group's own `volume` / `pan` / `pitch` / `mute` / transitions apply to the whole sub-mix.
Returns
Promise<AudioBuffer | null>renderFrame
(frame: number, force: boolean): Promise<void>Render a specific frame to the DOM. Skips if already at that frame (unless forced), queues if a render is already in progress.
Parameters
| Name | Type | Description |
|---|---|---|
frame | number | Frame number to render. |
force | boolean | Render even if already at this frame. |
Returns
Promise<void>reorderLayers
(orderedIds: string[]): Promise<void>Reorder layers to match the given id sequence. The runtime layers and the backing `videoJSON.layers` array are reordered, and the layers' `$element`s are re-appended to `$canvas` in the new order. For layers without a `track`, DOM order drives paint order. Layers with a `track` carry an explicit z-index (`track + 1`) written in `applyProperties`, so reordering them in the array without changing their `track` only changes DOM order, not visual stacking. No media is touched.
Parameters
| Name | Type | Description |
|---|---|---|
orderedIds | string[] | The new layer order. Must contain exactly the set of currently-loaded layer ids, in any order. Extra/missing ids throw. |
Returns
Promise<void>seek
(frame: number): Promise<void>Seek to a frame. If playback is active, stops it (which exits smooth mode), renders the target frame deterministically through the seek path, then restarts playback (re-entering smooth mode). When playback is not active, the frame is decoded by the seek path directly — pixel-deterministic, the same path used by export.
Parameters
| Name | Type | Description |
|---|---|---|
frame | number |
Returns
Promise<void>stop
(): voidStop playback. Bumps the play-token (so any loop suspended in `await renderAudio()` / `requestAnimationFrame` exits without running its cleanup), tears down the `Audio` element, and reverts every layer's smooth-playback hook so the next `renderFrame` (typically from `seek()` or `currentTime =`) decodes the exact requested timestamp instead of whatever the smooth decoder happened to be presenting.
updateLayer
(id: string, patch: { animations?: Animation[]; effects?: any[]; properties?: Record<string, any>; settings?: Partial<LayerSettingsJSON>; track?: number | null; transitionIn?: any; transitionOut?: any }): Promise<void>Apply a property / settings / animations patch to a single layer. - `settings` is shallow-merged into `layer.json.settings`. - `properties` replaces `layer.json.properties` wholesale so keys removed by the editor (e.g. via a reset-to-default) are actually cleared. Callers should pass the full post-mutation properties object. - `animations` replaces the array wholesale (same rationale — callers hold the diffing logic because per-keyframe reconciliation is cheap to do in editor state). If `settings.source` changed, the layer's media is re-initialized via `layer.initialize()` — callers should debounce rapid source swaps. If a text layer's `fontFamily` is among the patched properties, the font is loaded into the shadow DOM before the frame is re-rendered.
Parameters
| Name | Type | Description |
|---|---|---|
id | string | The layer id to patch. |
patch | { animations?: Animation[]; effects?: any[]; properties?: Record<string, any>; settings?: Partial<LayerSettingsJSON>; track?: number | null; transitionIn?: any; transitionOut?: any } | A partial patch. Any subset of settings/properties/animations. |
Returns
Promise<void>updateVideo
(patch: { backgroundColor?: string; duration?: number; height?: number; name?: string; width?: number }): Promise<void>Patch top-level video properties (width, height, backgroundColor, name, duration). `fps` changes are not supported here — they invalidate frame numbers across the pipeline and require a full `loadVideo()`. `duration` is safe to update incrementally: it's only used as the loop bound in `play()` and the divisor in the `totalFrames` getter; layer frame numbers depend on `fps`, not `duration`. An in-flight `play()` call captures `durationSec` at start, so the new bound takes effect on the next `play()` invocation.
Parameters
| Name | Type | Description |
|---|---|---|
patch | { backgroundColor?: string; duration?: number; height?: number; name?: string; width?: number } |
Returns
Promise<void>registerEffect
(name: string, glsl: string, params: Record<string, EffectParamDefinition>): voidRegister a GLSL effect. Shares its registry with `BrowserRenderer`.
Parameters
| Name | Type | Description |
|---|---|---|
name | string | |
glsl | string | |
params | Record<string, EffectParamDefinition> |
registerTransition
(name: string, fn: TransitionFn): voidRegister a transition preset. The registry is shared with `BrowserRenderer` so a transition registered on either renderer is available in both live preview and export.
Parameters
| Name | Type | Description |
|---|---|---|
name | string | |
fn | TransitionFn |
Properties
| Name | Type | Description |
|---|---|---|
currentFrame | number | Current frame rendered. |
layerById | Map<string, RuntimeBaseLayer> | Fast id → runtime layer lookup, kept in sync with `layers`. |
layers | RuntimeBaseLayer[] | Runtime layer instances. |
onFrame | (frame: number) => void | null | Optional callback fired whenever a new frame is rendered. Set this externally to keep a UI (seek bar, time label, …) in sync with playback. |
playing | boolean | Whether playback is active. |
currentTime | | |
duration | | |
fps | | |
totalFrames | |
Type aliases
DomRendererCallback
type DomRendererCallback = (event: string, data: any) => void