使用Swoole协程实现 WebRTC 信令服务器

一、什么是WebRTC
WebRTC技术是激烈的开放的Web战争中一大突破-Brendan Eich, inventor of JAVAScript 。
简单来说,WebRTC 是一个音视频处理+及时通讯的开源库 。在实时通信中,音视频的采集和处理是一个很复杂的过程 。比如音视频流的编解码、降噪和回声消除等 。由google发起开源,其中包含视频音频采集,编解码,数据传输,音视频展示等功能,我们可以通过技术快速地构建出一个音视频通讯应用 。虽然其名为WebRTC,但是实际上它不只是支持Web之间的音视频通讯,还支持Android以及IOS端,此外由于该项目是开源的,我们也可以通过编译C++代码,从而达到全平台的互通 。
WebRTC的架构图为:

使用Swoole协程实现 WebRTC 信令服务器

文章插图
 
我们可以看到模块化和分层的设计,我们文章的目的是演示浏览器端对端的连接流程,焦点是服务端信令服务器的实现方式,但需要提前介绍一些WebRTC的基本概念和连接流程 。
二、基础概念
流和轨
  • Track 轨道,可以理解每一路音频或视频,为一个轨,互不相交,类比火车轨道 。
  • MediaStream 媒体流,每个媒体流中包含若干轨道,可以将音频轨,视频轨打包在一起 。
三、几个关键类
  • MediaStream 媒体流类,MeidiaStream用于将多个MediaStreamTrack对象打包到一起 。一个MediaStream可包含audio track 与video track,并且可以添加或者删除 。
  • RTCPeerConnection 连接类,包含非常多重要功能,屏蔽复杂技术细节,便于应用层使用,包括但不限于连接管理,P2P类型检测,NAT穿透,中转等 。
  • RTCDataChannel 非音视频数据传输类,这个类在我们的例子中没有涉及到 。可以简单理解为将媒体流信息或者数据信息塞到连接中,进行传输 。
四、端对端连接流程
两个不同网络环境浏览器,要实现点对点的实时音视频对话,需要处理哪些问题?
媒体协商
双方需要知道对方支持的媒体格式,SDP(Session Description Protocol)是一种会话描述协议,视频通讯的双方必须先交换SDP信息,才能进一步互相通信 。
网络协商
双方要了解对方的网络情况,尝试寻求一个可以互相通讯的链路,其中有寻路选择,如果确实没办法建立点对点链路,会使用中继服务器来进行转发 。如果是内网,或者大部分NAT网络环境下,是可以建立端到端连接 。在解决网络打通问题时候,有几个概念 。
  • STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT在公网的端口映射信息 。这些信息被用来在两端创建UDP连接通信 。
  • TURN (Traversal Using Relays around NAT),如果客户端在NAT之后,那么在一些网络情景下,有可能建立点对点的通讯连接,这时就需要公网的服务器作为一个中继,对数据进行转发 。
学习过程中,STUN和TURN服务器我们可使用coturn开源项目来搭建 。
数据交换服务-信令服务器
WebRTC实现并没有规定信令服务器的实现方式和相关协议,这给了业务方技术选型极大的灵活 。我们今天就是使用php+Swoole协程实现一个简单信令服务器 。下面是一个端到端连接的流程图,整个核心流程逻辑都在图里面 。
使用Swoole协程实现 WebRTC 信令服务器

文章插图
 
五、使用Swoole实现信令服务器
客户端代码模拟
<body><div style="display: block"><button class="btn" onclick="start()">连接<tton><button class="btn" onclick="leave()">离开<tton></div><div><div class="videos"><h1>Local</h1><video id="localVideo" autoplay><ideo></div><div class="videos"><h1>Remote</h1><video id="remoteVideo" autoplay><ideo></div></div><script src="assets/js/adapter.js"></script><script type="text/JavaScript">const ws_config = '<?= $signaling_server ?>';const localVideo = document.getElementById('localVideo');const remoteVideo = document.getElementById('remoteVideo');const configuration = {iceServers: [{urls: '<?= $stun_server ?>'}]};let room_id = getQueryVariable('room_id');if (room_id == '' || room_id == null) {room_id = Math.random().toString(36).slice(-8);location.href = '?room_id=' + room_id;}let subject = 'room-' + room_id;//当前主题let answer = 0;let ws = null;let pc, localStream;function getMediaStream(stream) {localVideo.srcObject = localStream;localStream = stream;}function start() {ws = new WebSocket(ws_config);ws.onopen = function (e) {subscribe(subject);if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {console.error('the getUserMedia is not supported!');return;}navigator.mediaDevices.getUserMedia({audio: true,video: true}).then(function (stream) {if (localStream) {stream.getAudioTracks().forEach((track) => {localStream.addTrack(track);stream.removeTrack(track);});} else {localStream = stream;}localVideo.srcObject = localStream;publish('call', null);}).catch(function (e) {console.error('Failed to get Media Stream!', e);});};ws.onmessage = function (e) {let package = JSON.parse(e.data);let data = package.data;console.log(e);switch (package.event) {case 'call':icecandidate(localStream);pc.createOffer({offerToReceiveAudio: 1,offerToReceiveVideo: 1}).then(function (desc) {pc.setLocalDescription(desc).then(function () {publish('offer', pc.localDescription);}).catch(function (e) {alert(e);});}).catch(function (e) {alert(e);});break;case 'answer':pc.setRemoteDescription(new RTCSessionDescription(data),function () {}, function (e) {alert(e);});break;case 'offer':icecandidate(localStream);pc.setRemoteDescription(new RTCSessionDescription(data),function () {if (!answer) {pc.createAnswer(function (desc) {pc.setLocalDescription(desc, function () {publish('answer', pc.localDescription);}, function (e) {alert(e);});}, function (e) {alert(e);});answer = 1;}}, function (e) {alert(e);});break;case 'candidate':pc.addIceCandidate(new RTCIceCandidate(data), function () {}, function (e) {alert(e);});break;}};}function leave() {pc.close();}function icecandidate(localStream) {pc = new RTCPeerConnection(configuration);pc.onicecandidate = function (event) {if (event.candidate) {publish('candidate', event.candidate);}};try {pc.addStream(localStream);} catch (e) {let tracks = localStream.getTracks();for (let i = 0; i < tracks.length; i++) {pc.addTrack(tracks[i], localStream);}}pc.onaddstream = function (e) {remoteVideo.srcObject = e.stream;};}function publish(event, data) {let obj = {cmd: 'publish',subject: subject,event: event,data: data};console.log(obj);ws.send(JSON.stringify(obj));}function subscribe(subject) {let obj = {cmd: 'subscribe',subject: subject};console.log(obj);ws.send(JSON.stringify(obj));}function getQueryVariable(variable) {var query = window.location.search.substring(1);var vars = query.split("&");for (var i = 0; i < vars.length; i++) {var pair = vars[i].split("=");if (pair[0] == variable) {return pair[1];}}return false;}</script></body>


推荐阅读