Skip to content

How can I properly record a MediaStream?

The situation

I need to do the following:

  • Get the video from a <video> and play inside a <canvas>

  • Record the stream from the canvas as a Blob

That’s it. The first part is okay.

For the second part, I managed to record a Blob. The problem is that the Blob is empty.

The view

<video id="video" controls="true" src="http://upload.wikimedia.org/wikipedia/commons/7/79/Big_Buck_Bunny_small.ogv"></video>
<canvas id="myCanvas" width="532" height="300"></canvas>

The code

// Init
console.log(MediaRecorder.isTypeSupported('video/webm')) // true
const canvas = document.querySelector("canvas")
const ctx = canvas.getContext("2d")
const video = document.querySelector("video")

// Start the video in the player
video.play()

// On play event - draw the video in the canvas
video.addEventListener('play', () => {
  function step() {
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
    requestAnimationFrame(step)
  }
  requestAnimationFrame(step);

  // Init stream and recorder
  const stream = canvas.captureStream()
  const recorder = new MediaRecorder(stream, {
    mimeType: 'video/webm',
  });

  // Get the blob data when is available
  let allChunks = [];
  recorder.ondataavailable = function(e) {
    console.log({e}) // img1
    allChunks.push(e.data);
  }

  // Start to record
  recorder.start()

  // Stop the recorder after 5s and check the result
  setTimeout(() => {
    recorder.stop()
    const fullBlob = new Blob(allChunks, { 'type' : 'video/webm' });
    const downloadUrl = window.URL.createObjectURL(fullBlob)
    console.log({fullBlob}) // img2
  }, 5000);
})

The result

This the console.log of the ondataavailable event:

Enter image description here

This is the console.log of the Blob:

Enter image description here

The fiddle

Here is the JSFiddle. You can check the results in the console:

https://jsfiddle.net/1b7v2pen/

Browsers behavior

This behavior (Blob data size: 0) it happens on Chrome and Opera.
On Firefox it behaves slightly different. It records a very small video Blob (725 bytes). The video length is 5 seconds as it should be, but it’s just a black screen.

The question

What is the proper way to the record a stream from a canvas?
Is there something wrong in the code?

Why did the Blob come out empty?

Answer

MediaRecorder.stop() is kind of an asynchronous method.

In the stop algorithm, there is a call to requestData, which itself will queue a task to fire an event dataavailable with the currently available data since the last such event.

This means that synchronously after you called MediaRecorder#stop() the last data grabbed will not be part of your allChunks Array yet. They will become not so long after (normally in the same event loop).

So, when you are about to save recordings made from a MediaRecorder, be sure to always build the final Blob from the MediaRecorder’s onstop event, which will signal that the MediaRecorder is actually ended, did fire its last dataavailable event, and that everything is all good.

And one thing I missed at first, is that you are requesting a cross-domain video. Doing so, without the correct cross-origin request, will make your canvas (and MediaElement) tainted, so your MediaStream will be muted.

Since the video you are trying to request is from wikimedia, you can simply request it as a cross-origin resource, but for other resources, you’ll have to be sure the server is configured to allow these requests.

const canvas = document.querySelector("canvas")
const ctx = canvas.getContext("2d")
const video = document.querySelector("video")

// Start the video in the player
video.play()

// On play event - draw the video in the canvas
video.addEventListener('play', () => {
  function step() {
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
    requestAnimationFrame(step)
  }
  requestAnimationFrame(step);
  
  // Init stream and recorder
  const stream = canvas.captureStream()
  const recorder = new MediaRecorder(stream, {
    mimeType: 'video/webm',
  });

  // Get the blob data when is available
  let allChunks = [];
  recorder.ondataavailable = function(e) {
    allChunks.push(e.data);
  }
  recorder.onstop = (e) => {
    const fullBlob = new Blob(allChunks, { 'type' : 'video/webm' });
    const downloadUrl = window.URL.createObjectURL(fullBlob)
    console.log({fullBlob})
    console.log({downloadUrl})
  }

  // Start to record
  recorder.start()

  // Stop the recorder after 5s and check the result
  setTimeout(() => {
    recorder.stop()
  }, 5000);
})
<!--add the 'crossorigin' attribute to your video -->
<video id="video" controls="true" src="https://upload.wikimedia.org/wikipedia/commons/7/79/Big_Buck_Bunny_small.ogv" crossorigin="anonymous"></video>
<canvas id="myCanvas" width="532" height="300"></canvas>

Also, I can’t refrain to note that if you don’t do any special drawings from your canvas, you might want to save the video source directly, or at least, record the <video>’s captureStream MediaStream directly.