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)?
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>