在前端领域,WebRTC是一个相对小众的技术;但对于在线教育而言,却又是很是的核心。网上关于WebRTC的文章不少,本文将尝试以WebRTC工做过程为脉络进行介绍,让读者对这门技术有一个完整的概念。
WebRTC(Web Real-Time Communications)是由谷歌开源并推动归入W3C标准的一项音视频技术,旨在经过点对点的方式,在不借助中间媒介的状况下,实现浏览器之间的实时音视频通讯。javascript
与Web世界经典的B/S架构最大的不一样是,WebRTC的通讯不通过服务器,而直接与客户端链接,在节省服务器资源的同时,提升通讯效率。为了作到这点,一个典型的WebRTC通讯过程,包含四个步骤:找到对方,进行协商,创建链接,开始通信。下面将分别阐述这四个步骤。css
虽然不须要通过服务器进行通讯,可是在开始通讯以前,必须知道对方的存在,这个时候就须要信令服务器。html
所谓信令(signaling)服务器,是一个帮助双方创建链接的「中间人」,WebRTC并无规定信令服务器的标准,意味着开发者能够用任何技术来实现,如WebSocket
或AJAX
。前端
发起WebRTC通讯的两端被称为对等端(Peer),成功创建的链接被称为PeerConnection
,一次WebRTC通讯可包含多个PeerConnection
。java
const pc2 = new RTCPeerConnection({...});
在寻找对等端阶段,信令服务器的工做通常是标识与验证参与者的身份,浏览器链接信令服务器并发送会话必须信息,如房间号、帐号信息等,由信令服务器找到能够通讯的对等端并开始尝试通讯。git
其实在整个WebRTC通讯过程当中,信令服务器都是一个很是重要的角色,除了上述做用,SDP交换、ICE链接等都离不开信令,后文将会提到。github
协商过程主要指SDP交换。web
SDP(Session Description Protocol)指会话描述协议,是一种通用的协议,使用范围不只限于WebRTC。主要用来描述多媒体会话,用途包括会话声明、会话邀请、会话初始化等。浏览器
在WebRTC中,SDP主要用来描述:服务器
SDP协议基于文本,格式很是简单,它由多个行组成,每一行都为一下格式:
<type>=<value>
其中,type
表示属性名,value
表示属性值,具体格式与type
有关。下面是一份典型的SDP协议样例:
v=0 o=alice 2890844526 2890844526 IN IP4 host.anywhere.com s= c=IN IP4 host.anywhere.com t=0 0 m=audio 49170 RTP/AVP 0 a=rtpmap:0 PCMU/8000 m=video 51372 RTP/AVP 31 a=rtpmap:31 H261/90000 m=video 53000 RTP/AVP 32 a=rtpmap:32 MPV/90000
其中:
v=
表明协议版本号o=
表明会话发起者,包括username
、sessionId
等s=
表明session名称,为惟一字段c=
表明链接信息,包括网络类型、地址类型、地址等t=
表明会话时间,包括开始/结束时间,均为0
表示持久会话m=
表明媒体描述,包括媒体类型、端口、传输协议、媒体格式等a=
表明附加属性,此处用于对媒体协议进行扩展在WebRTC发展过程当中,SDP的语义(semantics)也发生了屡次改变,目前使用最多的是Plan B
和Unified Plan
两种。二者都可在一个PeerConnection
中表示多路媒体流,区别在于:
Plan B
:全部视频流和全部音频流各自放在一个m=
值里,用ssrc
区分Unified Plan
:每路流各自用一个m=
值目前最新发布的 WebRTC 1.0 采用的是Unified Plan
,已被主流浏览器支持并默认开启。Chrome浏览器支持经过如下API获取当前使用的semantics:
// Chrome RTCPeerconnection.getConfiguration().sdpSemantics; // 'unified-plan' or 'plan b'
协商过程并不复杂,以下图所示:
会话发起者经过createOffer
建立一个offer
,通过信令服务器发送到接收方,接收方调用createAnswer
建立answer
并返回给发送方,完成交换。
// 发送方,sendOffer/onReveiveAnswer为伪方法 const pc1 = new RTCPeerConnection(); const offer = await pc1.createOffer(); pc1.setLocalDescription(offer); sendOffer(offer); onReveiveAnswer((answer) => { pc1.setRemoteDescription(answer); }); // 接收方,sendAnswer/onReveiveOffer为伪方法 const pc2 = new RTCPeerConnection(); onReveiveOffer((offer) => { pc2.setRemoteDescription(answer); const answer = await pc2.createAnswer(); pc2.setLocalDescription(answer); sendAnswer(answer); });
值得注意的是,随着通讯过程当中双方相关信息的变化,SDP交换可能会进行屡次。
现代互联网环境很是复杂,咱们的设备一般隐藏在层层网关后面,所以,要创建直接的链接,还须要知道双方可用的链接地址,这个过程被称为NAT穿越,主要由ICE服务器完成,因此也称为ICE打洞。
ICE(Interactive Connectivity Establishment)服务器是独立于通讯双方外的第三方服务器,其主要做用,是获取设备的可用地址,供对等端进行链接,由STUN(Session Traversal Utilities for NAT)服务器来完成。每个可用地址,都被称为一个ICE候选项(ICE Candidate),浏览器将从候选项中选出最合适的使用。其中,候选项的类型及优先级以下:
新建PeerConnection
时可指定ICE服务器地址,每次WebRTC找到一个可用的候选项,都会触发一次icecandidate
事件,此时可调用addIceCandidate
方法来将候选项添加到通讯中:
const pc = new RTCPeerConnection({ iceServers: [ { "url": "stun:stun.l.google.com:19302" }, { "url": "turn:user@turnserver.com", "credential": "pass" } ] // 配置ICE服务器 }); pc.addEventListener('icecandidate', e => { pc.addIceCandidate(event.candidate); });
经过候选项创建的ICE链接,能够大体分为下图两种状况:
一样的,因为网络变更等缘由,通讯过程当中的ICE打洞,一样可能发生屡次。
WebRTC选择了UDP
做为底层传输协议。为何不选择可靠性更强的TCP
?缘由主要有三个:
UDP
协议无链接,资源消耗小,速度快TCP
协议的超时重连机制会形成很是明显的延迟而在UDP
之上,WebRTC使用了再封装的RTP
与RTCP
两个协议:
在实际通讯过程当中,两种协议的数据收发会同时进行。
下面将以一个demo的代码,来展现前端WebRTC中都用到了哪些API:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1"> <meta name="mobile-web-app-capable" content="yes"> <meta id="theme-color" name="theme-color" content="#ffffff"> <base target="_blank"> <title>WebRTC</title> <link rel="stylesheet" href="main.css"/> </head> <body> <div id="container"> <video id="localVideo" playsinline autoplay muted></video> <video id="remoteVideo" playsinline autoplay></video> <div class="box"> <button id="startButton">Start</button> <button id="callButton">Call</button> </div> </div> <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script> <script src="main.js" async></script> </body> </html>
'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); callButton.disabled = true; startButton.addEventListener('click', start); callButton.addEventListener('click', call); const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); let localStream; let pc1; let pc2; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; async function start() { /** * 获取本地媒体流 */ startButton.disabled = true; const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true}); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; } function gotRemoteStream(e) { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('pc2 received remote stream'); setTimeout(() => { pc1.getStats(null).then(stats => console.log(stats)); }, 2000) } } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } async function call() { callButton.disabled = true; /** * 建立呼叫链接 */ pc1 = new RTCPeerConnection({ sdpSemantics: 'unified-plan', // 指定使用 unified plan iceServers: [ { "url": "stun:stun.l.google.com:19302" }, { "url": "turn:user@turnserver.com", "credential": "pass" } ] // 配置ICE服务器 }); pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e)); // 监听ice候选项事件 /** * 建立应答链接 */ pc2 = new RTCPeerConnection(); pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e)); pc2.addEventListener('track', gotRemoteStream); /** * 添加本地媒体流 */ localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); /** * pc1 createOffer */ const offer = await pc1.createOffer(offerOptions); // 建立offer await onCreateOfferSuccess(offer); } async function onCreateOfferSuccess(desc) { /** * pc1 设置本地sdp */ await pc1.setLocalDescription(desc); /******* 如下以pc2为对方,来模拟收到offer的场景 *******/ /** * pc2 设置远程sdp */ await pc2.setRemoteDescription(desc); /** * pc2 createAnswer */ const answer = await pc2.createAnswer(); // 建立answer await onCreateAnswerSuccess(answer); } async function onCreateAnswerSuccess(desc) { /** * pc2 设置本地sdp */ await pc2.setLocalDescription(desc); /** * pc1 设置远程sdp */ await pc1.setRemoteDescription(desc); } async function onIceCandidate(pc, event) { try { await (getOtherPc(pc).addIceCandidate(event.candidate)); // 设置ice候选项 onAddIceCandidateSuccess(pc); } catch (e) { onAddIceCandidateError(pc, e); } console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); }
做为「概览」,本文从比较浅的层次介绍了WebRTC技术,不少细节及原理性的内容,限于篇幅未做深刻阐述。笔者也是刚接触几个月,若有谬误,还请告知。
在实际业务中,对WebRTC的使用并不是简单的P2P通讯,后面将另开话题,来聊聊在线教育业务在实时音视频中是如何使用WebRTC,又是如何实现百万级同时在线上课的。