<video> 使用 <canvas> 使用 canvas.captureStream() 使用 MediaRecorder() 播放录制的流在 firefox、chromium 上呈现不同

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

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

使用原版 javascript

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);
  }

};

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

尽管 returns Firefox 52 的预期结果。

通过将 MediaRecorderdataavailable 事件中的 Blob 推送到一个数组,然后在 stop 事件中连接 blob,从而调整 javascript 在 chromium 中的使用

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);
  }

};

以类似于 firefox 的方式呈现录制的流。

但是,为在 firefox 和 chromium 上播放视频所做的调整在串联的 blob 之间具有明显最小但明显的延迟。

我们如何渲染在 <video> 元素处使用 MediaRecorder() 录制的 canvas.captureStream() 的相同视觉回放?

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

您在此处从主 JS 线程驱动动画,因此其他主线程 JS 活动(例如 ondataavailable 回调触发)可能会扰乱时序以引起注意。

尝试从 canvas.captureStream() 调用中省略 (60) 帧率。

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

这应该有希望使输出更不受此类中断的影响,但代价是略微缩短了它的长度。

您还可以使用 start 方法指定 timeslice,例如recorder.start(2000) 限制 dataavailable 事件何时触发以避免中断。