阅读 225

WebRTC 之通信 — PeerConnections 与信令服务

本文为 RTC 系列第三篇,上一篇介绍了 RTCPeerConnection 在本地的连接过程,如果是和远程用户连接,SDP 和 ICE 的交换是需要经过网络传输的,在 RTC 中,负责信息交换的服务器称为信令服务器,它所做的事情就是在连接前传输信息。由于 SDP 和 ICE 都是简单的字符串数据,因此信令服务只要能够传递字符串数据即可,任何满足要求的服务器都可以作为信令服务器,我们可以根据需要搭建自己的信令服务,可以使用 websocket,也可以使用 http、sip 等其他协议。

server 实现:

因为使用 socket.io 处理双向通信比较容易,因此本文以 socket.io 为例展示一个简单的 P2P 信令服务的实现,使用其他协议也同理。

用户想和其他用户进行通话首先需要知道对方是谁,因此信令服务器需要有一套类似房间的机制,具体形式取决于业务场景,可能是聊天室,可能是广场,总之需要把网络上的用户连起来,我们这里实现一个简单的 user-list 系统:

io.on("connection", async (socket) => {   const userList = [...(await io.allSockets())];   io.emit("user-list", userList); }); 复制代码

当用户 A 向 B 发起连接时,A 只要把之前发给 B 的内容发给服务器,并告诉服务器发送目标是 B 即可,同样 B 发消息也一样,只需要把消息发给服务器,指定目标给 A。对于服务器而言只需要将内容转发出去即可,完全不需要理解内容:

socket.on("message", (msg) => {   const target = io.sockets.sockets.get(msg.target);   target?.emit("message", { from: socket.id, data: msg.data, type: `on-${msg.type}` }); }); 复制代码

这样就实现了一个 server,下面来看 client 端实现。

client 实现:

与之前的本地 demo 相比,客户端最大的改变就是发送端和接收端是分离的,因此我们需要把两分逻辑拆分开,现在再来看 SDP 交互流程:

  1. A createOffer offerA

  2. A setLocalDescription offerA

  3. A 发送 offerA 给 B

  4. B setRemoteDescription offerA

  5. B createAnswer AnswerB

  6. B setLocalDescription AnswerB

  7. B 发送 AnswerB 给 A

  8. A setRemoteDescription AnswerB

这里 A 与 B 在两个不同的页面上,它们不会直接通信,中间加入信令服务器,交互的流程就变成了这样:

  • 发送方:

    • createOffer

    • setLocalDescription

    • 发送 offer 给 server

    • 接收 server 的 answer

    • setRemoteDescription

  • 接收方:

    • 接收 server 的 offer

    • setRemoteDescription

    • createAnswer

    • setLocalDescription

    • 发送 answer 给 server

我们先实现一个发送客户端,由于发送端是主动发起方,因此需要知道发送目标,这里使用一个简单的 user-list 机制来作为演示,当点击用户列表中的某个用户时向其进行发送:

userWrapper.addEventListener('click', async (e) => { targetId = e.target.innerText; if (targetId !== socket.id) { if (targetId) { const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = localStream; createPeerConnection(); localStream.getTracks().forEach(track => pc.addTrack(track, localStream)); } } }); 复制代码

createPeerConnection 中创建一个 RTCPeerConnection,可以在 negotiationneeded 事件回调中创建 offer,在监听到 icecandidate 时发送出去:

function createPeerConnection() {     pc = new RTCPeerConnection(pcConfig);     pc.addEventListener('negotiationneeded', async () => {     const offer = await pc.createOffer();     await pc.setLocalDescription(offer);     socket.emit('message', {         type: 'offer',         target: targetId,         data: { offer }     });   }); } 复制代码

之后就是等待回复了,这里监听 server 的数据:

case 'on-answer': await pc.setRemoteDescription(data.answer); break; 复制代码

之后再看接收端的逻辑,接收端的数据源来自信令服务器,这里从监听 server 开始:

case 'on-offer': targetId = from; createPeerConnection(); await pc.setRemoteDescription(data.offer); const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = localStream; localStream.getTracks().forEach(track => { pc.addTrack(track, localStream); }); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); socket.emit('message', { type: 'answer', target: targetId, data: { answer }, }); break; 复制代码

其中 createPeerConnection 逻辑与发送端是一致的,这里不会触发 negotiationneeded,不需要创建 offer。之后数据转发回发送端正常处理,SDP 交换的流程就结束了。

SDP 之后进行 ICE 交换,ICE 逻辑很简单,不需要数据握手,只要在收到时发出去,收到时设置到 PeerConnection 上即可:

pc.addEventListener('icecandidate', e => {     if (e.candidate) {         socket.emit('message', {             type: 'candidate',             target: targetId,             data: { candidate: e.candidate }         });     } }); 复制代码

case 'on-candidate': await pc.addIceCandidate(data.candidate); break; 复制代码

这样就建立好连接了,想要获取远端流直接监听 track 就可以:

pc.addEventListener('track', e => { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; } }); 复制代码

以上就是一个简单的 WebRTC P2P 系统的实现,整个过程中,只有连接时需要信令服务器的参与,连接成功后,音视频的发送是通过 addTrack 完成的,这个过程不需要信令服务器参与。

但是上面的 P2P demo 有一个问题,它只能在同一内网环境工作,这就涉及到 P2P 中另一个知识点:打洞和穿越。

STUN/TURN

由于 NAT 的特性,外网主机无法和内网主机直接建立连接。这时需要在我自己的 NAT 设备上先打一个洞,外网主机穿越这个洞来与内网用户建立连接。

我们可以使用 STUN/TURN 服务来实现内网穿透的能力,在创建 RTCPeerConnection 时可以通过参数传入 STUN/TURN 信息:

const config = { 'iceServers': [ { 'urls': 'stun:stun.l.google.com:19302' }, { "urls": "turn:numb.viagenie.ca", "username": "webrtc@live.com", "credential": "muazkh" } ] }; new RTCPeerConnection(config); 复制代码

STUN/TURN 服务器的搭建过程在此不展开了,感兴趣可以阅读 coturn 项目。

多人场景架构

上面的是一对一场景下的实现方案,在实际应用中还有多人音视频的需求,处理多人场景有以下几种方案:

  • P2P:P2P(Point To Point)即点对点,在一对一会话场景我们已经见过了,多人会话和单人会话一样,每两个人都要建立 P2P 连接。

    • 客户端:n 人的会话,推流 n - 1 拉流 n - 1

    • 优点:不需要服务器感知视频流

    • 缺点:人数越多客户端压力越大,无法处理太多人的场景

  • MCU:MCU(Multi-point Control Unit)中文为多点控制单元,有一个中心服务器,用户的所有流都直接推到这个服务器上,由服务器将多路数据流合成一路,客户端拉这一路流即可。

    • 客户端:n 人的会话,推流 1 拉流 1

    • 优点:节省客户端资源,适用于广播业务场景

    • 缺点:流在服务端已经确定,不能定制,无法满足灵活定制的业务场景

  • SFU:SFU(Selective Forwarding Unit) 中文为选择性转发单元,中心服务器只负责流的转发,由客户端根据需要选择拉流。

    • 客户端:n 人的会话,推流 1 拉流 n

    • 优点:可以满足复杂多遍的需求场景

    • 缺点:拉流过多时客户端性能有影响,需要根据实际业务情况进行平衡

多人音视频会话有不同的业务形态,实际应用中需要根据具体的场景选择合适的架构方案。


作者:丨隋堤倦客丨
链接:https://juejin.cn/post/7036035156787855396

 伪原创工具 SEO网站优化  https://www.237it.com/ 


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