webRTC實戰總結

前言

前段時間一直在忙一個基於WebRTC的PC和移動端雙向視頻的項目。第一次接觸webRTC,不免遇到了許多問題,好比:webRTC移動端兼容性檢測,如何配置MediaStreamConstraints, 信令(iceCandidate, sessionDescription)傳輸方式的選擇,iceCandidate和sessionDescription設置的前後順序,STUN和TURN的概念,如何實現截圖及錄製視頻及上傳圖片和視頻功能,如何高效跟蹤錯誤等等。好記性不如爛筆頭,特寫此文以記之。web

移動端兼容性

對PC端來講,webRTC早已被各大瀏覽器支持了,Chrome 28,FF22,Edge…隨着不久以前發佈的IOS11也宣佈支持webRTC及getUserMedia,webRTC在移動端的應用前景也使人憧憬。canvas

具體到實際項目中,通過測試,各大國產安卓手機自帶的瀏覽器基本不支持webRTC,但這些安卓手機的微信內置瀏覽器均能良好地支持webRTC,雖然Chrome及Firefox的移動端版本也能良好的支持webRTC,但國情決定了微信內置瀏覽器做爲最佳切入點。另外一方面。IOS11中微信內置瀏覽器還不支持webRTC(我堅信不久的未來就會支持),但在Safari中可以完美支持。所以本項目選擇了微信公衆號爲切入點,經過檢測userAgent引導IOS11用戶在Safari中打開頁面。後端

檢測webRTC的可行性,主要從getUserMedia和webRTC自己來入手:瀏覽器

function detectWebRTC() { const WEBRTC_CONSTANTS = ['RTCPeerConnection', 'webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer']; const isWebRTCSupported = WEBRTC_CONSTANTS.find((item) => { return item in window; }); const isGetUserMediaSupported = navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia; if (!isWebRTCSupported || typeof isGetUserMediaSupported === 'undefined' ) { return false; } return true; }

若是返回false,再去檢測userAgent給予用戶不支持的具體提示。性能優化

配置MediaStreamConstraints

所謂MediaStreamConstraints,就是navigator.mediaDevices.getUserMedia(constraints)傳入的constraints,至於它的寫法及功能,參考MDN,本文不作贅述。我在這裏想要強調的是,對於移動端來講控制好視頻圖像的大小是很重要的,例如本項目中想要對方的圖像佔據全屏,這不只是改變video元素的樣式或者屬性能作到的,首先要作的是改變MediaStreamConstraints中的視頻分辨率(width, height),使其長寬比例大體與移動端屏幕的相似,而後再將video元素的長和寬設置爲容器的長和寬(例如100%)。服務器

另外對於getUserMedia必定要捕獲可能出現的錯誤,若是是老的API,設置onErr回調,若是是新的(navigator.mediaDevices.getUserMedia),則catch異常。這樣作的緣由:getUserMedia每每不會徹底符合咱們的預期,有時即便設置的是ideal的約束,仍然會報錯,若是不追蹤錯誤,每每一臉懵逼。這也是後文要提到的高效追蹤錯誤的方法之一。微信

搭建信令傳輸服務

要傳輸的信令包括兩個部分:sessionDescription和iceCandidate。爲了便於傳輸可將其處理成字符串,另外一端接收時還原並用對應的構造函數構造對應的實例便可。websocket

webRTC並無規定信令的傳輸方式,而是徹底由開發者自定義。常見的方式有短輪詢、webSocket(socket.io等),短輪詢的優勢無非是簡單,兼容性強,但在併發量較大時,服務器負荷會很重。而webSocket就不存在這個問題,但webSocket搭建起來較爲複雜,並非全部的瀏覽器都支持websocket。綜合來講socket.io是個不錯的解決方案,事件機制和自帶的房間概念對撮合視頻會話都是自然有利的,而且當瀏覽器不支持websocket時能夠切換爲輪詢,也解決了兼容性的問題。markdown

發起視頻會話的流程

能夠看到不管是發起方仍是接受方,第一步都是getUserMedia獲取本地媒體流,而後新建一個RTCPeerConnection實例,並指定好onicecandidate、onaddstream等回調:網絡

// 指定TURN及STUN const peerConnectionConfig = { 'iceServers': [ { 'urls': 'turn:numb.viagenie.ca', 'username': 'muazkh', 'credential': 'webrtc@live.com' }, { 'urls': 'stun:stun.l.google.com:19302' } ], bundlePolicy: 'max-bundle', }; const pc = new RTCPeerConnection(peerConnectionConfig); pc.onicecandidate = ...; pc.onaddstream = ...;

而後addTrack指定要傳輸的視頻流

stream.getTracks().forEach((track) => { pc.addTrack(track, stream); });

