摘要:中微信內置瀏覽器還不支持我堅信不久的將來就會支持,但在中能夠完美支持。因此本項目選擇了微信公眾號為切入點,通過檢測引導用戶在中打開頁面。為了便于傳輸可將其處理成字符串,另一端接收時還原并用對應的構造函數構造對應的實例即可。
前言
前段時間一直在忙一個基于WebRTC的PC和移動端雙向視頻的項目。第一次接觸webRTC,難免遇到了許多問題,比如:webRTC移動端兼容性檢測,如何配置MediaStreamConstraints, 信令(iceCandidate, sessionDescription)傳輸方式的選擇,iceCandidate和sessionDescription設置的先后順序,STUN和TURN的概念,如何實現截圖及錄制視頻及上傳圖片和視頻功能,如何高效跟蹤錯誤等等。好記性不如爛筆頭,特寫此文以記之。
移動端兼容性對PC端來說,webRTC早已被各大瀏覽器支持了,Chrome 28,FF22,Edge...隨著不久之前發布的IOS11也宣布支持webRTC及getUserMedia,webRTC在移動端的應用前景也令人憧憬。
具體到實際項目中,經過測試,各大國產安卓手機自帶的瀏覽器基本不支持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。為了便于傳輸可將其處理成字符串,另一端接收時還原并用對應的構造函數構造對應的實例即可。
webRTC并沒有規定信令的傳輸方式,而是完全由開發者自定義。常見的方式有短輪詢、webSocket(socket.io等),短輪詢的優點無非是簡單,兼容性強,但在并發量較大時,服務器負荷會很重。而webSocket就不存在這個問題,但webSocket搭建起來較為復雜,并不是所有的瀏覽器都支持websocket。綜合來說socket.io是個不錯的解決方案,事件機制和自帶的房間概念對撮合視頻會話都是天然有利的,并且當瀏覽器不支持websocket時可以切換為輪詢,也解決了兼容性的問題。
發起視頻會話的流程
可以看到無論是發起方還是接受方,第一步都是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。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92664.html
摘要:隨著通信的發展,實時音視頻服務將進一步覆蓋更多的生活場景。什么是實時通訊,我們很容易把和混淆。另外的延遲是毫秒級,在正常的網絡情況下,延遲在之間,可以多方通話實時互動。這篇文章主要是圍繞告訴大家什么是,能解決什么問題的普及貼。2020年初爆發的疫情,催生了在線教育、視頻會議、遠程醫療等實時音視頻應用的大規模增長,也使得服務于這些場景背后的底層框架RTC技術站上了風口。早在 2010 年,Go...
摘要:譯年你不能錯過的類庫后端掘金各位讀者好,這篇文章是在我看過的一篇介紹文后,整理出來的。上線后平穩運行我的后端書架后端掘金我的后端書架月前本書架主要針對后端開發與架構。 【譯】2017 年你不能錯過的 Java 類庫 - 后端 - 掘金各位讀者好, 這篇文章是在我看過 Andres Almiray 的一篇介紹文后,整理出來的。 因為內容非常好,我便將它整理成參考列表分享給大家, 同時附上...
閱讀 1561·2023-04-25 15:50
閱讀 1304·2021-09-22 15:49
閱讀 2930·2021-09-22 15:06
閱讀 3567·2019-08-30 15:54
閱讀 2331·2019-08-29 11:33
閱讀 2117·2019-08-23 17:56
閱讀 2144·2019-08-23 17:06
閱讀 1293·2019-08-23 15:55