使用 WebRTC 编码转换

有限可用性

此功能不是基线功能,因为它在一些最广泛使用的浏览器中不起作用。

WebRTC 编码转换提供了一种机制,可以通过高性能的 Stream API 修改编码的视频和音频帧,并将它们注入到传入和传出的 WebRTC 管道中。这使得端到端加密第三方代码编码的帧等用例成为可能。

该 API 定义了主线程和 worker 端的对象。主线程接口是 RTCRtpScriptTransform 实例,它在构造时指定了将实现转换器代码的 Worker。通过将 RTCRtpScriptTransform 添加到 RTCRtpReceiver.transformRTCRtpSender.transform 中,分别将运行在 worker 中的转换插入到传入或传出的 WebRTC 管道中。

在 worker 线程中创建了一个对应的 RTCRtpScriptTransformer 对象,它具有 ReadableStream readable 属性、WritableStream writable 属性以及从关联的 RTCRtpScriptTransform 构造函数传递的 options 对象。来自 WebRTC 管道的编码视频帧 (RTCEncodedVideoFrame) 或音频帧 (RTCEncodedAudioFrame) 将排队到 readable 上以进行处理。

RTCRtpScriptTransformer 可作为 rtctransform 事件的 transformer 属性提供给代码,每当将编码帧排队以进行处理时(以及在相应的 RTCRtpScriptTransform 构造时最初),都会在 worker 全局作用域中触发此事件。worker 代码必须实现一个事件处理程序,该处理程序从 transformer.readable 读取编码帧,根据需要修改它们,并按相同的顺序且不重复地将它们写入 transformer.writable

虽然接口没有对实现施加任何其他限制,但转换帧的一种自然方法是创建一个 管道链,该管道链将排队到 event.transformer.readable 流上的帧通过 TransformStream 发送到 event.transformer.writable 流。我们可以使用 event.transformer.options 属性来配置任何依赖于转换是否正在将来自打包器的传入帧或来自编解码器的传出帧排队的转换代码。

RTCRtpScriptTransformer 接口还提供了在发送编码视频时可用于让编解码器生成“关键”帧,以及在接收视频时可用于请求发送新关键帧的方法。如果(例如)它们在发送增量帧时加入电话会议,则这些方法可能有助于接收方更快地开始查看视频。

以下示例提供了更多关于如何使用基于 TransformStream 的实现的框架的具体示例。

测试是否支持编码转换

通过检查 RTCRtpSender.transform(或 RTCRtpReceiver.transform)是否存在来测试是否 支持编码转换

js
const supportsEncodedTransforms =
  window.RTCRtpSender && "transform" in RTCRtpSender.prototype;

为输出帧添加转换

通过将其对应的 RTCRtpScriptTransform 分配给传出轨道的 RTCRtpSender.transform,将运行在 worker 中的转换插入到传出的 WebRTC 管道中。

此示例演示了如何通过 WebRTC 流式传输用户网络摄像头的视频,并添加 WebRTC 编码转换以修改传出流。代码假设有一个名为 peerConnectionRTCPeerConnection 已连接到远程对等方。

首先,我们使用 getUserMedia() 从媒体设备获取视频 MediaStream,然后使用 MediaStream.getTracks() 方法获取流中的第一个 MediaStreamTrack,从而获取 MediaStreamTrack

使用 addTrack() 将轨道添加到对等连接,这会开始将其流式传输到远程对等方。addTrack() 方法返回用于发送轨道的 RTCRtpSender

js
// Get Video stream and MediaTrack
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const [track] = stream.getTracks();
const videoSender = peerConnection.addTrack(track, stream);

然后构造一个 RTCRtpScriptTransform,它接收一个 worker 脚本(定义转换)和一个可选对象(可用于将任意消息传递给 worker,在本例中,我们使用了值为“senderTransform”的 name 属性来告诉 worker 此转换将添加到出站流中)。我们通过将其分配给 RTCRtpSender.transform 属性,将转换添加到传出管道中。

js
// Create a worker containing a TransformStream
const worker = new Worker("worker.js");
videoSender.transform = new RTCRtpScriptTransform(worker, {
  name: "senderTransform",
});

下面的 使用单独的发件人和接收器转换 部分显示了如何在 worker 中使用 name

请注意,您可以随时添加转换,但通过在调用 addTrack() 后立即添加转换,转换将获得发送的第一个编码帧。

为输入帧添加转换

通过将其对应的 RTCRtpScriptTransform 分配给传入轨道的 RTCRtpReceiver.transform,将运行在 worker 中的转换插入到传入的 WebRTC 管道中。

此示例演示了如何添加转换以修改传入流。代码假设有一个名为 peerConnectionRTCPeerConnection 已连接到远程对等方。

