Render API

Methods

renderToVideo(options?)

Export timegroup to MP4 video with WebCodecs

Returns: Promise<Uint8Array | undefined>
createRenderClone()

Create off-DOM clone for rendering without affecting preview

Returns: Promise<RenderCloneResult>
getRenderData<T>()

Access custom data passed from CLI at render time

Returns: T | undefined

Export compositions to MP4 video using browser-based WebCodecs or server-side rendering via CLI.

Browser Export with renderToVideo()

Export videos directly in the browser using the WebCodecs API. Best for client-side applications and interactive exports.

Basic Usage

Live
Hello Video!

RenderToVideoOptions

All options for renderToVideo():

OptionTypeDefaultDescription
fpsnumber30Frame rate (frames per second)
codecstring"avc"Video codec: "avc", "hevc", "vp9", "av1", "vp8"
bitratenumber5_000_000Video bitrate in bits per second
filenamestring"video.mp4"Download filename
scalenumber1Rendering scale multiplier (2 = 2x resolution)
keyFrameIntervalnumber150Frames between keyframes
fromMsnumber0Start time in milliseconds
toMsnumberdurationEnd time in milliseconds
onProgressfunction-Progress callback (see below)
streamingbooleanfalseStream output using File System Access API
signalAbortSignal-AbortSignal to cancel render
includeAudiobooleantrueInclude audio tracks in output
audioBitratenumber128_000Audio bitrate in bits per second
contentReadyModestring"immediate""immediate" or "blocking" for video readiness
blockingTimeoutMsnumber5000Timeout for blocking video loads
returnBufferbooleanfalseReturn Uint8Array instead of downloading
preferredAudioCodecsarray["opus", "aac"]Preferred audio codecs in order
benchmarkModebooleanfalseSkip encoding for performance testing
customWritableStreamWritableStream-Custom output stream for programmatic control
progressPreviewIntervalnumber10Frames between preview updates
canvasModestring"foreignObject""native" or "foreignObject" rendering

Progress Callback

The onProgress callback receives a RenderProgress object with detailed information:

interface RenderProgress {
progress: number; // 0.0 to 1.0
currentFrame: number; // Current frame index
totalFrames: number; // Total frames to render
renderedMs: number; // Milliseconds rendered so far
totalDurationMs: number; // Total video duration
elapsedMs: number; // Real time elapsed
estimatedRemainingMs: number; // Estimated time remaining
speedMultiplier: number; // Render speed (2.0 = 2x real-time)
framePreviewCanvas?: HTMLCanvasElement; // Preview of current frame
}

Codec Support Matrix

Browser support varies by codec. Check availability before rendering:

CodecChromeSafariFirefoxNotes
avc (H.264)Best compatibility, widely supported
hevc (H.265)⚠️macOS/iOS only, better compression
vp9Open codec, good compression
av1⚠️Modern, best compression, slower encoding
vp8Legacy WebM codec

Recommendation: Use avc for maximum compatibility or av1 for best quality/size.

Audio Inclusion

Audio from ef-video and ef-audio elements is automatically mixed and included:

await timegroup.renderToVideo({
includeAudio: true, // Include audio tracks
audioBitrate: 192_000, // Higher quality audio (192 kbps)
preferredAudioCodecs: ['opus', 'aac'] // Codec preference order
});

Audio codecs available: opus (best quality), aac (most compatible), mp3 (legacy).

Streaming Output

Stream large videos to disk without loading entire file into memory:

await timegroup.renderToVideo({
streaming: true, // Uses File System Access API
filename: 'large-video.mp4' // User picks save location
});

Requires browser support for File System Access API (Chrome 86+, Edge 86+).

Aborting Renders

Cancel long-running exports with AbortController:

const controller = new AbortController();
// Start render
const renderPromise = timegroup.renderToVideo({
signal: controller.signal,
onProgress: (progress) => {
console.log(`${Math.round(progress.progress * 100)}%`);
}
});
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
await renderPromise;
} catch (error) {
if (error.name === 'RenderCancelledError') {
console.log('Render was cancelled');
}
}

Partial Exports

Export specific time ranges without modifying the composition:

// Export only seconds 5-15
await timegroup.renderToVideo({
fromMs: 5000,
toMs: 15000,
filename: 'clip.mp4'
});

High-Resolution Export

Render at higher resolutions using the scale option:

// Render at 2x resolution (1440x960 from 720x480 composition)
await timegroup.renderToVideo({
scale: 2,
bitrate: 10_000_000 // Increase bitrate for higher resolution
});

Programmatic Buffer Access

Get video data as Uint8Array instead of downloading:

const videoBuffer = await timegroup.renderToVideo({
returnBuffer: true,
filename: 'video.mp4'
});
// Upload to server
const formData = new FormData();
formData.append('video', new Blob([videoBuffer], { type: 'video/mp4' }));
await fetch('/api/upload', { method: 'POST', body: formData });

Off-DOM Rendering with createRenderClone()

Create an independent clone of a timegroup for off-screen rendering. This enables rendering without affecting the user's preview position and allows concurrent renders.

Why Use Render Clones?

  • Non-disruptive: Render in background without affecting preview playback
  • Concurrent: Run multiple renders simultaneously with different clones
  • Isolated state: Each clone has independent time position and state
  • JavaScript re-execution: Initializer functions run on each clone

Basic Usage

