使用 WebRTC 编码转换
WebRTC 编码转换提供了一种机制,可以在传入和传出的 WebRTC 流水线中注入高性能的 Stream API,以修改编码的视频和音频帧。这使得第三方代码能够实现编码帧的端到端加密等用例。
API 定义了主线程和 Worker 侧对象。主线程接口是一个 RTCRtpScriptTransform 实例,它在构造时指定了将实现转换器代码的 Worker。运行在 Worker 中的转换通过将 RTCRtpScriptTransform 添加到 RTCRtpReceiver.transform 或 RTCRtpSender.transform 中,分别插入到传入或传出的 WebRTC 流水线中。
在 Worker 线程中创建了一个对应的 RTCRtpScriptTransformer 对象,该对象具有一个 ReadableStream readable 属性、一个 WritableStream writable 属性,以及一个从关联的 RTCRtpScriptTransform 构造函数传递的 options 对象。来自 WebRTC 流水线的编码视频帧 (RTCEncodedVideoFrame) 或音频帧 (RTCEncodedAudioFrame) 被添加到 readable 中进行处理。
当编码帧被排队等待处理时(以及最初在构造相应的 RTCRtpScriptTransform 时),rtctransform 事件会在 Worker 全局作用域触发,而 RTCRtpScriptTransformer 作为该事件的 transformer 属性提供给代码。Worker 代码必须实现一个事件处理程序,该处理程序从 transformer.readable 中读取编码帧,根据需要修改它们,然后以相同的顺序且不重复地将它们写入 transformer.writable。
虽然接口没有对实现施加任何其他限制,但转换帧的自然方式是创建一个 管道链,该管道链通过 TransformStream 将在 event.transformer.readable 流上排队的帧发送到 event.transformer.writable 流。我们可以使用 event.transformer.options 属性来配置任何依赖于转换是将来自打包器的传入帧还是来自编解码器的传出帧排队的转换代码。
RTCRtpScriptTransformer 接口还提供了在发送编码视频时用于让编解码器生成“关键”帧以及在接收视频时请求发送新关键帧的方法。这些可能有助于接收方更快地开始观看视频,例如,如果他们加入一个正在发送增量帧的电话会议。
以下示例提供了使用基于 TransformStream 的实现来使用该框架的更具体示例。
测试是否支持编码转换
通过检查是否存在 RTCRtpSender.transform(或 RTCRtpReceiver.transform)来测试是否支持编码转换。
const supportsEncodedTransforms =
window.RTCRtpSender && "transform" in RTCRtpSender.prototype;
为传出帧添加转换
通过将其对应的 RTCRtpScriptTransform 分配给传出轨道的 RTCRtpSender.transform,在 Worker 中运行的转换被插入到传出的 WebRTC 流水线中。
此示例展示了如何通过 WebRTC 从用户的网络摄像头流式传输视频,并添加 WebRTC 编码转换来修改传出流。代码假设有一个名为 peerConnection 的 RTCPeerConnection,该连接已连接到远程对等方。
首先,我们获取一个 MediaStreamTrack,使用 getUserMedia() 从媒体设备获取视频 MediaStream,然后使用 MediaStream.getTracks() 方法获取流中的第一个 MediaStreamTrack。
使用 addTrack() 将轨道添加到对等连接,这会开始将其流式传输到远程对等方。addTrack() 方法返回用于发送轨道的 RTCRtpSender。
// 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 属性来将转换添加到传出流水线中。
// 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 流水线中。
此示例展示了如何添加转换以修改传入流。代码假设有一个名为 peerConnection 的 RTCPeerConnection,该连接已连接到远程对等方。
首先,我们添加一个 RTCPeerConnection track 事件处理程序,以捕获对等方开始接收新轨道时的事件。在处理程序中,我们构造一个 RTCRtpScriptTransform 并将其添加到 event.receiver.transform(event.receiver 是一个 RTCRtpReceiver)。与上一节一样,构造函数接受一个具有 name 属性的对象,但在这里我们使用 receiverTransform 作为值来告知 Worker 帧是传入的。
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 实现
Worker 脚本必须实现 rtctransform 事件的处理程序,创建一个 管道链,将 event.transformer.readable (ReadableStream) 流通过 TransformStream 管道到 event.transformer.writable (WritableStream) 流。
Worker 可能支持转换传入或传出的编码帧,或者两者都支持,并且转换可以是硬编码的,也可以在运行时使用从 Web 应用程序传递的信息进行配置。
基本 WebRTC 编码转换
下面的示例展示了一个基本的 WebRTC 编码转换,它对排队帧中的所有位进行反转。它不需要使用从主线程传入的选项,因为相同的算法可以在发送方流水线中用于反转位,并在接收方流水线中用于恢复位。
代码实现了 rtctransform 事件的事件处理程序。它构造一个 TransformStream,然后使用 ReadableStream.pipeThrough() 通过它进行管道传输,最后使用 ReadableStream.pipeTo() 管道到 event.transformer.writable。
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() 方法(每次有数据块需要处理时调用)的对象。与通用构造函数不同,在构造函数对象中传递的任何 writableStrategy 或 readableStrategy 属性都会被忽略,并且排队策略完全由用户代理管理。
transform() 方法也有所不同,因为它传递的是 RTCEncodedVideoFrame 或 RTCEncodedAudioFrame,而不是通用的“数据块”。这里显示的该方法的实际代码除了演示如何将帧转换为可以修改并随后在流上排队的格式之外,并没有什么值得注意的。
使用单独的发送方和接收方转换
如果发送和接收时的转换函数相同,则前面的示例有效,但在许多情况下,算法会不同。您可以为发送方和接收方使用单独的 Worker 脚本,或者在一个 Worker 中处理这两种情况,如下所示。
如果 Worker 用于发送方和接收方,它需要知道当前编码帧是来自编解码器的传出帧,还是来自打包器的传入帧。此信息可以使用 RTCRtpScriptTransform 构造函数中的第二个选项指定。例如,我们可以为发送方和接收方定义一个单独的 RTCRtpScriptTransform,传递相同的 Worker,以及一个具有 name 属性的 options 对象,该属性指示转换是在发送方还是接收方中使用(如前面几节所示)。然后,该信息在 Worker 中的 event.transformer.options 中可用。
在此示例中,我们在全局专用 Worker 作用域对象上实现了 onrtctransform 事件处理程序。name 属性的值用于确定要构造哪个 TransformStream(实际的构造方法未显示)。
// 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 构造函数允许您将选项和可传输对象传递给 Worker。在前面的示例中,我们传递了静态信息,但有时您可能希望在运行时修改 Worker 中的转换算法,或者从 Worker 获取信息。例如,支持加密的 WebRTC 电话会议可能需要向转换使用的算法添加新密钥。
虽然可以使用 Worker.postMessage() 在运行转换代码的 Worker 和主线程之间共享信息,但通常更容易共享 MessageChannel 作为 RTCRtpScriptTransform 构造函数选项,因为这样在处理新的编码帧时,通道上下文可以直接在 event.transformer.options 中使用。
下面的代码创建一个 MessageChannel 并将其第二个端口传输到 Worker。主线程和转换随后可以使用第一个和第二个端口进行通信。
// 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();
在 Worker 中,该端口作为 event.transformer.options.port 可用。下面的代码展示了如何侦听端口的 message 事件以从主线程获取消息。您还可以使用该端口将消息发送回主线程。
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”)传递给 Worker(“rid”是流 ID,指示必须生成关键帧的编码器)。这里我们使用 MessageChannel 来实现,采用与上一节相同的模式。代码假设已经存在对等连接,并且 videoSender 是一个 RTCRtpSender。
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",
});
Worker 中的 rtctransform 事件处理程序获取端口并使用它侦听来自主线程的 message 事件。如果收到事件,它会获取 rid 和 key,然后调用 generateKeyFrame()。
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”。这是仅端口消息处理程序的代码
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();
};
浏览器兼容性
加载中…