Canvas 使用 captureStream 和 mediaRecorder 进行录制

Canvas recording using captureStream and mediaRecorder

如何录制来自多个 canvas 的流? 即,当我将一个 canvas 更改为另一个时,它必须记录活动 canvas 继续到第一个。

我这样做过:

stream = canvas.captureStream();
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.ondataavailable = handleDataAvailable;
mediaRecorder.start(10);

function handleDataAvailable(event) {
  recordedBlobs.push(event.data);
}

但是当添加另一个流时,只记录了第一部分。我正在将记录的数据推送到全局数组。

在当前的实现中,您无法切换 MediaRecorder 流的录制轨道

当您尝试这样做时,Firefox 会将您带到

的控制台

MediaRecorder does not support recording multiple tracks of the same type at this time.

而Chrome保持沉默并记录黑帧而不是第二轨...

var canvases = Array.prototype.slice.call(document.querySelectorAll('canvas')),
  recordingStream,
  current = 0,
  chunks = [],
  recorder,
  switchInterval;


function startRecording() {

  // first gather both canvases streams & extract the videoTracks
  let streams = canvases.map((c) => {
    return c.captureStream(30)
  });
  let tracks = streams.map((s) => {
    return s.getVideoTracks()[0]
  });
  // create a new MediaStream with both tracks in it
  // we don't use addTrack because of https://bugzilla.mozilla.org/show_bug.cgi?id=1296531
  recordingStream = 'MediaStream' in window && new MediaStream(tracks) || new webkitMediaStream(tracks);

  // init the MediaRecorder
  recorder = new MediaRecorder(recordingStream);
  recorder.ondataavailable = saveChunks;
  recorder.onstop = exportVideo;
  recorder.onerror = (e) => {
    console.log(e.name)
  };
  recorder.start();

  stopRec.disabled = false;
  // switch the canvas to be recorder every 200ms
  switchInterval = setInterval(switchStream, 200);

}


// switch mute one of the tracks, then the other
function switchStream() {
  current = +!current;
  var tracks = recordingStream.getVideoTracks();
  tracks[current].enabled = true;
  // commented because it seems FF doesn't support canvasTrack's method yet
  // doesn't work in chrome even when there anyway
  //  tracks[current].requestFrame(); 
  tracks[+!current].enabled = false;
}

function saveChunks(evt) {
  // store our video's chunks
  if (evt.data.size > 0) {
    chunks.push(evt.data);
  }

}

stopRec.onclick = function stopRecording() {
  if (recorder.state !== 'recording') {
    this.disabled = true;
    return;
  }
  // stop everything
  recorder.stop(); // this will trigger exportVideo
  clearInterval(switchInterval);
  stopCanvasAnim();
  a.style.display = b.style.display = 'none';
  this.parentNode.innerHTML = "";
}


function exportVideo() {
  //  we've got everything
  vid.src = URL.createObjectURL(new Blob(chunks));
}



var stopCanvasAnim = (function initCanvasDrawing() {
  // some fancy drawings

  var aCtx = canvases[0].getContext('2d'),
    bCtx = canvases[1].getContext('2d');

  var objects = [],
    w = canvases[0].width,
    h = canvases[0].height;
  aCtx.fillStyle = bCtx.fillStyle = 'ivory';

  for (var i = 0; i < 100; i++) {
    objects.push({
      angle: Math.random() * 360,
      x: 100 + (Math.random() * w / 2),
      y: 100 + (Math.random() * h / 2),
      radius: 10 + (Math.random() * 40),
      speed: 1 + Math.random() * 20
    });
  }
  var stop = false;
  var draw = function() {

    aCtx.fillRect(0, 0, w, h);
    bCtx.fillRect(0, 0, w, h);
    for (var n = 0; n < 100; n++) {
      var entity = objects[n],
        velY = Math.cos(entity.angle * Math.PI / 180) * entity.speed,
        velX = Math.sin(entity.angle * Math.PI / 180) * entity.speed;

      entity.x += velX;
      entity.y -= velY;

      aCtx.drawImage(imgA, entity.x, entity.y, entity.radius, entity.radius);
      bCtx.drawImage(imgB, entity.x, entity.y, entity.radius, entity.radius);

      entity.angle++;
    }
    if (!stop) {
      requestAnimationFrame(draw);
    }
  }


  var imgA = new Image();
  var imgB = new Image();
  imgA.onload = function() {
    draw();
    startRecording();
  };
  imgA.crossOrigin = imgB.crossOrigin = 'anonymous';
  imgA.src = "https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png";
  imgB.src = "https://dl.dropboxusercontent.com/s/rumlhyme6s5f8pt/ABC.png";

  return function() {
    stop = true;
  };
})();
<p>
  <button id="stopRec" disabled>stop recording</button>