// Create a render clone
const { clone, container, cleanup } = await timegroup.createRenderClone();
try {
// Clone is fully functional and independent
await clone.seekForRender(5000); // Seek to 5 seconds
// Render single frame to canvas
const canvas = await renderToImageNative(clone, 1920, 1080);
// Use canvas data...
const dataUrl = canvas.toDataURL('image/png');
} finally {
// Always clean up when done
cleanup();
}

Automatic Clone Management

renderToVideo() automatically manages clones internally. You typically don't need to use createRenderClone() directly unless you're building custom rendering logic.

// This internally creates and manages a render clone
await timegroup.renderToVideo({ fps: 30 });

Clone Factory Pattern

For compositions with JavaScript behavior, provide an initializer function that runs on both the prime timeline and all clones:

<ef-timegroup id="myComp" mode="sequence"></ef-timegroup>
<script type="module">
const timegroup = document.getElementById('myComp');
// This function runs on the original AND on all render clones
timegroup.initializer = (tg) => {
// Set up reactive state, register callbacks, etc.
tg.addEventListener('frame-task', (e) => {
// Update canvas, modify DOM, etc.
console.log('Frame:', e.detail.currentTimeMs);
});
};
</script>

The initializer:

  • Must be synchronous (no async/await, no Promise return)
  • Must complete quickly (<10ms warning, <100ms error)
  • Runs once per instance (original + each clone)
  • Enables JavaScript-driven animations in renders

CLI Rendering

For server-side rendering, use the Editframe CLI. See the editframe-cli skill for full documentation.

Quick Render

npx editframe render -o output.mp4

Custom Render Data

Pass dynamic data into compositions at render time:

npx editframe render --data '{"userName":"John","theme":"dark"}' -o video.mp4

Read the data in your composition with getRenderData():

import { getRenderData } from "@editframe/elements";
interface MyRenderData {
userName: string;
theme: "light" | "dark";
}
const data = getRenderData<MyRenderData>();
if (data) {
console.log(data.userName); // "John"
console.log(data.theme); // "dark"
}

When to Use CLI vs Browser

Use CLI rendering when:

  • Running on a server or CI/CD pipeline
  • Need consistent encoding across platforms
  • Processing videos in batch
  • Require specific encoder settings not available in browsers

Use browser rendering when:

  • Building interactive client-side applications
  • Want instant preview and export without server
  • Need real-time progress feedback
  • Exporting user-generated content

Advanced: Custom Writable Streams

For fine-grained control over output, provide a custom WritableStream:

class VideoUploadStream extends WritableStream<Uint8Array> {
constructor() {
super({
async write(chunk) {
// Stream chunks directly to server
await fetch('/api/upload/chunk', {
method: 'POST',
body: chunk
});
},
async close() {
// Finalize upload
await fetch('/api/upload/complete', { method: 'POST' });
}
});
}
}
await timegroup.renderToVideo({
customWritableStream: new VideoUploadStream(),
returnBuffer: false
});

Performance Tips

  1. Use appropriate codecs: avc encodes fastest, av1 encodes slowest but smallest
  2. Reduce resolution: Lower resolution renders much faster
  3. Limit audio bitrate: High audio bitrates don't improve quality much
  4. Use contentReadyMode: "immediate": Skip waiting for videos to fully load
  5. Disable progress previews: Set high progressPreviewInterval or omit callback
  6. Test codec support: Not all codecs are hardware-accelerated on all devices

Browser Requirements

  • WebCodecs API (Chrome 94+, Edge 94+, Safari 16.4+)
  • File System Access API for streaming (Chrome 86+, Edge 86+)
  • Requires HTTPS or localhost (secure context)

Check support:

const hasWebCodecs = 'VideoEncoder' in window && 'VideoDecoder' in window;
const hasFileSystemAccess = 'showSaveFilePicker' in window;

Error Handling

try {
await timegroup.renderToVideo({ fps: 60, codec: 'av1' });
} catch (error) {
if (error.name === 'RenderCancelledError') {
console.log('User cancelled export');
} else if (error.name === 'NoSupportedAudioCodecError') {
console.log('No compatible audio codec available');
// Retry without audio
await timegroup.renderToVideo({ includeAudio: false });
} else {
console.error('Render failed:', error);
}
}

Examples

Export with Custom Settings

await timegroup.renderToVideo({
fps: 60, // Smooth 60fps
codec: 'avc', // H.264 for compatibility
bitrate: 8_000_000, // 8 Mbps
scale: 1.5, // 1.5x resolution
includeAudio: true,
audioBitrate: 256_000, // High quality audio
filename: 'high-quality.mp4'
});

Progress Bar with Time Estimates

await timegroup.renderToVideo({
onProgress: ({ progress, elapsedMs, estimatedRemainingMs, speedMultiplier }) => {
const percent = Math.round(progress * 100);
const elapsed = Math.round(elapsedMs / 1000);
const remaining = Math.round(estimatedRemainingMs / 1000);
console.log(
`${percent}% complete | ` +
`Elapsed: ${elapsed}s | ` +
`Remaining: ${remaining}s | ` +
`Speed: ${speedMultiplier.toFixed(1)}x`
);
}
});

Export Multiple Clips

const clips = [
{ fromMs: 0, toMs: 5000, filename: 'intro.mp4' },
{ fromMs: 5000, toMs: 15000, filename: 'main.mp4' },
{ fromMs: 15000, toMs: 20000, filename: 'outro.mp4' },
];
for (const clip of clips) {
await timegroup.renderToVideo(clip);
}