首先,我们添加一个 RTCPeerConnection track 事件 处理程序,以在对等方开始接收新轨道时捕获该事件。在处理程序中,我们构造一个 RTCRtpScriptTransform 并将其添加到 event.receiver.transform 中(event.receiverRTCRtpReceiver)。与上一节一样,构造函数接收一个带有 name 属性的对象,但在这里我们使用 receiverTransform 作为值来告诉 worker 帧是传入的。

js
peerConnection.ontrack = (event) => {
  const worker = new Worker("worker.js");
  event.receiver.transform = new RTCRtpScriptTransform(worker, {
    name: "receiverTransform",
  });
  received_video.srcObject = event.streams[0];
};

再次注意,您可以随时添加转换流。但是,通过在 track 事件处理程序中添加它,可以确保转换流将获得轨道的第一个编码帧。

Worker 实现

工作线程脚本必须实现一个针对 rtctransform 事件的处理程序,创建一个 管道链,将 event.transformer.readableReadableStream)流通过 TransformStream 传递到 event.transformer.writableWritableStream)流。

一个工作线程可能支持转换传入或传出的编码帧,或两者兼而有之,并且转换可以是硬编码的,也可以使用从 Web 应用程序传递的信息在运行时配置。

基本的 WebRTC 编码转换

下面的示例展示了一个基本的 WebRTC 编码转换,它对排队帧中的所有位进行取反。它不使用也不需要从主线程传递的选项,因为相同的算法可以在发送方管道中用于对位进行取反,在接收方管道中用于恢复它们。

代码实现了 rtctransform 事件的处理程序。它构建了一个 TransformStream,然后使用 ReadableStream.pipeThrough() 通过它传递,最后使用 ReadableStream.pipeTo() 传递到 event.transformer.writable

js
addEventListener("rtctransform", (event) => {
  const transform = new TransformStream({
    start() {}, // Called on startup.
    flush() {}, // Called when the stream is about to be closed.
    async transform(encodedFrame, controller) {
      // Reconstruct the original frame.
      const view = new DataView(encodedFrame.data);

      // Construct a new buffer
      const newData = new ArrayBuffer(encodedFrame.data.byteLength);
      const newView = new DataView(newData);

      // Negate all bits in the incoming frame
      for (let i = 0; i < encodedFrame.data.byteLength; ++i) {
        newView.setInt8(i, ~view.getInt8(i));
      }

      encodedFrame.data = newData;
      controller.enqueue(encodedFrame);
    },
  });
  event.transformer.readable
    .pipeThrough(transform)
    .pipeTo(event.transformer.writable);
});

WebRTC 编码转换的实现类似于“通用” TransformStream,但有一些重要的区别。与通用流一样,其 构造函数 获取一个对象,该对象定义了一个可选的 start() 方法(在构造时调用),flush() 方法(在流即将关闭时调用),以及 transform() 方法(每次有要处理的块时调用)。与通用构造函数不同,在构造函数对象中传递的任何 writableStrategyreadableStrategy 属性都会被忽略,并且排队策略完全由用户代理管理。

transform() 方法的不同之处还在于它传递的是 RTCEncodedVideoFrameRTCEncodedAudioFrame,而不是通用的“块”。此处显示的该方法的实际代码并不突出,除了它演示了如何将帧转换为可以修改它的形式,并在之后将其排队到流上。

使用单独的发送方和接收方转换

如果发送和接收时的转换函数相同,则前面的示例有效,但在许多情况下,算法将不同。您可以为发送方和接收方使用单独的工作线程脚本,或者在一个工作线程中处理这两种情况,如下所示。

如果工作线程同时用于发送方和接收方,则需要知道当前编码帧是来自编解码器的输出,还是来自封包器的输入。可以使用 RTCRtpScriptTransform 构造函数 中的第二个选项来指定此信息。例如,我们可以为发送方和接收方定义一个单独的 RTCRtpScriptTransform,传递相同的工作线程和一个包含属性 name 的选项对象,该属性指示转换是在发送方还是接收方使用(如前面各节所示)。然后,该信息在工作线程的 event.transformer.options 中可用。

在此示例中,我们在全局专用工作线程作用域对象上实现了 onrtctransform 事件处理程序。name 属性的值用于确定要构建哪个 TransformStream(未显示实际的构造函数方法)。

js
// Code to instantiate transform and attach them to sender/receiver pipelines.
onrtctransform = (event) => {
  let transform;
  if (event.transformer.options.name == "senderTransform")
    transform = createSenderTransform(); // returns a TransformStream
  else if (event.transformer.options.name == "receiverTransform")
    transform = createReceiverTransform(); // returns a TransformStream
  else return;
  event.transformer.readable
    .pipeThrough(transform)
    .pipeTo(event.transformer.writable);
};

