歡迎關注個人知乎專欄:https://zhuanlan.zhihu.com/starkwangjavascript
在傳統的 Web 應用中,瀏覽器與瀏覽器之間是沒法直接相互通訊的,必須藉助服務器的幫助,可是隨着 WebRTC 在各大瀏覽器中的普及,這一現狀獲得了改變。css
WebRTC(Web Real-Time Communication,Web實時通訊),是一個支持網頁瀏覽器之間進行實時數據傳輸(包括音頻、視頻、數據流)的技術,谷歌於2011年5月開放了工程的源代碼,目前在各大瀏覽器的最新版本中都獲得了不一樣程度的支持。html
這篇文章裏咱們採用 WebRTC 來構建一個簡單的視頻傳輸應用。java
傳統的視頻推流的技術實現通常是這樣的:客戶端採集視頻數據,推流到服務器上,服務器再根據具體狀況將視頻數據推送到其餘客戶端上。web
可是 WebRTC 卻大相徑庭,它能夠在客戶端之間直接搭建基於 UDP 的數據通道,通過簡單的握手流程以後,能夠在不一樣設備的兩個瀏覽器內直接傳輸任意數據。瀏覽器
這其中的流程包括:安全
採集視頻流數據,建立一個 RTCPeerConnection服務器
建立一個 SDP offer 和相應的迴應websocket
爲雙方找到 ICE 候選路徑網絡
成功建立一個 WebRTC 鏈接
下面咱們介紹這其中涉及到的一些關鍵詞:
RTCPeerConnection
對象是 WebRTC API 的入口,它負責建立、維護一個 WebRTC 鏈接,以及在這個鏈接中的數據傳輸。目前新版本的瀏覽器大都支持了這一對象,可是因爲目前 API 還不穩定,因此須要加入各個瀏覽器內核的前綴,例如 Chrome 中咱們使用 webkitRTCPeerConnection
來訪問它。
爲了鏈接到其餘用戶,咱們必需要對其餘用戶的設備狀況有所瞭解,好比音頻視頻的編碼解碼器、使用何種編碼格式、使用何種網絡、設備的數據處理能力,因此咱們須要一張「名片」來得到用戶的全部信息,而 SDP 爲咱們提供了這些功能。
一個 SDP 的握手由一個 offer 和一個 answer 組成。
通訊的兩側可能會處於不一樣的網絡環境中,有時會存在好幾層的訪問控制、防火牆、路由跳轉,因此咱們須要一種方法在複雜的網絡環境中找到對方,而且鏈接到相應的目標。WebRTC 使用了集成了 STUN、TURN 的 ICE 來進行雙方的數據通訊。
首先咱們的目標是在同一個頁面中建立兩個實時視頻,一個的數據直接來自你的攝像頭,另外一個的數據來自本地建立的 WebRTC 鏈接。看起來是這樣的:
圖圖圖。。。。。。。
首先咱們建立一個簡單的 HTML 頁面,含有兩個 video
標籤:
<!DOCTYPE html> <html> <head> <title></title> <style type="text/css"> #theirs{ position: absolute; top: 100px; left: 100px; width: 500px; } #yours{ position: absolute; top: 120px; left: 480px; width: 100px; z-index: 9999; border:1px solid #ddd; } </style> </head> <body> <video id="yours" autoplay></video> <video id="theirs" autoplay></video> </body> <script type="text/javascript" src="./main.js"></script> </html>
下面咱們建立一個 main.js
文件,先封裝一下各瀏覽器的 userMedia
和 RTCPeerConnection
對象:
function hasUserMedia() { navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; return !!navigator.getUserMedia; } function hasRTCPeerConnection() { window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection; return !!window.RTCPeerConnection; }
而後咱們須要瀏覽器調用系統的攝像頭 API getUserMedia
得到媒體流,注意要打開瀏覽器的攝像頭限制。Chrome因爲安全的問題,只能在 https 下或者 localhost 下打開攝像頭。
var yourVideo = document.getElementById("yours"); var theirVideo = document.getElementById("theirs"); var yourConnection, theirConnection; if (hasUserMedia()) { navigator.getUserMedia({ video: true, audio: false }, stream => { yourVideo.src = window.URL.createObjectURL(stream); if (hasRTCPeerConnection()) { // 稍後咱們實現 startPeerConnection startPeerConnection(stream); } else { alert("沒有RTCPeerConnection API"); } }, err => { console.log(err); } ) }else{ alert("沒有userMedia API") }
沒有意外的話,如今應該能在頁面中看到一個視頻了。
下一步是實現 startPeerConnection
方法,創建傳輸視頻數據所須要的 ICE 通訊路徑,這裏咱們以 Chrome 爲例:
function startPeerConnection(stream) { //這裏使用了幾個公共的stun協議服務器 var config = { 'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }] }; yourConnection = new RTCPeerConnection(config); theirConnection = new RTCPeerConnection(config); yourConnection.onicecandidate = function(e) { if (e.candidate) { theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate)); } } theirConnection.onicecandidate = function(e) { if (e.candidate) { yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate)); } } }
咱們使用這個函數建立了兩個鏈接對象,在 config
裏,你能夠任意指定 ICE 服務器,雖然有些瀏覽器內置了默認的 ICE 服務器,能夠不用配置,但仍是建議加上這些配置。下面,咱們進行 SDP 的握手。
因爲是在同一頁面中進行的通訊,因此咱們能夠直接交換雙方的 candidate
對象,但在不一樣頁面中,可能須要一個額外的服務器協助這個交換流程。
瀏覽器爲咱們封裝好了相應的 Offer 和 Answer 方法,咱們能夠直接使用。
function startPeerConnection(stream) { var config = { 'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }] }; yourConnection = new RTCPeerConnection(config); theirConnection = new RTCPeerConnection(config); yourConnection.onicecandidate = function(e) { if (e.candidate) { theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate)); } } theirConnection.onicecandidate = function(e) { if (e.candidate) { yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate)); } } //本方產生了一個offer yourConnection.createOffer().then(offer => { yourConnection.setLocalDescription(offer); //對方接收到這個offer theirConnection.setRemoteDescription(offer); //對方產生一個answer theirConnection.createAnswer().then(answer => { theirConnection.setLocalDescription(answer); //本方接收到一個answer yourConnection.setRemoteDescription(answer); }) }); }
和 ICE 的鏈接同樣,因爲咱們是在同一個頁面中進行 SDP 的握手,因此不須要藉助任何其餘的通訊手段來交換 offer 和 answer,直接賦值便可。若是須要在兩個不一樣的頁面中進行交換,則須要藉助一個額外的服務器來協助,能夠採用 websocket 或者其它手段進行這個交換過程。
如今咱們已經有了一個可靠的視頻數據傳輸通道了,下一步只須要向這個通道加入數據流便可。WebRTC 直接爲咱們封裝好了加入視頻流的接口,當視頻流添加時,另外一方的瀏覽器會經過 onaddstream
來告知用戶,通道中有視頻流加入。
yourConnection.addStream(stream); theirConnection.onaddstream = function(e) { theirVideo.src = window.URL.createObjectURL(e.stream); }
如下是完整的 main.js
代碼:
function hasUserMedia() { navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; return !!navigator.getUserMedia; } function hasRTCPeerConnection() { window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection; return !!window.RTCPeerConnection; } var yourVideo = document.getElementById("yours"); var theirVideo = document.getElementById("theirs"); var yourConnection, theirConnection; if (hasUserMedia()) { navigator.getUserMedia({ video: true, audio: false }, stream => { yourVideo.src = window.URL.createObjectURL(stream); if (hasRTCPeerConnection()) { startPeerConnection(stream); } else { alert("沒有RTCPeerConnection API"); } }, err => { console.log(err); }) } else { alert("沒有userMedia API") } function startPeerConnection(stream) { var config = { 'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }] }; yourConnection = new RTCPeerConnection(config); theirConnection = new RTCPeerConnection(config); yourConnection.onicecandidate = function(e) { if (e.candidate) { theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate)); } } theirConnection.onicecandidate = function(e) { if (e.candidate) { yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate)); } } theirConnection.onaddstream = function(e) { theirVideo.src = window.URL.createObjectURL(e.stream); } yourConnection.addStream(stream); yourConnection.createOffer().then(offer => { yourConnection.setLocalDescription(offer); theirConnection.setRemoteDescription(offer); theirConnection.createAnswer().then(answer => { theirConnection.setLocalDescription(answer); yourConnection.setRemoteDescription(answer); }) }); }