Plugins
Plugins & Custom Renderers
Swap the renderer, add custom GPU layers, or bring your own media demuxer.
Custom Renderers
Any object that implements the Renderer interface can replace the built-in GpuRenderer. The renderer receives a Scene each tick and is responsible for writing pixels:
MyRenderer.ts
import { type Renderer, type Scene } from '@elah/core'
export class CanvasRenderer implements Renderer {
private ctx: CanvasRenderingContext2D
constructor(canvas: HTMLCanvasElement) {
this.ctx = canvas.getContext('2d')!
}
render(scene: Scene): void {
const { ctx } = this
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
// Render in zIndex order
const allClips = [
...scene.videos,
...scene.images,
...scene.texts,
].sort((a, b) => a.zIndex - b.zIndex)
for (const clip of allClips) {
ctx.globalAlpha = clip.opacity
if ('src' in clip) {
// draw image/video frame
const frame = getFrame(clip.src, scene.frame)
if (frame) ctx.drawImage(frame, clip.drawRect.x, clip.drawRect.y,
clip.drawRect.width, clip.drawRect.height)
}
}
ctx.globalAlpha = 1
}
destroy(): void {
// cleanup
}
}Custom GPU Layers
The GpuRenderer uses a layer registry. Each layer type (VideoLayer, ImageLayer, TextLayer) is a class that handles setup, texture upload, and draw calls for its clip type.
typescript
// Layers are registered on the renderer.
// To add a custom layer, extend GpuLayer (internal class):
// 1. Create a new layer class
class GradientLayer {
setup(gl: WebGL2RenderingContext): void { /* shader setup */ }
draw(gl: WebGL2RenderingContext, clip: ActiveVideoClip): void { /* draw call */ }
destroy(): void { /* cleanup */ }
}
// 2. Register on the renderer (internal API — subject to change)
renderer.registerLayer('gradient', GradientLayer)
// 3. Scene clips with matching type are routed to your layerCustom Demuxers
The decode pipeline is fully pluggable via the DemuxerFactory interface. The built-in implementation uses mediabunny, but you can swap in any demuxer that implements the interface:
typescript
import { type DemuxerFactory, type DemuxerBackend } from '@elah/core'
const myDemuxerFactory: DemuxerFactory = () => {
// Return a DemuxerBackend implementation
return {
async probe(src: string): Promise<MediaInfo> {
// Return video dimensions, duration, track info
},
async demux(
src: string,
options: DemuxOptions,
onChunk: (chunk: EncodedVideoChunk) => void
): Promise<void> {
// Feed EncodedVideoChunks to the WebCodecs decoder
},
destroy(): void {},
}
}
// Pass your factory to Preview and exportVideo
<Preview demuxerFactory={myDemuxerFactory} />
await exportVideo(project, {
fps: 30,
demuxerFactory: myDemuxerFactory,
})