RTCPeerConnection:addTrack() 方法

Baseline Widely available

This feature is well established and works across many devices and browser versions. It’s been available across browsers since January 2020.

RTCPeerConnection 接口的 addTrack() 方法将媒体轨道添加到将传输给其他对等端的轨道集合中。

备注: 通过向连接添加轨道来触发 negotiationneeded 事件,从而重新进行协商。详情请参见开始协商

语法

js
addTrack(track)
addTrack(track, stream1)
addTrack(track, stream1, stream2)
addTrack(track, stream1, stream2, /* …, */ streamN)

参数

track

一个 MediaStreamTrack 对象,表示要添加到对等连接的媒体轨道。

stream1、…、streamN 可选

一个或多个将要添加到轨道的本地 MediaStream 对象。

指定的 track 不一定必须是任何指定 stream 的一部分。相反,stream 是连接的接收端将 track 组合在一起的一种方式,以确保它们是同步的。将任一轨道添到连接的本地端的同一个 stream 中,该轨道在远程端也将位于同一个 stream 中。

返回值

将用于传输媒体数据的 RTCRtpSender 实例。

备注: 每个 RTCRtpSender 都与一个 RTCRtpReceiver 配对,组成一个 RTCRtpTransceiver。相对应的接收器会被置于静默状态(无法传递数据包),直到远程对等端向接收器添加一个或多个流。

异常

InvalidAccessError DOMException

如果指定的轨道(或其所有底层流)已经是 RTCPeerConnection 的一部分则抛出此异常。

InvalidStateError DOMException

如果 RTCPeerConnection 被关闭则抛出此异常。

使用说明

单轨多流

track 参数的后面,你可以选择性地指定一个或多个 MediaStream 对象,以便将轨道添加到这些流中。只有轨道会从一个对等端发送到另一个对等端,而不是流。由于流是针对每个对等端特定的,因此指定一个或多个流意味着在连接的另一端就会自动创建对应的流(或多个流),并自动将接收到的轨道添加到这些流中。

无流轨道

如果没有给轨道指定任何流,那么该轨道就是无流轨道。尽管将轨道插入哪个流中(如果有的话)是由远程对等端来决定的,这(无流轨道)也完全可行的。使用 addTrack() 的这种方式来构建只需一个流的简单应用程序类型也是非常普遍的。例如,如果你只与远程对等方共享一个包含音频轨道和视频轨道的流,那么你不需要处理哪个轨道在哪个流中的问题,你完全可以让收发器为你处理这些事情。

下面是一个示例函数,它使用 getUserMedia() 从用户的摄像头和麦克风获取流,然后将流中的每个轨道添加到对等连接中,而不需要为每个轨道指定流:

js
async function openCall(pc) {
  const gumStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true,
  });
  for (const track of gumStream.getTracks()) {
    pc.addTrack(track);
  }
}

结果是一组轨道被发送到远程对等端,而没有流与其关联。远程对等端上的 track 事件处理器将负责确定将每个轨道添加到哪个流中,这意味着也可以将它们全部添加到同一个流中。ontrack 处理器可能如下所示:

js
let inboundStream = null;

pc.ontrack = (ev) => {
  if (ev.streams && ev.streams[0]) {
    videoElem.srcObject = ev.streams[0];
  } else {
    if (!inboundStream) {
      inboundStream = new MediaStream();
      videoElem.srcObject = inboundStream;
    }
    inboundStream.addTrack(ev.track);
  }
};

在这里,事件中如果指定了特定的流,则 track 事件处理器会将轨道添加到由事件对象所指定的第一个流中。否则,当 ontrack 第一次被调用时,会创建一个新的流并将其附加到视频元素上,然后将轨道添加到新流中。此后,新的轨道会被添加到该流中。

你也可以为每个接收到的轨道创建一个新的流:

js
pc.ontrack = (ev) => {
  if (ev.streams && ev.streams[0]) {
    videoElem.srcObject = ev.streams[0];
  } else {
    let inboundStream = new MediaStream(ev.track);
    videoElem.srcObject = inboundStream;
  }
};

轨道与流关联

