# Video export

`renderVideo` turns the video on the page into an MP4 file, in the browser, with no server. It is not a screen recording: every frame is the real stage at the video's full resolution, set to its exact moment and handed to the browser's own H.264 encoder. Use it behind a download button.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/player/video-export/
>
> Two scenes, to keep the wait short.

## Usage

Ask the player's handle for a take, hand it to `renderVideo` with the size of the format, and save the blob that comes back. Always release the take, whether the render finished, failed or was stopped.

`download-video.tsx`

```tsx
import { type CSSProperties, useEffect, useRef, useState } from "react";

import { type SceneInstance, WRAPPED_FORMATS, type WrappedConfig, type WrappedSubject } from "@react-wrapped/core";
import { frameTimes, renderVideo, VIDEO_FPS, WrappedPlayer, type WrappedPlayerHandle } from "@react-wrapped/player";

const ROOM: CSSProperties = { display: "grid", height: 560 };

export function DownloadVideo({
  subject,
  seed,
  scenes,
  config,
  fileName,
}: {
  subject: WrappedSubject;
  seed: string;
  scenes: readonly SceneInstance[];
  config: WrappedConfig;
  /** The saved file's name, extension included. */
  fileName: string;
}) {
  const player = useRef<WrappedPlayerHandle>(null);
  const stop = useRef<AbortController | null>(null);
  const [percent, setPercent] = useState<number | null>(null);
  const [said, setSaid] = useState("No file made yet.");
  // Leaving the page stops a video that is still being made.
  useEffect(() => () => stop.current?.abort(), []);

  const download = async () => {
    // The stage and the playhead, on loan. The player stands still until `release` gives them back.
    const take = player.current?.take();
    if (!take) return;
    const controller = new AbortController();
    stop.current = controller;
    setPercent(0);
    setSaid(`Drawing ${frameTimes(take.seconds, VIDEO_FPS).length} frames, one by one.`);
    try {
      const video = await renderVideo(take, WRAPPED_FORMATS[config.format], {
        signal: controller.signal,
        // Called once a frame. Whole percents, so the page renders a hundred times and not six thousand.
        onProgress: (share) => setPercent(Math.floor(share * 100)),
      });
      const url = URL.createObjectURL(video);
      const link = document.createElement("a");
      link.href = url;
      link.download = fileName;
      link.click();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
      setSaid(`Saved ${(video.size / 1_000_000).toFixed(1)} MB as ${fileName}.`);
    } catch (error) {
      // Stopped on purpose is not a failure. Anything else comes with a sentence a person can read.
      if (controller.signal.aborted) setSaid("Stopped.");
      else setSaid(error instanceof Error ? error.message : "The video did not come through. Try again.");
    } finally {
      take.release();
      stop.current = null;
      setPercent(null);
    }
  };

  return (
    <div>
      {/* While the stage is being filmed nothing else may move it. */}
      <div style={ROOM} inert={percent !== null}>
        <WrappedPlayer ref={player} subject={subject} seed={seed} scenes={scenes} config={config} />
      </div>
      <div role="group" aria-label="Video file">
        {percent === null ? (
          <button type="button" onClick={() => void download()}>
            Download the MP4
          </button>
        ) : (
          <button type="button" onClick={() => stop.current?.abort()}>
            Stop at {percent}%
          </button>
        )}
      </div>
      <output>{said}</output>
    </div>
  );
}
```

## How take() lends the stage

`take()` on the [player's handle](https://bettertyped.github.io/react-wrapped/docs/player/wrapped-player/) pauses the video and hands over the stage and the playhead. While the take is out the render owns the frame: it calls `show(seconds)` for each frame's moment and draws what is there. `release()` puts the playhead back where it was and plays on if the video was playing.

| Key | Type | What it does |
| --- | --- | --- |
| `stage` | `HTMLElement` | The stage element itself, at the video's real resolution. |
| `seconds` | `number` | How long the video plays at the speed it is set to. |
| `show` | `(seconds: number) => void` | Puts that moment of the video on the stage. |
| `intact` | `() => boolean` | False once the video was rebuilt under the render: a setting changed, a scene joined. |
| `release` | `() => void` | Gives the stage back. The player returns to where it was and plays on if it was playing. |

> Keep the viewer's hands off the video while it is filmed. The example sets `inert` on the player, and `WrappedView` does the same for its settings. If a setting does change, `intact()` turns false and `renderVideo` stops with an error that says to try again.

## renderVideo

```ts
function renderVideo(
  take: StageTake,
  size: { width: number; height: number },
  options: { onProgress: (share: number) => void; signal: AbortSignal },
): Promise<Blob>;
```

| Argument | What it is |
| --- | --- |
| `take` | What `player.take()` returned. |
| `size` | The frame in pixels. `WRAPPED_FORMATS[config.format]` has the right numbers: 1080 by 1920, 1080 by 1080 or 1920 by 1080. |
| `onProgress` | Called after every frame with the share done, from 0 to 1. |
| `signal` | An `AbortSignal`. Aborting it stops the render and rejects the promise. |

## Frame by frame

- `VIDEO_FPS` is 60. `frameTimes(seconds, fps)` lists the moment of every frame, and the last one is the very end of the video, so the file stops on the finished last scene.
- A frame is the stage's own markup drawn onto a canvas. The style rules and fonts it needs are collected once and travel with every frame.
- The file is H.264 in an MP4, about 12 megabits a second at 1080p, with the hardware encoder when the machine has one.
- Speed counts: a video set to 1.5x makes a shorter file.
- The result does not depend on how big the player is on the page, how fast the machine is, or whether the tab is in front.

## When it cannot be made

`renderVideo` rejects with a sentence you can show as it is. A browser without an H.264 encoder gets "This browser cannot make the video. Chrome, Edge and Safari can." A video that changed under the render gets "The video changed while it was being made. Try again." An aborted render rejects too: check `signal.aborted` before treating it as a failure.

## Questions

### Does the export need a server?

No. Every frame is drawn and encoded in the browser, and the file is a blob the page saves itself. Nothing is uploaded.

### Why is it slower than real time on some machines?

Because it is not a recording. A 40 second video is 2,400 frames, each drawn at full size. A fast machine makes the file quicker than the video plays and a slow one takes longer, but the file is the same.

## Related

- [WrappedPlayer](https://bettertyped.github.io/react-wrapped/docs/player/wrapped-player/)
- [Formats](https://bettertyped.github.io/react-wrapped/docs/formats/)
- [Graphics](https://bettertyped.github.io/react-wrapped/docs/graphics/)
- [Player](https://bettertyped.github.io/react-wrapped/docs/player/)
- [Build the page from parts](https://bettertyped.github.io/react-wrapped/docs/guides/from-parts/)

Source: https://bettertyped.github.io/react-wrapped/docs/player/video-export/
