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.

Audio and text

Export uses the same source trims and playbackRate as preview, including pitch-preserving audio speed changes. A media element's volumeAtTime callback is evaluated for each output audio sample; see volume and fades.

Text capture retains computed styles, including default values such as normal. Relative @font-face URLs resolve against the stylesheet that declares them. Use CSS line-height to choose your title spacing and ensure the browser can load the font files.

Export after development hot reload

If a hot reload replaces media classes while their custom elements remain registered, export stops with “Reload the page to export”. Reload the page, then retry. This prevents an export with missing audio from stale development elements.

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

Use TimelineRoot to register a React composition for render cloning, then call renderToVideo on its live root timegroup:

tsx
import { useRef } from "react";
import { TimelineRoot, Timegroup, Text } from "@editframe/react";
import type { EFTimegroupElement } from "@editframe/elements";

function MyComposition({ id }: { id: string }) {
  return (
    <Timegroup id={id} duration="3s" style={{ width: 1920, height: 1080 }}>
      <Text>Hello from React</Text>
    </Timegroup>
  );
}

function App() {
  const hostRef = useRef<HTMLDivElement>(null);

  async function handleRender() {
    const timegroup = hostRef.current?.querySelector<EFTimegroupElement>("ef-timegroup");
    if (!timegroup) return;
    const { buffer, mimeType } = await timegroup.renderToVideo({
      fps: 30,
      onProgress: ({ frame, totalFrames }) => console.log(`${frame}/${totalFrames}`),
    });
    // download new Blob([buffer], { type: mimeType }) …
  }

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

TimelineRoot remounts the composition when export creates its isolated render clone, preserving hooks, state setup, and function props. Forward its supplied id to the root Timegroup. Keep animation and fade functions deterministic and driven by composition time.