Rendering

Render a composition to an MP4 file directly in the browser.

Call renderToVideo on the root ef-timegroup element to encode the composition to MP4 entirely in the browser using WebCodecs — no server required.

Basic usage

js
const timegroup = document.getElementById("root");
const { buffer, mimeType } = await timegroup.renderToVideo({ fps: 30, videoCodec: "avc" });

const blob = new Blob([buffer], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement("a"), { href: url, download: "output.mp4" });
a.click();

buffer holds the finished MP4. It is omitted when you pass a custom mediabunny target, which receives the output instead.

Progress

Pass onProgress to receive an update after each encoded frame:

js
await timegroup.renderToVideo({
  fps: 30,
  onProgress: ({ frame, totalFrames, estimatedRemainingMs }) => {
    progressBar.style.width = `${(frame / totalFrames) * 100}%`;
    console.log(`frame ${frame}/${totalFrames}, ETA ${estimatedRemainingMs}ms`);
  },
});

Each update also carries time and duration in composition seconds, elapsedMs, and speedMultiplier (composition seconds rendered per wall-clock second). estimatedRemainingMs is based on the mean cost per frame so far.

Live frame preview

framePreviewCanvas is a thumbnail of the last captured frame, redrawn every progressPreviewInterval frames (15 by default). The same canvas is passed on every update, so attach it once:

js
await timegroup.renderToVideo({
  fps: 30,
  progressPreviewInterval: 15,
  onProgress: ({ frame, totalFrames, framePreviewCanvas }) => {
    progressBar.style.width = `${(frame / totalFrames) * 100}%`;
    if (framePreviewCanvas && !framePreviewCanvas.isConnected) {
      previewContainer.replaceChildren(framePreviewCanvas);
    }
  },
});

Set progressPreviewInterval: 0 to skip the preview.

Cancellation

Pass an AbortSignal to stop mid-render:

js
const controller = new AbortController();
cancelButton.onclick = () => controller.abort();

await timegroup.renderToVideo({
  fps: 30,
  signal: controller.signal,
  onProgress: ({ frame, totalFrames }) => {
    progressBar.style.width = `${(frame / totalFrames) * 100}%`;
  },
});

Options

React

Get a ref to the root timegroup element and call renderToVideo on it:

jsx
import { useRef } from "react";
import { TimelineRoot } from "@editframe/react";
import type { EFTimegroup } from "@editframe/elements";

function App() {
  const tgRef = useRef<EFTimegroup>(null);

  async function handleRender() {
    const { buffer, mimeType } = await tgRef.current!.renderToVideo({
      fps: 30,
      onProgress: ({ frame, totalFrames }) => console.log(`${frame}/${totalFrames}`),
    });
    // download new Blob([buffer], { type: mimeType }) …
  }

  return (
    <>
      <TimelineRoot id="root" component={MyComposition} />
      <button onClick={handleRender}>Render</button>
    </>
  );
}

TimelineRoot ensures createRenderClone() works correctly when renderToVideo forks the composition for parallel encoding.