通过指定流并允许 RTCPeerConnection 为你创建的流,WebRTC 底层会自动为你管理这个流的轨道关联。包括像收发器的 direction 属性变更以及使用 removeTrack() 停止轨道传输等事情。

例如,考虑以下函数,应用程序可能会使用它来开始通过 RTCPeerConnection 将设备的摄像头和麦克风输入流传送到远程对等端:

js
async function openCall(pc) {
  const gumStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true,
  });
  for (const track of gumStream.getTracks()) {
    pc.addTrack(track, gumStream);
  }
}

然后远程对等端可以使用一个 track 事件处理器如下:

js
pc.ontrack = ({ streams: [stream] }) => (videoElem.srcObject = stream);

将视频元素的当前流设置为已添加到在包含连接上的轨道的流。

复用发送器

该方法返回一个新的 RTCRtpSender(发送器)或用于复用的现有实例。一个 RTCRtpSender 实例只有在满足以下条件时才能被复用:

  • 发送器尚未与任何轨道关联。
  • 与发送器关联的 RTCRtpTransceiver(收发器)拥有一个 RTCRtpReceiver(接收器),其 track 属性指定了一个 MediaStreamTrack,且该 kind 属性与调用 RTCPeerConnection.addTrack() 时指定的 track 参数的 kind 相同。这确保了收发器只处理音频或视频,而不是两者都处理。
  • RTCRtpTransceiver.currentDirection 属性的值不是 "stopped"
  • 考虑复用的 RTCRtpSender 从未被用于发送数据。如果收发器的 currentDirection 曾经是 "sendrecv""sendonly",则发送器不能被复用。

如果以上这些条件都满足,那么该发送器将被复用,这将导致现有的 RTCRtpSender 及其 RTCRtpTransceiver 发生以下变化:

  • RTCRtpSendertrack 属性被设置为指定的轨道。
  • 发送器关联的流集合被设置为传入此方法的流集合,即 stream...
  • 关联的 RTCRtpTransceivercurrentDirection 属性被更新为表示发送中;
  • 如果其当前值为 "recvonly",则变为 "sendrecv",如果其当前值为 "inactive",则变为 "sendonly"

新建发送器

如果没有现有的可复用发送器,则会创建一个新的。这也会导致创建必须存在的相关对象。创建新发送器的过程将产生以下变化:

  • 使用指定的 trackstream (集合) 创建新的 RTCRtpSender
  • 创建一个新的 RTCRtpReceiver,其 track 属性是一个新的 MediaStreamTrack(不是调用 addTrack(track) 时参数的 track)。这个媒体轨道的 kind 属性被设置为与输入参数 trackkind 属性相匹配。
  • 创建一个新的 RTCRtpTransceiver,并将其与新发送器和接收器关联。
  • 新收发器的 direction 的属性值设置为 "sendrecv"
  • 新收发器被添加到 RTCPeerConnection 的收发器集合中。

示例

下面这个例子是从文章信令与视频通话中给出的相应示例代码中提取的。它来自远程对等端接收到邀请消息时被调用的方法 handleVideoOfferMsg() 中。

js
const mediaConstraints = {
  audio: true, // 我们需要一个音频轨道
  video: true, // 以及一个视频轨道
};

const desc = new RTCSessionDescription(sdp);

pc.setRemoteDescription(desc)
  .then(() => navigator.mediaDevices.getUserMedia(mediaConstraints))
  .then((stream) => {
    previewElement.srcObject = stream;

    stream.getTracks().forEach((track) => pc.addTrack(track, stream));
  });

这段代码获取从远程对等端接收到的 SDP,并构造一个新的 RTCSessionDescription 实例并传递到 setRemoteDescription()。执行成功后,使用 MediaDevices.getUserMedia 来访问本地摄像头和麦克风。

如果(调用)成功,则将拿到的流作为变量 previewElement 所指向的 <video> 元素的源输入给它。

这是通过遍历 MediaStream.getTracks() 返回的列表中的每个轨道,并将它们与其所属的流一起传递给 addTrack() 方法来完成的。

规范

Specification
WebRTC: Real-Time Communication in Browsers
# dom-rtcpeerconnection-addtrack

浏览器兼容性

BCD tables only load in the browser

参见