同时捕获媒体流、画布和音频

34

我正在进行一个项目,希望能够:

  1. 加载video js并将其显示在画布上。
  2. 使用滤镜来改变画布的外观(因此也更改了视频)。
  3. 使用MediaStream captureStream()方法和MediaRecorder对象来记录画布的表面和原始视频的音频。
  4. 在HTML video元素中播放画布和音频的流。

通过调整这个WebRTC演示代码,我已经能够在视频元素中显示画布录制:https://webrtc.github.io/samples/src/content/capture/canvas-record/

尽管如此,我无法弄清楚如何同时记录视频的音频。是否可能创建一个包含来自两个不同源/元素的MediaStreamTrack实例的MediaStream?

根据MediaStream API的规范,理论上应该有一些方法可以实现这一点: https://w3c.github.io/mediacapture-main/#introduction

"MediaStream API中的两个主要组件是MediaStreamTrack和MediaStream接口。 MediaStreamTrack对象表示源自用户代理中一个媒体源的单个类型的媒体,例如由网络摄像机产生的视频。 MediaStream用于将多个MediaStreamTrack对象组合成一个单元,可以在媒体元素中记录或呈现。"


由于被接受的答案并不特别涉及p5.js,您介意我从您的问题标题和正文中将其删除吗? - Kaiido
2
完全没有问题,去做吧。 - dsing7
2个回答

53
“是否可以创建一个包含来自两个不同源/元素的MediaStreamTrack实例的MediaStream?”
“是的,您可以使用MediaStream.addTrack()方法或new MediaStream([track1, track2])来实现。”

OP已经知道如何获取所有内容,但这里是为未来的读者提醒:

  • 要从<canvas>中获取视频流,请调用canvas.captureStream(framerate)方法。

  • 要从<video>元素中获取音频流,请使用Web Audio API及其createMediaStreamDestination方法。 这将返回一个包含我们的音频流的MediaStreamAudioDestinationNode节点(dest)。然后,您需要将从您的<video>元素创建的MediaElementAudioSourceNode连接到此dest。 如果您需要向此流添加更多音轨,则应将所有这些源连接到dest

现在我们有两个流,一个用于<canvas>视频,另一个用于音频,我们可以在初始化录制器之前将音频轨道添加到画布流中:
canvasStream.addTrack(audioStream.getAudioTracks()[0]);
const recorder = new MediaRecorder(canvasStream)

或者我们可以从这两个轨道创建第三个MediaStream对象:

const [videoTrack] = canvasStream.getVideoTracks();
const [audioTrack] = audioStream.getAudioTracks();
const recordedStream = new MediaStream(videoTrack, audioTrack)
const recorder = new MediaRecorder(recordedStream);

这是一个完整的例子:

var
  btn = document.querySelector("button"),
  canvas,
  cStream,
  aStream,
  vid,
  recorder,
  analyser,
  dataArray,
  bufferLength,
  chunks = [];

function clickHandler() {

  btn.textContent = 'stop recording';
  
  if (!aStream) {
    initAudioStream();
  }
  
  cStream = canvas.captureStream(30);
  cStream.addTrack(aStream.getAudioTracks()[0]);

  recorder = new MediaRecorder(cStream);
  recorder.start();

  recorder.ondataavailable = saveChunks;

  recorder.onstop = exportStream;

  btn.onclick = stopRecording;

};

function exportStream(e) {

  if (chunks.length) {

    var blob = new Blob(chunks, { type: chunks[0].type });
    var vidURL = URL.createObjectURL(blob);
    var vid = document.createElement('video');
    vid.controls = true;
    vid.src = vidURL;
    vid.onend = function() {
      URL.revokeObjectURL(vidURL);
    }
    document.body.insertBefore(vid, canvas);

  } else {

    document.body.insertBefore(document.createTextNode('no data saved'), canvas);

  }
}

function saveChunks(e) {

  e.data.size && chunks.push(e.data);

}

function stopRecording() {

  vid.pause();
  btn.remove();
  recorder.stop();

}

