在前端領域,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,又是如何實現百萬級同時在線上課的。