</p>
<canvas id="a"></canvas>
<canvas id="b"></canvas>
<video id="vid" controls></video>

请注意,目前有一个关于此的 open issue on the w3c github project mediacapture-record


但是,这个问题有一个简单的解决方法:

  • 使用另一个 offscreen [hidden]* offscreen (chrome bug 已在最新的 58 canary 中修复) canvas,仅用于记录器,
  • 在上面画出想要的canvas的边框

这样,没问题;-)
同样的解决方法也可以用于在同一个 MediaRecorder 上保存不同的视频。

var canvases = document.querySelectorAll('canvas'),
  recordingCtx,
  current = 0,
  chunks = [],
  recorder,
  switchInterval;

// draw one of our canvas on a third one
function recordingAnim() {
  recordingCtx.drawImage(canvases[current], 0, 0);
  // if recorder is stopped, stop the animation
  if (!recorder || recorder.state === 'recording') {
    requestAnimationFrame(recordingAnim);
  }
}

function startRecording() {

  var recordingCanvas = canvases[0].cloneNode();
  recordingCtx = recordingCanvas.getContext('2d');
  recordingCanvas.id = "";
  // chrome forces us to display the canvas in doc so it can be recorded,
  // This bug has been fixed in chrome 58.0.3014.0
  recordingCtx.canvas.style.height = 0;
  document.body.appendChild(recordingCtx.canvas);

  // draw one of the canvases on our recording one
  recordingAnim();

  // init the MediaRecorder
  recorder = new MediaRecorder(recordingCtx.canvas.captureStream(30));
  recorder.ondataavailable = saveChunks;
  recorder.onstop = exportVideo;
  recorder.start();

  stopRec.onclick = stopRecording;
  // switch the canvas to be recorder every 200ms
  switchInterval = setInterval(switchStream, 200);

}

function saveChunks(evt) {
  // store our final video's chunks
  if (evt.data.size > 0) {
    chunks.push(evt.data);
  }

}

function stopRecording() {
    // stop everything, this will trigger recorder.onstop
    recorder.stop();
    clearInterval(switchInterval);
    stopCanvasAnim();
    a.style.display = b.style.display = 'none';
    this.parentNode.innerHTML = "";
    recordingCtx.canvas.parentNode.removeChild(recordingCtx.canvas)
  }
  // when we've got everything

function exportVideo() {
  vid.src = URL.createObjectURL(new Blob(chunks));
}

// switch between 1 and 0
function switchStream() {
    current = +!current;
  }
  // some fancy drawings
var stopCanvasAnim = (function initCanvasDrawing() {

  var aCtx = canvases[0].getContext('2d'),
    bCtx = canvases[1].getContext('2d');

  var objects = [],
    w = canvases[0].width,
    h = canvases[0].height;
  aCtx.fillStyle = bCtx.fillStyle = 'ivory';
  // taken from 
  for (var i = 0; i < 100; i++) {
    objects.push({
      angle: Math.random() * 360,
      x: 100 + (Math.random() * w / 2),
      y: 100 + (Math.random() * h / 2),
      radius: 10 + (Math.random() * 40),
      speed: 1 + Math.random() * 20
    });
  }
  var stop = false;
  var draw = function() {

    aCtx.fillRect(0, 0, w, h);
    bCtx.fillRect(0, 0, w, h);
    for (var n = 0; n < 100; n++) {
      var entity = objects[n],
        velY = Math.cos(entity.angle * Math.PI / 180) * entity.speed,
        velX = Math.sin(entity.angle * Math.PI / 180) * entity.speed;

      entity.x += velX;
      entity.y -= velY;

      aCtx.drawImage(imgA, entity.x, entity.y, entity.radius, entity.radius);
      bCtx.drawImage(imgB, entity.x, entity.y, entity.radius, entity.radius);

      entity.angle++;
    }
    if (!stop) {
      requestAnimationFrame(draw);
    }
  }


  var imgA = new Image();
  var imgB = new Image();
  imgA.onload = function() {
    draw();
    startRecording();
  };
  imgA.crossOrigin = imgB.crossOrigin = 'anonymous';
  imgA.src = "https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png";
  imgB.src = "https://dl.dropboxusercontent.com/s/rumlhyme6s5f8pt/ABC.png";

  return function() {
    stop = true;
  };
})();
<p>
  <button id="stopRec">stop recording</button>
</p>
<canvas id="a"></canvas>
<canvas id="b"></canvas>
<video id="vid" controls></video>