MediaStream 同时捕获画布和音频

IT技术 javascript canvas mediarecorder mediastream
2021-02-06 15:06:34

我正在做一个项目,我想:

  1. 加载一个视频js并显示在画布上。
  2. 使用过滤器来改变画布(以及视频)的外观。
  3. 使用 MediaStream captureStream() 方法和 MediaRecorder 对象记录画布表面和原始视频的音频。
  4. 在 HTML 视频元素中播放画布和音频流。

通过调整此 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 对象合并为一个单元,可以在媒体元素中记录或呈现。”

2个回答

是否可以创建一个包含来自两个不同源/元素的 MediaStreamTrack 实例的 MediaStream?

是的,您可以使用该MediaStream.addTrack()方法来完成。

但是 Firefox 只会将初始流的轨道用于记录器,直到此错误得到修复。


OP 已经知道如何获得所有这些,但这里提醒未来的读者:

  • 要从画布中获取 videoStream 轨道,您可以调用canvas.captureStream(framerate)方法。

  • 要从视频元素获取音频流跟踪,您可以使用 WebAudio API 及其createMediaStreamDestination方法。这将返回一个dest包含我们的音频流的 MediaStreamDestination 节点 ( )。然后,您必须将从您的视频元素创建MediaElementSource连接到此dest. 如果您需要向此流添加更多音轨,则应将所有这些源连接到dest.

现在我们有两个流,一个用于画布视频,一个用于音频,我们可以canvasStream.addTrack(audioStream.getAudioTracks()[0])在初始化我们的new MediaRecorder(canvasStream).

这是一个完整的示例,它现在只能在 chrome 中工作,并且可能很快在 Firefox 中工作,当他们修复错误时:

var cStream,
  aStream,
  vid,
  recorder,
  analyser,
  dataArray,
  bufferLength,
  chunks = [];

function clickHandler() {

  this.textContent = 'stop recording';
  cStream = canvas.captureStream(30);
  cStream.addTrack(aStream.getAudioTracks()[0]);

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

  recorder.ondataavailable = saveChunks;

  recorder.onstop = exportStream;

  this.onclick = stopRecording;

};

function exportStream(e) {

  if (chunks.length) {

    var blob = new Blob(chunks)
    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();
  this.parentNode.removeChild(this);
  recorder.stop();

}

function initAudioStream(evt) {

  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(this);
  sourceNode.connect(dest)
    // start the video
  this.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();

  rec.onclick = clickHandler;
  rec.disabled = false;
};

var loadVideo = function() {

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

  
}

function startCanvasAnim() {
  // from MDN https://developer.mozilla.org/en/docs/Web/API/AnalyserNode#Examples
  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();
<canvas id="canvas" width="500" height="200"></canvas>
<button id="rec" disabled>record</button>


Ps : 由于 FF 团队似乎需要一些时间来修复错误,这里有一个快速修复,使其也适用于 FF。

您还可以使用 混合两个音轨new MediaStream([track1, track2])
然而,chrome 目前在这个构造函数addTrack前面加上了前缀,但因为它确实支持,所以并不是真的需要它,我们可以用一些丑陋的东西来

var mixedStream = 'MediaStream' in window ? 
  new MediaStream([cStream.getVideoTracks()[0], aStream.getAudioTracks()[0]]) :
  cStream;
recorder = new MediaRecorder(mixedStream);

FF 和 chrome 的工作小提琴。

我将如何向我的 MediaStream 添加第二个独特的音频元素?这将是来自 mp3 的音频,而不是视频。我应该创建一个额外的媒体流目的地、目的地流和媒体元素源吗?连接它们,然后使用 addTrack() 将新的目标流添加到我的 MediaStream?我刚试过这个,导出的视频没有来自我的 mp3 的音频。
2021-03-24 15:06:34
以下是我的代码的相关部分:pastebin.com/f4RwH7MD
2021-03-24 15:06:34
感谢您的快速响应和代码示例!很高兴知道,一旦错误得到解决,混合画布/音频流将是可行的(至少在 Firefox 中)。
2021-03-25 15:06:34
@dsing7,实际上 chrome 没有错误,我打错了(忘记将 sourceNode 连接到 MediaStreamDestination...)修复了代码,现在可以在 Chrome 中使用了。
2021-03-25 15:06:34
很高兴听到 Chrome 按预期工作。感谢您的提醒!
2021-03-30 15:06:34

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);
// ...
@Kaiido,您通过对内容进行投票而不是留下理由而使社区变得更糟。我把它变成了一个社区维基,所以你可以在需要时改进它。此代码片段可防止人们在只需要原始逻辑时不得不解析您的完整演示。我已经把它变成了一个社区维基,所以如果你的反对票是由答案中的错误引起的,你可以改进它。
2021-03-26 15:06:34
是否可以使用来自麦克风的音频和来自网页的另一个音频录制画布?是否可以在同一个录音中包含 2 个或更多音频?我有一个游戏,我想录制并使用麦克风,但还要录制我录制时正在播放的任何其他音频。
2021-04-12 15:06:34