概述
Insertable Stream 可插入流是新的 WebRTC API,,可用来操作通过 RTCPeerConnection 传送的 MediaStreamTracks 中的每一个字节。它让上层应用能对 WebRTC 底层媒体进行访问,让以往 WebRTC 应用中许多不可能做的情况都成为可能了, 比如替换视频聊天时的背景,实时进行音视频处理(降噪,美颜,打水印,加特效等)。
最新的规范在这里 https://w3c.github.io/webrtc-encoded-transform/。
webRTC 音视频处理流程
发送流程
- 从媒体设备/其他采集源中获得一帧一帧的数据
- 对原始数据进行编码
- <- 在这里插入自定义逻辑
- SRTP 加密
- 发送
接收流程
- 接收网络 RTP 包
- SRTP 解密
- RTP 组包
- <- 在这里插入自定义逻辑
- 解码数据
- 渲染数据
WebRTC Insertable Streams 可以让我们在发送流程中的 3 ,接收流程的 4 加入处理编码后的数据的能力, 起初是为了端到端加密而设计, 但他的使用场景确可以进一步的拓展。
Stream API
Streams 标准提供了一组通用的 API,用于创建此类流数据并与之交互,这些数据体现在可读流、可写流和转换流中。
- readable streams
- writable streams
- transform streams
这些 API 旨在更有效地映射到低级的 I/O 原始操作,包括在适当的情况下对字节流进行专门的处理。
它们允许将多个流轻松组合到管道链中,或者可以通过读取器和写入器直接使用。最后,它们被设计为自动提供背压和排队。
https://streams.spec.whatwg.org/ 有关于 Streams 的各种属性和方法的介绍。
ReadableStream
readable stream 是在 JavaScript 中由来自底层的 ReadableStream 对象表示的数据源,这是网络上或者本地某个地方的资源,可以从中获取数据。
WritableStream
可写流是您可以写入数据的目的地,在 JavaScript 中由 WritableStream 对象表示。 它用作对于底层接收器之上的抽象,一个可写入原始数据的底层的 I/O sink。
转换流,它允许我们对媒体流的原始数据进行操作,这也是端到端加密的核心部分。它的定义如下:
1 2 3 4 5 6 7 8 9 10
| new TransformStream({ transform: transformFuction });
function transformFuction (chunk, controller) { ... controller.enqueue(chunk); }
|
transform 接受一个方法,该方法有两个参数:
- chunk:数据块。chunk.data 是原始数据,它是一个 ArrayBuffer 类型的数据
- controller:控制器,用于将修改后的 chunk 压入队列
管道链
Stream API 可以用一个称为 pipe chain 的结构将这些流一个一个串起来,具体方法有 pipeThrough 和 pipeTo。
1
| ReadableStream.pipeThrough(TransformStream).pipeTo(WritableStream)
|
这样就实现了对原始数据的修改。
Insertable Streams API
可插入流其实指的是一种转换流,它可以在媒体流的处理过程中插入一些处理逻辑。它通过转换器 RTCRtpTransform 来实现,而 RTCRtpSender 和 RTCRtpReceiver 可操作 RTCRtpTransform。也就是说,可插入流可通过 RTCRtpSender 和 RTCRtpReceiver 上附加的 API 来将处理。
RTCRtpSender 对象的获取方法:
1 2 3 4 5 6 7 8 9 10
| localStream.getTracks().forEach(track => { const sender = pc.addTrack(track, localStream); });
localStream.getTracks().forEach(track => { pc.addTrack(track, localStream); }); const senders = pc.getSenders(); senders.forEach(sender => {});
|
其中 sender 就是 RTCRtpSender 对象。
RTCRtpReceiver 对象的获取方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| pc.ontrack(event => { const receiver = event.receiver; event.streams.forEach(stream => { video.srcObject = stream; }); });
pc.ontrack(event => { event.streams.forEach(stream => { video.srcObject = stream; }); }); const receivers = pc.getReceivers(); receivers.forEach(receiver => {})
|
其中 receiver 就是 RTCRtpReceiver 对象。
接下来我们来看一下使用可插入流的具体代码(信令的交互这里省略):
定义全局变量:
1 2 3 4 5
| var localStream; var pc
const localVideo = document.getElementById('local_video'); const remoteVideo = document.getElementById('remote_video');
|
调用媒体:
1 2 3 4 5 6
| navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(function (mediastream) { localStream = mediastream; localVideo.srcObject = mediastream; }).catch(function (e) { alert(e); });
|
初始化 PeerConnection 的时候需要加上特殊参数:
1 2 3
| pc = new RTCPeerConnection({ encodedInsertableStreams: true, });
|
发送方插入逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| localMediaStream.getTracks().forEach(track => { pc.addTrack(track, this.localMediaStream); }); pc.getSenders().forEach(sender => {setupSenderTransform(sender)});
function setupSenderTransform (sender) { const senderStreams = sender.createEncodedStreams();
const { readable, writable } = senderStreams;
const transformStream = new TransformStream({ transform: encodeFunction });
readable.pipeThrough(transformStream).pipeTo(writable); }
function encodeFunction(chunk, controller) { chunk.data = Encrypt(chunk.data);
controller.enqueue(chunk); }
|
接收方插入逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| pc.addEventListener('track', function (event) { setupReceiverTransform(event.receiver); event.streams.forEach(stream => { remoteVideo.srcObject = stream; }); });
function setupReceiverTransform (receiver) { const receiverStreams = receiver.createEncodedStreams();
const { readable, writable } = receiverStreams;
const transformStream = new TransformStream({ transform: decodeFuction });
readable.pipeThrough(transformStream).pipeTo(writable); }
function decodeFuction (chunk, controller) { chunk.data = Decrypt(chunk.data);
controller.enqueue(chunk); }
|
这里的 Encrypt 和 Decrypt 是加解密的方法。我们用 AES 来进行加解密:
先安装依赖
使用方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import CryptoJS from "crypto-js";
const key = CryptoJS.enc.Utf8.parse("1234123412ABCDEF"); const iv = CryptoJS.enc.Utf8.parse('ABCDEF1234123412');
function Decrypt(data) { const hex_str = Buffer2String(data); let decrypt = CryptoJS.AES.decrypt(hex_str, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return String2Buffer(decrypt.toString(CryptoJS.enc.Utf8)); }
function Encrypt(data) { let encrypted = CryptoJS.AES.encrypt(Buffer2String(data), key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return String2Buffer(encrypted.toString()); }
function String2Buffer (str) { var buffer = new ArrayBuffer(str.length); var uint8arr = new Uint8Array(buffer); for(var i=0; i<str.length; i++) { uint8arr[i] = str.charCodeAt(i); } return buffer; }
function Buffer2String (buffer) { var uint8arr = new Uint8Array(buffer); var str = ''; for(var i=0; i<uint8arr.length; i++) { str += String.fromCharCode(uint8arr[i]); } return str; }
|
由于 chunk.data 是 ArrayBuffer 类型,所以需要经过一系列的类型转换。