Render to Video Tutorial
Build a composition and export it as MP4 video directly in the browser using WebCodecs.
Prerequisites
Browser must support WebCodecs API (Chrome 94+, Edge 94+, Safari 16.4+). Check support:
const supported = 'VideoEncoder' in window && 'VideoDecoder' in window;
Step 1: Create a Composition
Start with a basic composition that you want to render:
Live
Step 2: Add Export Button
Create UI for triggering the export:
<button id="exportBtn" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">Export Video</button><div id="progressContainer" class="mt-4 hidden"><div class="flex justify-between text-sm text-gray-300 mb-2"><span id="progressText">Preparing...</span><span id="progressPercent">0%</span></div><div class="w-full bg-gray-700 rounded-full h-3"><div id="progressBar" class="bg-blue-600 h-3 rounded-full transition-all" style="width: 0%"></div></div><div class="mt-2 text-xs text-gray-400"><span id="timeInfo"></span></div></div>
Step 3: Implement Basic Render
Call renderToVideo() on the timegroup element:
const timegroup = document.getElementById('myComposition');const exportBtn = document.getElementById('exportBtn');exportBtn.addEventListener('click', async () => {exportBtn.disabled = true;try {await timegroup.renderToVideo({fps: 30,codec: 'avc',filename: 'my-video.mp4'});alert('Video exported successfully!');} catch (error) {alert('Export failed: ' + error.message);} finally {exportBtn.disabled = false;}});
The video automatically downloads when rendering completes.
Step 4: Add Progress Tracking
Show real-time progress with the onProgress callback:
const progressContainer = document.getElementById('progressContainer');const progressBar = document.getElementById('progressBar');const progressText = document.getElementById('progressText');const progressPercent = document.getElementById('progressPercent');const timeInfo = document.getElementById('timeInfo');exportBtn.addEventListener('click', async () => {exportBtn.disabled = true;progressContainer.classList.remove('hidden');try {await timegroup.renderToVideo({fps: 30,codec: 'avc',filename: 'my-video.mp4',onProgress: (progress) => {// Update progress barconst percent = Math.round(progress.progress * 100);progressBar.style.width = `${percent}%`;progressPercent.textContent = `${percent}%`;// Update status textprogressText.textContent = `Rendering frame ${progress.currentFrame} of ${progress.totalFrames}`;// Show timing informationconst elapsed = Math.round(progress.elapsedMs / 1000);const remaining = Math.round(progress.estimatedRemainingMs / 1000);const speed = progress.speedMultiplier.toFixed(1);timeInfo.textContent = `Elapsed: ${elapsed}s | Remaining: ${remaining}s | Speed: ${speed}x`;}});progressText.textContent = 'Export complete!';progressPercent.textContent = '100%';} catch (error) {progressText.textContent = 'Export failed: ' + error.message;} finally {exportBtn.disabled = false;}});
Step 5: Add Codec Selection
Let users choose the codec based on browser support:
<select id="codecSelect" class="px-4 py-2 bg-gray-700 text-white rounded"><option value="avc">H.264 (Best Compatibility)</option><option value="hevc">H.265 (Better Compression)</option><option value="vp9">VP9 (Open Codec)</option><option value="av1">AV1 (Best Quality)</option></select>
const codecSelect = document.getElementById('codecSelect');// Check codec support on loadconst codecs = ['avc', 'hevc', 'vp9', 'av1'];codecs.forEach(async (codec) => {const config = {codec: codec === 'avc' ? 'avc1.42E01E' :codec === 'hevc' ? 'hvc1.1.6.L93.B0' :codec === 'vp9' ? 'vp09.00.10.08' : 'av01.0.05M.08',width: 1280,height: 720,bitrate: 5_000_000,framerate: 30};const supported = await VideoEncoder.isConfigSupported(config);if (!supported.supported) {const option = codecSelect.querySelector(`option[value="${codec}"]`);option.disabled = true;option.textContent += ' (Not Supported)';}});// Use selected codec when renderingexportBtn.addEventListener('click', async () => {const codec = codecSelect.value;// ... rest of render codeawait timegroup.renderToVideo({fps: 30,codec: codec,filename: 'my-video.mp4',onProgress: (progress) => { /* ... */ }});});
Step 6: Include Audio
Audio from ef-video and ef-audio elements is automatically mixed:
<ef-timegroup id="compositionWithAudio" mode="sequence" class="w-[1280px] h-[720px]"><ef-timegroup mode="fixed" duration="5s" class="absolute w-full h-full"><ef-video src="/assets/clip.mp4" class="size-full object-cover"></ef-video><ef-audio src="/assets/music.mp3" volume="0.3"></ef-audio></ef-timegroup></ef-timegroup>
await timegroup.renderToVideo({fps: 30,codec: 'avc',includeAudio: true, // Default: trueaudioBitrate: 192_000, // 192 kbps for high qualitypreferredAudioCodecs: ['opus', 'aac'], // Preference orderfilename: 'video-with-audio.mp4'});
Step 7: Add Cancel Support
Allow users to abort long renders:
<button id="cancelBtn" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 hidden">Cancel Render</button>
let abortController = null;const cancelBtn = document.getElementById('cancelBtn');exportBtn.addEventListener('click', async () => {exportBtn.disabled = true;cancelBtn.classList.remove('hidden');progressContainer.classList.remove('hidden');abortController = new AbortController();try {await timegroup.renderToVideo({fps: 30,codec: 'avc',filename: 'my-video.mp4',signal: abortController.signal,onProgress: (progress) => {// Update progress UIconst percent = Math.round(progress.progress * 100);progressBar.style.width = `${percent}%`;progressPercent.textContent = `${percent}%`;}});progressText.textContent = 'Export complete!';} catch (error) {if (error.name === 'RenderCancelledError') {progressText.textContent = 'Render cancelled by user';} else {progressText.textContent = 'Export failed: ' + error.message;}} finally {exportBtn.disabled = false;cancelBtn.classList.add('hidden');abortController = null;}});cancelBtn.addEventListener('click', () => {if (abortController) {abortController.abort();}});
Complete Example
Live
Preparing...
0%
Advanced Options
High-Resolution Export
Render at higher resolution than the composition:
await timegroup.renderToVideo({scale: 2, // 2x resolution (2560x1440 from 1280x720)bitrate: 10_000_000, // Increase bitrate for qualityfilename: 'high-res.mp4'});
Partial Export
Export only a portion of the composition:
await timegroup.renderToVideo({fromMs: 2000, // Start at 2 secondstoMs: 8000, // End at 8 secondsfilename: 'clip.mp4'});
Programmatic Access
Get the video as a buffer instead of downloading:
const videoBuffer = await timegroup.renderToVideo({returnBuffer: true,filename: 'video.mp4'});// Upload to serverconst formData = new FormData();formData.append('video', new Blob([videoBuffer], { type: 'video/mp4' }));await fetch('/api/upload', { method: 'POST', body: formData });
Next Steps
- See render-api.md for all options
- See render-strategies.md for choosing between browser, CLI, and cloud rendering
- See the
editframe-cliskill for server-side rendering