發起方經過createOffer生成localDescription並傳給pc.setLocalDescription(),pc獲取了本地的sdp後開始獲取candidate,這裏的candidate指的是網絡信息(ip、端口、協議),根據優先級從高到低分爲三類:

  • host: 設備的ipv4或ipv6地址,即內網地址,通常會有兩個,分別對應udp和tcp,ip相同,端口不一樣;
  • srflx(server reflexive): STUN返回的外網地址;
  • relay: 當STUN不適用時(某些NAT會爲每一個鏈接分配不一樣的端口,致使獲取的端口和視頻鏈接端口並不一致),中繼服務器的地址;

三者之中只須要有一類鏈接成功便可,因此若是通訊雙方在同一內網,不配置STUN和TURN也能夠直接鏈接。其實這裏隱藏着性能優化的點:如上圖所示,webRTC通訊雙方在交換candidate時,首先由發起方先收集全部的candidate,而後在icegatheringstatechange事件中檢測iceGatheringState是否爲’complete’,再發送給接收方。接收方設置了發送方傳來的sdp和candidate後,一樣要收集完本身全部的candidate,再發送給對方。若是這些candidate中有一對能夠鏈接成功,則P2P通訊創建,不然鏈接失敗。

問題來了,接受端要等待發起方收集完全部的candidate以後纔開始收集本身的candidate,這實際上是能夠同時進行的;另外其實不必定須要全部的candidate才能創建鏈接,這也是能夠省下時間的;最後若是網絡,STUN或者TURN出現問題,在上述傳輸模式下是很是致命的,會讓鏈接的時間變得很長不可接受。

解決方案就是IETF提出的Trickle ICE。即發起方每獲取一個candidate便當即發送給接收方,這樣作的好處在於第一類candidate即host,會當即發送給接收方,這樣接收方收到後能夠馬上開始收集candidate,也就是發起方和接收方同時進行收集candidate的工做。另外,接收方每收到一個candidate會當即去檢查它的有效性,若是有效直接接通視頻,若是無效也不至於浪費時間。詳情能夠參見ICE always tastes better when it trickles.

至於sessionDescription及iceCandidate的傳輸,由於JavaScript沒有處理sdp格式數據的方法,因此直接將其當作字符串處理,這樣作的壞處是難以改變sdp中的信息(若是非要改,經過正則匹配仍是能改的)。

在掛斷視頻時,不只要關閉peerConnection,也要中止本地及遠程的媒體流:

 const tracks = localStream.getTracks().concat(remoteStream.getTracks()); tracks.forEach((track) => { track.stop(); }); peerConnection.close();

截圖&錄製視頻

截圖其實並不算什麼新鮮的東西,無非是利用canvas的drawImage函數獲取video元素在某一幀的圖像,獲得的是圖片的base64格式字符串,但要注意的是這樣獲得的base64碼以前有這樣一串文本:

data:image/png;base64,

這是對數據協議,格式,編碼方式的聲明,是給瀏覽器看的。因此在將drawImage獲得的字符串上傳給服務器時,最好將這串文本去掉,防止後端在轉換圖片時出現錯誤。

錄製視頻使用的是MediaRecorder API 詳情參考MDN MediaRecorder,目前僅支持錄製webm格式的視頻。能夠在新建MediaRecorder實例的時候,設置mimeType、videoBitsPerSecond、audioBitsPerSecond:

const options = { mimeType: 'video/webm;codecs=vp8', // 視頻格式及編碼格式 videoBitsPerSecond: 2500000, // 視頻比特率,影響文件大小和質量  audioBitsPerSecond: 128000 // 音頻比特率,影響文件大小和質量 }; const recorder = new MediaRecorder(options);

在recorder的ondataavailable事件中拿到數據,將其轉換爲Blob對象,再經過Formdata異步上傳至服務器。

錯誤追蹤

整個雙向視頻涉及到的步驟較多,作好錯誤追蹤是很是重要的。像getUserMedia時,必定要catch可能出現的異常。由於不一樣的設備,不一樣的瀏覽器或者說不一樣的用戶每每不能徹底知足咱們設置的constraints。還有在實例化RTCPeerConnection時,每每會出現不可預期的錯誤,常見的有STUN、TURN格式不對,還有createOffer時傳遞的offerOptions格式不對,正確的應該爲:

const offerOptions = { 'offerToReceiveAudio': true, 'offerToReceiveVideo': true };

CAVEAT

由於webRTC標準還在不斷地更新中,因此相關的API常常會有改動。

  • navigator.getUserMeida(已廢棄),如今改成navigator.mediaDevices.getUserMedia;
  • RTCPeerConnection.addStream被RTCPeerConnection.addTrack取代;
  • STUN,TURN配置裏的url現被urls取代;

另外,對video元素也要特殊處理。設置autoPlay屬性,對播放本地視頻源的video還要設置muted屬性以去除迴音。針對IOS播放視頻自動全屏的特性,還要設置playsinline屬性的值爲true。

相關文章
相關標籤/搜索