Live preview with the DOM renderer
@videoflow/renderer-dom paints a running composition into a DOM element you own. It's what the playground and the React video editor use internally. Seekable, no recompilation on each edit, and completely isolated in a shadow root.
Minimal example
import DomRenderer from '@videoflow/renderer-dom';
const host = document.getElementById('player');
const r = new DomRenderer(host);
await r.loadVideo(videoJSON);
await r.play(); // auto-loops
// Or scrub precisely:
await r.seek(42); // frame 42
await r.renderFrame(0); // paint a single frame
r.stop();
r.destroy();Methods
| Method | Effect |
|---|---|
new DomRenderer(host) | Attach to a DOM element. Creates a shadow root internally. |
.loadVideo(json) | Load or swap in a VideoJSON. Resolves when media is ready. |
.play() | Start a rAF loop. Auto-loops by default. |
.stop() | Pause at the current frame. |
.seek(frame) | Jump to a specific frame (integer). |
.renderFrame(frame) | Paint a single frame without starting a play loop. |
.destroy() | Tear down. Call on unmount. |
Properties & events
| Name | Notes |
|---|---|
onFrame: (frame) => void | Fires on every painted frame during playback. Use for a scrub bar. |
currentFrame | Read-only. Last painted frame. |
duration | Composition duration in seconds. |
fps | From the loaded video. |
Granular updates
Calling loadVideo() reloads everything. For typing-speed edits, use the incremental patch API — it skips decode for unchanged layers:
await r.updateVideo({ duration: 4 }); // top-level patch
await r.addLayer(newLayer); // append a layer
await r.removeLayer(layerId); // drop one
await r.reorderLayers([id3, id1, id2]); // reorder
// Patch an existing layer. Each key is its own channel — `properties` and
// `animations` REPLACE wholesale, so pass the full post-edit object; only
// `settings` is shallow-merged.
await r.updateLayer(layerId, {
properties: { ...layer.properties, scale: 1.2 },
settings: { startTime: 1.5 },
});Every mutation is queued internally, so they can be fired without awaiting each one, and each concludes by re-rendering the current frame.
Custom layer types
DomRenderer owns its own layer-type registry, so a preview can render layer types the library doesn't ship. Register between construction and the first loadVideo():
import DomRenderer, { RuntimeVisualLayer } from '@videoflow/renderer-dom';
const r = new DomRenderer(host);
r.registerLayerType('custom', {
runtime: RuntimeCustomLayer,
propertiesDefinition: CustomLayer.propertiesDefinition,
});
await r.loadVideo(videoJSON);Registrations stay attached to that instance and survive reloads. Register the same type on the renderer you export with and preview and export agree. See the browser renderer guide for the full contract.
Use it as a headless player. Mount it in a hidden host, call renderFrame(n) to rasterise thumbnails, and read the canvas contents — no MP4 encode needed.