Skip to content
Logo Theodo

Saving Canvas Animations With MediaRecorder

Lawrence Mullen6 min read

Cover-Photo

The HTML canvas is a simple and useful tool in generative art and animation. I hadn’t had much experience with it, but I recently finished a project that required us to pick it up and I’ve since been blown away by the complex and versatile images people can create with it. I also uncovered a newfound appreciation for the mathematical principals behind those psychedelic edm concert visuals.

One shortcoming of canvas animations, however, is that they exist solely in the browser environment. This makes it difficult to playback or use the animations in other media applications. For our project specifically, we had to save a dynamically generated animation and reformat it so that it could be shared on media platforms, like Instagram. There wasn’t a lot of documentation on how to do this, so I thought it would be helpful to share what I’ve learned. Thankfully the MediaStream Recording API is perfect for this and FFmpeg.wasm allows you to do all kinds of things with media encoding all inside the browser.

For our latest project, I had to record and save an HTML5 canvas animation. Something entirely new to me. Thankfully with the [MediaStream Recording API] (https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder) and ffmpeg.wasm we can record end export the animation in a standardized format.

Demo

https://codepen.io/lawrencem/pen/RwJjQOX

After 10 seconds a download button will appear.

Downloading a file inside an iframe may be disabled in your browser. You may have to open the sandbox to fully view the demo.

Animation was made from a tutorial from here. Definitely check out other videos from the author, there are a lot of cool tutorials.

Recording the Canvas Animation

To save and download the HTML Canvas animation you’ll need:

This is where we define our canvas.

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Animation Sandbox</title>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="src/styles.css" />
  </head>
  <body>
    <div>
      <canvas id="canvas"></canvas>
      <script src="src/index.js"></script>
    </div>
  </body>
</html>

index.js

const startRecording = () => {
  const canvas = document.querySelector("#canvas");
  const data = []; // here we will store our recorded media chunks (Blobs)
  const stream = canvas.captureStream(30); // records the canvas in real time at our preferred framerate 30 in this case.
  const mediaRecorder = new MediaRecorder(stream); // init the recorder
  // whenever the recorder has new data, we will store it in our array
  mediaRecorder.ondataavailable = (e) => data.push(e.data);
  // only when the recorder stops, we construct a complete Blob from all the chunks
  mediaRecorder.onstop = (e) =>
    downloadVideo(new Blob(data, { type: "video/webm;codecs=h264" }));
  mediaRecorder.start();
  setTimeout(() => {
    mediaRecorder.stop();
  }, 6000); // stop recording in 6s
};

const downloadVideo = async (blob) => {
  const div = document.querySelector("div");
  var url = URL.createObjectURL(blob);
  var a = document.createElement("a");
  a.href = url;
  a.download = "test.webm";
  a.className = "button";
  a.innerText = "click here to download";
  div.appendChild(a);
};

// animation code...

startRecording();
animate();

When startRecording is called we grab our canvas as defined in index.html using document.querySelector. We gave our canvas the id #canvas so it’s easy to select.

const canvas = document.querySelector("#canvas");

We then begin to stream the canvas in real-time using at a rate of 30 frames per second determined by the frame rate we pass to canvas.captureStream(). After instantiating a new Media Recorder object we can record the stream into chunks of data which can then be exported as a Blob object.

Media Recorder allows for some options such as setting the containers MIME type (for example: “video/webm”) as well as the ability to record audio.

Our recording length is determined by the delay in our setTimeout function which in this instance we’ve set to be 10 seconds.

setTimeout(() => {
  rec.stop();
}, 10); // stop recording in 10s

After 10 seconds we call rec.stop() which fires our stop event. Here is where we have our video blob.

rec.onstop = (e) =>
  downloadVideo(new Blob(chunks, { type: "video/webm;codecs=h264" }));

Now that we have our blob what can we do with this?

We can download our recording automatically as a webm file.

Using javascript we can easily have our recording download as a WebM file: a royalty-free, media file format designed for the web.

WebM plays on almost all major browsers.

const exportVid(blob) {
const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.style = "display: none";
  a.href = url;
  a.download = "recording.webm";
  document.body.appendChild(a);
  a.click();
}

What if we need to convert our video to mp4?

MediaRecorder isn’t able to handle recording to mp4 in chrome or safari (as of 2022), but ffmpeg.wasm is an option that allows you to convert webm to mp4 inside of the browser.

In the onstop of our startRecording function we can pass our blob to this exportVid function which will convert our blob to mp4 before downloading it.

import { createFFmpeg, fetchFile } from "@ffmpeg/ffmpeg";

const ffmpeg = createFFmpeg({
  corePath: "https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js",
  log: true,
});

const exportVid = async (blob) => {
  if (!ffmpeg.isLoaded()) {
    await ffmpeg.load();
    ffmpeg.FS("writeFile", "test.avi", await fetchFile(blob));
    await ffmpeg.run("-i", "test.avi", "test.mp4");
    const data = ffmpeg.FS("readFile", "test.mp4");
    const url = URL.createObjectURL(
      new Blob([data.buffer], { type: "video/mp4" })
    );
    const a = document.createElement("a");
    a.style = "display: none";
    a.href = url;
    a.download = "recording.webm";
    document.body.appendChild(a);
    a.click();
  }
};

How to share your canvas recording

Most devices and browsers can natively share links and files through the navigator.share() method of the Web Share API

A basic implementation of this in react would be to:

  1. In the exportVid function of the previous snippet instead of having the file download set a state variable to the url output of the blob.
			const exportVid = async (blob) => {
			...
			await ffmpeg.run("-i", "test.avi","test.mp4");
      const data = ffmpeg.FS("readFile", "test.mp4");

      setVideoSrc(URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" })))

  1. Create a button that calls onShare: <button onClick={onShare}>share!</button>

  2. Use the web share api to share the file.

    const onShare = async () => {
      if (!videoSrc) return;
      const blob = await fetch(videoSrc).then((response) => response.blob());
      const file = new File([blob], "test.mp4", { type: "video/mp4" });
      const filesArray = [file];
      if (navigator.canShare && navigator.canShare({ files: filesArray })) {
        await navigator.share({
          files: filesArray,
        });
      }
    };
    

Glossary:

Liked this article?