function initAudioStream() {

  var audioCtx = new AudioContext();
  // create a stream from our AudioContext
  var dest = audioCtx.createMediaStreamDestination();
  aStream = dest.stream;
  // connect our video element's output to the stream
  var sourceNode = audioCtx.createMediaElementSource(vid);
  sourceNode.connect(dest)
  // start the video
  vid.play();

  // just for the fancy canvas drawings
  analyser = audioCtx.createAnalyser();
  sourceNode.connect(analyser);

  analyser.fftSize = 2048;
  bufferLength = analyser.frequencyBinCount;
  dataArray = new Uint8Array(bufferLength);
  analyser.getByteTimeDomainData(dataArray);

  // output to our headphones
  sourceNode.connect(audioCtx.destination)

  startCanvasAnim();
  
}
function enableButton() {

  vid.oncanplay = null;
  btn.onclick = clickHandler;
  btn.disabled = false;

};

var loadVideo = function() {

  vid = document.createElement('video');
  vid.crossOrigin = 'anonymous';
  vid.oncanplay = enableButton;
  vid.src = 'https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4';

}

function startCanvasAnim() {
  // from MDN https://developer.mozilla.org/en/docs/Web/API/AnalyserNode#Examples
  canvas = Object.assign(document.createElement("canvas"), { width: 500, height: 200});
  document.body.prepend(canvas);
  var canvasCtx = canvas.getContext('2d');

  canvasCtx.fillStyle = 'rgb(200, 200, 200)';
  canvasCtx.lineWidth = 2;
  canvasCtx.strokeStyle = 'rgb(0, 0, 0)';

  var draw = function() {

    var drawVisual = requestAnimationFrame(draw);

    analyser.getByteTimeDomainData(dataArray);

    canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
    canvasCtx.beginPath();

    var sliceWidth = canvas.width * 1.0 / bufferLength;
    var x = 0;

    for (var i = 0; i < bufferLength; i++) {

      var v = dataArray[i] / 128.0;
      var y = v * canvas.height / 2;

      if (i === 0) {
        canvasCtx.moveTo(x, y);
      } else {
        canvasCtx.lineTo(x, y);
      }

      x += sliceWidth;
    }

    canvasCtx.lineTo(canvas.width, canvas.height / 2);
    canvasCtx.stroke();

  };

  draw();

}

loadVideo();
button { vertical-align: top }
<button disabled>record</button>


感谢您的快速回复和代码示例!一旦修复了这个问题,混合画布/音频流将是可行的(至少在Firefox中),这是一个好消息。 - dsing7
这是我代码的相关部分:http://pastebin.com/f4RwH7MD - dsing7
@dsing7 我还没有尝试过,但我会将两个sourceNodes连接到同一个mediaStreamDestination。 - Kaiido
@Kaiido:“你是否在视频加载完成之前点击了按钮?”这就是问题所在。点击停止后,会记录“Uncaught InvalidStateError: Failed to execute 'stop' on 'MediaRecorder': The MediaRecorder's state is 'inactive'.”。也许可以在按钮被点击一次后将其设置为“禁用”状态? - guest271314
或许不是因为它只是一个记录画布和音频的示例,而不是一个纯功能应用程序?还需要进行很多特性检测,以避免在旧浏览器中引发的许多错误,但问题的重点并不在于此,它不是一个教程,只是一个最小化的示例。我认为这对答案没有任何帮助。 - Kaiido
显示剩余19条评论

28
Kaiido的演示非常出色。对于那些只想要tl;dr代码将音频流添加到现有画布流的人:
let videoOrAudioElement = /* your audio source element */;
// get the audio track:
let ctx = new AudioContext();
let dest = ctx.createMediaStreamDestination();
let sourceNode = ctx.createMediaElementSource(videoOrAudioElement);
sourceNode.connect(dest);
sourceNode.connect(ctx.destination);
let audioTrack = dest.stream.getAudioTracks()[0];
// add it to your canvas stream:
canvasStream.addTrack(audioTrack);
// use your canvas stream like you would normally:
let recorder = new MediaRecorder(canvasStream);
// ...

5
@Kaiido,您通过给内容投反对票并未留下原因而使社区变得更糟。我已将它设为社区Wiki,这样您如果需要的话可以进行改善。这段代码片段可以避免人们在只想获取纯粹逻辑时必须解析完整个演示文稿。如果您的反对票是基于答案错误,我已将其设为社区Wiki,您可以改进它。 - user993683
能否录制带有麦克风音频和网页中的其他音频的画布?是否可以在同一录音中使用2个或更多音频?我想录制一个游戏并使用麦克风,同时记录任何在录制时播放的其他音频。 - savram

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接