guest271314 guest271314 - 2 months ago 28
Javascript Question

<video> playback of recorded stream using MediaRecorder() from <canvas> using canvas.captureStream() renders differently at firefox, chromium

Using a original

javascript
at
MediaRecorder-examples/record-canvas-to-video.js



Software requirements


  • Firefox 45. This is a Firefox technical demo. So it might not work on your browser, if it doesn't implement what we're demoing. At the
    time of writing (January 2016), you need to download either Firefox
    Developer Edition or Firefox Nightly.




window.onload = function () {
var video = document.getElementById('video');
var canvas = document.getElementById('canvas');
var width = canvas.width;
var height = canvas.height;
var capturing = false;

video.width = width;
video.height = height;

// We need the 2D context to individually manipulate pixel data
var ctx = canvas.getContext('2d');

// Start with a black background
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, width, height);

// Since we're continuously accessing and overwriting the pixels
// object, we'll request it once and reuse it across calls to draw()
// for best performance (we don't need to create ImageData objects
// on every frame)
var pixels = ctx.getImageData(0, 0, width, height);
var data = pixels.data;
var numPixels = data.length;

var stream = canvas.captureStream(15);
var recorder = new MediaRecorder(stream);

recorder.addEventListener('dataavailable', finishCapturing);

startCapturing();
recorder.start();

setTimeout(function() {
recorder.stop();
}, 2000);


function startCapturing() {
capturing = true;
draw();
}


function finishCapturing(e) {
capturing = false;
var videoData = [ e.data ];
var blob = new Blob(videoData, { 'type': 'video/webm' });
var videoURL = URL.createObjectURL(blob);
video.src = videoURL;
video.play();
}


function draw() {
// We don't want to render again if we're not capturing
if(capturing) {
requestAnimationFrame(draw);
}
drawWhiteNoise();
}


function drawWhiteNoise() {
var offset = 0;

for(var i = 0; i < numPixels; i++) {
var grey = Math.round(Math.random() * 255);

// The data array has pixel values in RGBA order
// (Red, Green, Blue and Alpha for transparency)
// We will make R, G and B have the same value ('grey'),
// then skip the Alpha value by increasing the offset,
// as we're happy with the opaque value we set when painting
// the background black at the beginning
data[offset++] = grey;
data[offset++] = grey;
data[offset++] = grey;
offset++; // skip the alpha component
}

// And tell the context to draw the updated pixels in the canvas
ctx.putImageData(pixels, 0, 0);
}

};


produces errors at chromium 55

Uncaught (in promise) DOMException: The play() request was interrupted by a new load request.




Failed to load resource: the server responded with a status of 416 (Requested Range Not Satisfiable)


though returns expected result at firefox 52.

Adjusting
javascript
for use at chromium by pushing
Blob
at
dataavailable
event of
MediaRecorder
to an array, then concatenating blobs at
stop
event

window.onload = function () {
var blobs = [];
var video = document.getElementById('video');
var canvas = document.getElementById('canvas');
var width = canvas.width;
var height = canvas.height;
var capturing = false;

video.width = width;
video.height = height;

// We need the 2D context to individually manipulate pixel data
var ctx = canvas.getContext('2d');

// Start with a black background
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, width, height);

// Since we're continuously accessing and overwriting the pixels
// object, we'll request it once and reuse it across calls to draw()
// for best performance (we don't need to create ImageData objects
// on every frame)
var pixels = ctx.getImageData(0, 0, width, height);
var data = pixels.data;
var numPixels = data.length;

var stream = canvas.captureStream(15);
var recorder = new MediaRecorder(stream);

recorder.addEventListener('dataavailable', finishCapturing);
recorder.addEventListener('stop', function(e) {
video.oncanplay = video.play;
video.src = URL.createObjectURL(new Blob(blobs, {type:"video/webm"}));
});
startCapturing();
recorder.start();

setTimeout(function() {
capturing = false;
recorder.stop();
}, 2000);


function startCapturing() {
capturing = true;
draw();
}


function finishCapturing(e) {
blobs.push(e.data);
}


function draw() {
// We don't want to render again if we're not capturing
if(capturing) {
requestAnimationFrame(draw);
}
drawWhiteNoise();
}


function drawWhiteNoise() {
var offset = 0;

for(var i = 0; i < numPixels; i++) {
var grey = Math.round(Math.random() * 255);

// The data array has pixel values in RGBA order
// (Red, Green, Blue and Alpha for transparency)
// We will make R, G and B have the same value ('grey'),
// then skip the Alpha value by increasing the offset,
// as we're happy with the opaque value we set when painting
// the background black at the beginning
data[offset++] = grey;
data[offset++] = grey;
data[offset++] = grey;
offset++; // skip the alpha component
}

// And tell the context to draw the updated pixels in the canvas
ctx.putImageData(pixels, 0, 0);
}

};


renders the recorded stream similarly to firefox.

However, the adjustments made to play video at both firefox and chromium render with apparent minimal, though noticeable delay between the concatenated blobs.

How can we render the same visual playback of
canvas.captureStream()
recorded using
MediaRecorder()
at
<video>
element?

plnkr http://plnkr.co/edit/KgGpkCJRvPG2T2Jy4wyH?p=preview

jib jib
Answer

You're driving the animation from the main JS thread here, so it's possible other main thread JS activities - like the ondataavailable callback firing - could disrupt timing enough to be noticeable.

Try omitting the (60) framerate from the canvas.captureStream() call.

MDN says: "If not set, a new frame will be captured each time the canvas changes; if set to 0, one single frame will be captured."

This should hopefully make the output more impervious to such interruptions, at the cost of shortening its length marginally.

You can also specify a timeslice with the start method, e.g. recorder.start(2000) to limit when the dataavailable event is fires to avoid interruptions.