Search code examples
javascriptmediarecorderscreen-recordingweb-mediarecordermediarecorder-api

How to screen record a rolling n-second window


I'm aware of the MediaRecorder API and how to record screen/audio/video, and then download those recordings. I'm also aware of npm modules such as react-media-recorder that leverage that API.

I would like to record a rolling n-second window of screen recording, to allow the user to create clips and then be able to share those clips. I cannot record the entire session as I don't know how long they will last, meaning I don't know how big the recordings might get (I assume there is a limit to what the recording can have in memory.)

Is there any easy way to use MediaRecorder to record a rolling window (i.e. to always have in memory the last 30 seconds recorded)?


Solution

  • I spent quite a while trying to make this work. Unfortunately, the only solution that works for me involves making 30 recorders.

    The naive solution to this problem is to call recorder.start(1000) to record data in one second intervals, then maintain a circular buffer on the dataavailable event. The issue with this is that MediaRecorder supports a very, very limited number of encodings. None of these encodings allow data packets to be dropped from the beginning, since they contain important metadata. With better understanding of the protocols, I'm sure that it is to some extent possible to make this strategy work. However, simply concatenating the packets together (when some are missing) does not create a valid file.

    Another attempt I made used two MediaRecorder objects at once. One of them would record second-long start packets, and the other would record regular data packets. When taking a clip, this then combined a start packet from the first recorder with the packets from the second. However, this usually resulted in corrupted recordings.

    This solution is not fantastic, but it does work: the idea is to keep 30 MediaRecorder objects, each offset by one second. For the sake of this demo, the clips are 5 seconds long, not 30:

    <canvas></canvas><button>Clip!</button>
    
    <style>
       canvas, video, button {
          display: block;
       }
    </style>
    
    <!-- draw to the canvas to create a stream for testing -->
    <script>
       const canvas = document.querySelector('canvas');
       const ctx = canvas.getContext('2d');
       // fill background with white
       ctx.fillStyle = 'white';
       ctx.fillRect(0, 0, canvas.width, canvas.height);
    
       // randomly draw stuff
       setInterval(() => {
          const x = Math.floor(Math.random() * canvas.width);
          const y = Math.floor(Math.random() * canvas.height);
          const radius = Math.floor(Math.random() * 30);
          ctx.beginPath();
          ctx.arc(x, y, radius, 0, Math.PI * 2);
          ctx.stroke();
       }, 100);
    </script>
    
    <!-- actual recording -->
    <script>
       // five second clips
       const LENGTH = 5;
    
       const codec = 'video/webm;codecs=vp8,opus'
       const stream = canvas.captureStream();
    
       // circular buffer of recorders
       let head = 0;
       const recorders = new Array(LENGTH)
          .fill()
          .map(() => new MediaRecorder(stream, { mimeType: codec }));
    
       // start them all
       recorders.forEach((recorder) => recorder.start());
    
       let data = undefined;
       recorders.forEach((r) => r.addEventListener('dataavailable', (e) => {
          data = e.data;
       }));
    
       setInterval(() => {
          recorders[head].stop();
          recorders[head].start();
    
          head = (head + 1) % LENGTH;
       }, 1000);
    
       // download the data
       const download = () => {
          if (data === undefined) return;
          const url = URL.createObjectURL(data);
    
          // download the url
          const a = document.createElement('a');
          a.download = 'test.webm';
          a.href = url;
          a.click();
          URL.revokeObjectURL(url);
       };
    
       // stackoverflow doesn't allow downloads
       // we show the clip instead
       const show = () => {
          if (data === undefined) return;
          const url = URL.createObjectURL(data);
    
          // display url in new video element
          const v = document.createElement('video');
          v.src = url;
          v.controls = true;
          document.body.appendChild(v);
       };
    
       document.querySelector('button').addEventListener('click', show);
    </script>