请注意,创建管道链的代码与前面的示例相同。

与转换的运行时通信

RTCRtpScriptTransform 构造函数 允许您将选项和传输对象传递给工作线程。在前面的示例中,我们传递了静态信息,但有时您可能希望在运行时修改工作线程中的转换算法,或从工作线程获取信息。例如,支持加密的 WebRTC 会议呼叫可能需要向转换使用的算法添加新密钥。

虽然可以使用 Worker.postMessage() 在运行转换代码的工作线程和主线程之间共享信息,但通常更容易共享 MessageChannel 作为 RTCRtpScriptTransform 构造函数 选项,因为这样,在处理新编码帧时,通道上下文可以直接在 event.transformer.options 中使用。

下面的代码创建了一个 MessageChannel 并将其第二个端口 传输 到工作线程。主线程和转换随后可以使用第一个和第二个端口进行通信。

js
// Create a worker containing a TransformStream
const worker = new Worker("worker.js");

// Create a channel
// Pass channel.port2 to the transform as a constructor option
// and also transfer it to the worker
const channel = new MessageChannel();
const transform = new RTCRtpScriptTransform(
  worker,
  { purpose: "encrypt", port: channel.port2 },
  [channel.port2],
);

// Use the port1 to send a string.
// (we can send and transfer basic types/objects).
channel.port1.postMessage("A message for the worker");
channel.port1.start();

在工作线程中,端口作为 event.transformer.options.port 可用。下面的代码显示了如何侦听端口的 message 事件以获取来自主线程的消息。您还可以使用端口向主线程发送消息。

js
event.transformer.options.port.onmessage = (event) => {
  // The message payload is in 'event.data';
  console.log(event.data);
};

触发关键帧

原始视频很少被发送或存储,因为表示每一帧为完整图像需要消耗大量空间和带宽。相反,编解码器会定期生成一个“关键帧”,其中包含足够的信息来构建完整图像,并在关键帧之间发送“增量帧”,这些帧仅包含自上次增量帧以来的更改。虽然这比发送原始视频效率高得多,但这意味着为了显示与特定增量帧关联的图像,您需要最后的关键帧和所有后续的增量帧。

这可能会导致新用户加入 WebRTC 会议应用程序时出现延迟,因为在他们收到第一个关键帧之前,他们无法显示视频。同样,如果使用编码转换来加密帧,则接收方在收到使用其密钥加密的第一个关键帧之前将无法显示视频。

为了确保在需要时尽早发送新的关键帧,event.transformer 中的 RTCRtpScriptTransformer 对象有两个方法:RTCRtpScriptTransformer.generateKeyFrame()(导致编解码器生成关键帧)和 RTCRtpScriptTransformer.sendKeyFrameRequest()(接收方可以使用它来请求发送方发送关键帧)。

下面的示例显示了主线程如何将加密密钥传递给发送方转换,并触发编解码器生成关键帧。请注意,主线程无法直接访问 RTCRtpScriptTransformer 对象,因此它需要将密钥和限制标识符(“rid”)传递给工作线程(“rid”是流 ID,指示必须生成关键帧的编码器)。在这里,我们使用 MessageChannel 执行此操作,使用与上一节相同的模式。代码假设已存在对等连接,并且 videoSender 是一个 RTCRtpSender

js
const worker = new Worker("worker.js");
const channel = new MessageChannel();

videoSender.transform = new RTCRtpScriptTransform(
  worker,
  { name: "senderTransform", port: channel.port2 },
  [channel.port2],
);

// Post rid and new key to the sender
channel.port1.start();
channel.port1.postMessage({
  rid: "1",
  key: "93ae0927a4f8e527f1gce6d10bc6ab6c",
});

工作线程中的 rtctransform 事件处理程序获取端口并使用它来侦听来自主线程的 message 事件。如果接收到事件,则获取 ridkey,然后调用 generateKeyFrame()

js
event.transformer.options.port.onmessage = (event) => {
  const { rid, key } = event.data;
  // key is used by the transformer to encrypt frames (not shown)

  // Get codec to generate a new key frame using the rid
  // Here 'rcevent' is the rtctransform event.
  rcevent.transformer.generateKeyFrame(rid);
};

接收方请求新关键帧的代码几乎相同,只是未指定“rid”。以下是仅端口消息处理程序的代码

js
event.transformer.options.port.onmessage = (event) => {
  const { key } = event.data;
  // key is used by the transformer to decrypt frames (not shown)

  // Request sender to emit a key frame.
  transformer.sendKeyFrameRequest();
};

浏览器兼容性

BCD 表仅在启用 JavaScript 的浏览器中加载。

另请参阅