Export Pipeline
The export pipeline runs frame-by-frame in a dedicated Web Worker, muxes MP4 with mediabunny, and never drifts from the live preview.
exportVideo()
exportVideo() is the primary export entry point. It spins up the export worker, renders every frame to OffscreenCanvas, muxes the result, and resolves with an MP4 Blob:
import {
exportVideo,
useTimelineEngine,
type ExportOptions,
type ExportProgress,
} from '@elah/editor'
import { useState } from 'react'
export function ExportButton() {
const engine = useTimelineEngine()
const [progress, setProgress] = useState<ExportProgress | null>(null)
const handleExport = async () => {
// fps comes from project.fps — you don't pass it to exportVideo.
const project = engine.getProject()
const options: ExportOptions = {
videoCodec: 'avc', // 'avc' | 'vp9' | 'vp8' — default: 'avc'
audioCodec: 'aac', // 'aac' | 'opus'
videoBitrate: 8_000_000, // 8 Mbps (default)
audioBitrate: 192_000,
outputHeight: 1080, // optional downscale; default = stage height
onProgress: (p) => setProgress(p),
}
try {
const blob = await exportVideo(project, options)
// Download the file
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'export.mp4'
a.click()
URL.revokeObjectURL(url)
} finally {
setProgress(null)
}
}
const percent = progress
? Math.round((progress.frame / Math.max(1, progress.totalFrames)) * 100)
: 0
return (
<div>
<button onClick={handleExport} disabled={!!progress}>
{progress ? `Exporting ${percent}%` : 'Export MP4'}
</button>
{progress && <progress value={percent} max={100} />}
</div>
)
}Export Worker
The export worker runs in a Web Worker with an OffscreenCanvas. It uses the exact same resolveTimeline() and placement math (resolveDrawRect, computeTextLayout) as the live renderer — so preview and export never drift.
To keep mediabunny out of your main bundle, use lazyExportVideo() — the same call and the same Promise<Blob> result, but the export module (and mediabunny) is dynamically imported only when you first export, so bundlers code-split it out:
import { lazyExportVideo } from '@elah/editor'
// Identical signature to exportVideo — resolves with an MP4 Blob.
// mediabunny is only loaded on first call.
const blob = await lazyExportVideo(project, options)Audio Pipeline
Web Audio API is not available in Web Workers. Audio is decoded and mixed on the main thread during export. For long projects this is acceptable; for very large projects consider chunking.
During live playback, AudioPlaybackController reads scene.audios and schedules Web Audio nodes beside the renderer on the same PlaybackEngine clock:
// AudioPlaybackController is wired by EditorProvider automatically.
// Control master output volume / mute via the playback store:
import { usePlaybackStore } from '@elah/editor'
const volume = usePlaybackStore((s) => s.volume) // 0..1
const setVolume = usePlaybackStore((s) => s.setVolume)
const muted = usePlaybackStore((s) => s.muted)
const toggleMute = usePlaybackStore((s) => s.toggleMute)
// To disable the audio pipeline entirely, pass enableAudio to Preview
// (default: true):
<Preview demuxerFactory={demuxerFactory} enableAudio={false} />Progress Tracking
interface ExportProgress {
frame: number // current frame being encoded
totalFrames: number // total frames in the project
}
// Derive a percentage yourself from frame / totalFrames.
const blob = await exportVideo(project, {
onProgress: (p) => {
const percent = Math.round((p.frame / Math.max(1, p.totalFrames)) * 100)
console.log(`${percent}% (${p.frame}/${p.totalFrames})`)
},
})