WebRTC 学习笔记
导论
笔者在写本文时在参加C4网络挑战赛的可编程网络应用创新赛项,由于指导老师在sdn方面有一定的研究,我们对SRV6,组播,P4都有一定了解, 而比赛要求基于江苏省未来网络创新研究院研发的虚拟仿真实 验平台搭建环境并开发应用,通过平台使用 OpenDaylight、Open vSwitch、Mininet 等开源软件,基于 Python、Java、P4 等语言进行编程开发,实现但不局限于智能路由选择、负载均衡网关、DDoS 防御、 流量工程等场景下的网络应用。
而为了找到这样一个场景,我们想做一个基于SRV6、组播的视频会议,因为视频会议对实时性要求很高,所以我们可以运用SRV6和组播的技术,来优化链路传输,提升会议质量。市面上大多数开源的视频会议都使用了WebRTC协议来实现,故在此对WebRTC进行学习和研究,并记录一篇笔记。
参考
相对于从我这进口“二手”知识,我更希望读者可以去看原文,故在此写出我写本文时参考的文章。
https://web.dev/webrtc-basics/
https://web.dev/webrtc-infrastructure/
WebRTC概述
通过语音和视频实现人与人之间的交流被称为实时通信或简称RTC。Google在2008年做了Gmail视频聊天,2011年引入了环聊,Google随后又开源GIPS的编解码器和回声消除技术,2011年5月爱立信第一次实现了webRTC。
WebRTC实现了三套API:
- MediaStream
- RTCPeerConnection
- RTCDataChannel
WebRTC应用程序所要做的几件事:
- 获取流音频,视频或其他数据。
- 获取网络信息,譬如IP地址和端口,并与其他WebRTC客户端(称为对等端)进行交换以启用连接,即使通过NAT和防火墙。
- 协调信令通信以报告错误并启动或关闭会话。
- 交换有关媒体和客户端功能的信息,例如分辨率和编解码器。
- 交换音频流,视频流或数据流。
因为网络层之类的我们想自己写,这里详细介绍一下前面两条:
MediaStream API
我们现在用浏览器看一下这个API具体的内容
这个Mediastream就作为我们生成的对象,他有一个输入和一个输出(传递给视频元素 or RTCPeerConnection
)。getAudioTracks
和getVideoTracks
方法返回MediaStreamTracks的数组。而这些可以追踪来自摄像头的值。我们可以用secObject.attribute
来设置绑定的视频元素。我们利用track.stop
则可以关闭摄像头。
A constraint saying that we absolutely must have a minimum resolution
of 1024x768:
getUserMedia({
video: { mandatory: { minWidth: 1024, minHeight: 768 } }
}, successCallback, errorCallback);
我们可以使用类似上面的代码来设置约束条件。
我们还可以使用 chrome.tabCapture
来设置屏幕捕获。
信令
WebRTC使用RTCPeerConnection在浏览器(也称为对等设备)之间通信流数据,但还需要一种机制来协调通信并发送控制消息,此过程被称为信令。WebRTC未指定信令方法和协议。信令不是RTCPeerConnection API的一部分。
我们假设为 Alice 和 Bob 进行通信,他们会使用onicecandidate()
来选取候选者。
ICE 是一种能够在两个对等实体(例如 WebRTC 客户端)之间建立连接的技术。当两个实体想要彼此通信时,它们会交换 ICE 候选者,这些候选者包含了可以连接到对方的一组网络地址和端口。在交换和选择这些候选者的过程中,WebRTC 使用了 ICE 协议。
在上面的代码中,当本地 ICE 候选项可用时,它将被发送到远程端点,以便双方能够完成 ICE 过程,建立连接。在这里,pc 表示 PeerConnection 对象,onicecandidate 是一个事件监听函数,每当有新的 ICE 候选项生成时,就会触发该函数;signaling.send 是发送 ICE 候选项的函数,通常通过 WebSocket、Socket.IO 等进行信令传输。
很显然,我们也可以自己去包装一个候选者出来,并且自己实现代码完成类似ICE
的过程,但是显然,ICE
是一个很成熟的方案。
选取好候选者后,Alice可以把序列号的候选数据发送给Bob(使用类似websocket
之类的)。
然后Bob要调用addicecandidate
(同样我们也可以封装出一个我们自己的)将候选信息存到添加到自身RTCPeerConnection
对象中远程对等描述中。
当然,对于传输的视频文件,我们还要做编解码和解析度等信息,我们可以通过会话描述协议完成,这些函数封装在 RTCPeerConnection
里面。
pc.onnegotiationneeded = async () => {
try {
await pc.setLocalDescription(await pc.createOffer());
// Send the offer to the other peer.
signaling.send({desc: pc.localDescription});
} catch (err) {
console.error(err);
}
};
执行类似上面的语句,Alice执行 RTCPeerConnection createOffer()
方法。 方法返回的对象传递给RTCSessionDescription
(Alice的本地会话描述)。
signaling.onmessage = async ({desc, candidate}) => {
try {
if (desc) {
// If you get an offer, you need to reply with an answer.
if (desc.type === 'offer') {
await pc.setRemoteDescription(desc);
const stream =
await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach((track) =>
pc.addTrack(track, stream));
await pc.setLocalDescription(await pc.createAnswer());
signaling.send({desc: pc.localDescription});
} else if (desc.type === 'answer') {
await pc.setRemoteDescription(desc);
} else {
console.log('Unsupported SDP type.');
}
} else if (candidate) {
await pc.addIceCandidate(candidate);
}
} catch (err) {
console.error(err);
}
};
在回调中,Alice使用setLocalDescription设置本地描述,然后通过其信令通道将此会话描述发送给Bob。
Bob 使用setRemoteDescription将爱丽丝Alice发送给他的描述设置为远程描述。
Bob 执行 RTCPeerConnection createAnswer()
方法,将他从 Alice 那里得到的远程描述传递给它(RTCPeerConnection)
,以便可以生成与她兼容的本地会话。createAnswer()
的回调会回传 RTCSessionDescription
对象,Bob 将其设置为本地描述,并将其发送给 Alice。
以上的过程称为 JavaScript 会话建立协议(JSEP)。
RTCPeerConnection
WebRTC的解编码器和协议做了很多工作,优化了在较差网络条件下的 RTC:
- 隐藏丢包
- 回声消除
- 带宽适应性
- 动态抖动缓冲
- 自动增益控制
- 降噪抑制
- 影像清理
这里我们重点关注RTCPeerConnection API plus servers
,服务器功能可以归纳成这四个:
- 用户发现和交流
- 发信号
- NAT / 防火墙的穿透
- 对等通信失败时,失败尝试的中继服务器和转发服务器
WebRTC自带的ICE
框架用于连接对等方(例如两个视频聊天客户端)的框架。最初,ICE尝试通过UDP以尽可能短的延迟直接连接对等方。在此过程中,STUN服务器只有一个任务:使NAT后面的对等方能够找到其公共地址和端口,这些应该是服务端做的事情,我会在下面详细讨论这些问题。
WebRTC所需要的后端服务
Peer discovery(发现对端)
我们需要完成可以找到我们需要对话的人,那么我们就需要设计一个身份和状态管理系统,并且给与用户发起对话的方法。
消息推送
用于信令的消息服务必须是双向的,客户端到服务器以及服务器到客户端。
我们可以使用多种手段实现双向通信,比如长轮询和EventSource API
。
但是最佳实践应该是websocket
,当然建立了会话之后,由于其他对等方会更改或终止会话,对等方也需要轮询信令消息。(个人理解成保活,类似于心跳)
Scale signaling (规模化信号)
作为信令服务器,当客户端较多时,应该支持高并发级别处理大量消息,因此我们应该实现一个高容量高性能的消息传递服务。
下面是一些可能的实现方案:
- XMPP
- ZeroMQ
- Pusher等商业云消息平台
Readymade signaling servers(现成的信令服务器)
这里有几种现成的WebRTC信令服务器,它们像上例一样使用Socket.IO,并与WebRTC客户端JavaScript库集成在一起:
- webRTC.io 是WebRTC的第一个抽象库之一。
- Signalmaster 是创建用于SimpleWebRTC JavaScript客户端库的信令服务器。
After signaling(应对NAT和防火墙)
在实际环境上,大多数设备都位于一层或多层NAT的后面,当我们将ICE
服务器的url
传给RTCPeerConnection
以后,ICE
会试图找到最佳的路径,首先直接进行连接,如果失败了,那么我们先用STUN
协议获得外部网络地址,然后用TURN
来中继转发流量。
STUN
NAT为设备提供了IP地址,让其可以在公共局域网里面使用,没有公共地址,我们就没办法实现WebRTC通信,那么我们就可以使用STUN
服务器来实现该问题的解决。
STUN的任务很简单,检查传入请求的ip:port
,然后传回ip:port
即可,通过这个过程就可以找到公共可访问地址了。
TURN
RTCPeerConnection尝试通过UDP建立对等方之间的直接通信。如果失败,则RTCPeerConnection求助于TCP。如果失败,则可以将TURN服务器用作后备,在端点之间中继数据,也就相当于一个传话筒。做一个简单的比喻,你想找我,发微信打电话都找不到我,你就选择打电话给我的室友,让他来找我并且传话。
总结
这篇文章参考官方的文档和一些函数的说明写成,可以在短时间内对WebRTC建立起基本的认识。其中,由于ICE
框架的功能我们可以通过一些sdn的知识来完成修改,所以着重关注了相关知识,其他方面缺漏较多可以阅读我参考的两篇文章作为补充。