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:
<buttonid="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"><divid="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