使用WebRTC搭建前端視頻聊天室——信令篇

  • 衆所周知,瀏覽器自己不支持相互之間直接創建信道進行通訊,都是經過服務器進行中轉。好比如今有兩個客戶端,甲和乙,他們倆想要通訊,首先須要甲和服務器、乙和服務器之間創建信道。甲給乙發送消息時,甲先將消息發送到服務器上,服務器對甲的消息進行中轉,發送到乙處,反過來也是同樣。這樣甲與乙之間的一次消息要經過兩段信道,通訊的效率同時受制於這兩段信道的帶寬。同時這樣的信道並不適合數據流的傳輸,如何創建瀏覽器之間的點對點傳輸,一直困擾着開發者。WebRTC應運而生javascript

    WebRTC是一個開源項目,旨在使得瀏覽器能爲實時通訊(RTC)提供簡單的JavaScript接口。說的簡單明瞭一點就是讓瀏覽器提供JS的即時通訊接口。這個接口所創立的信道並非像WebSocket同樣,打通一個瀏覽器與WebSocket服務器之間的通訊,而是經過一系列的信令,創建一個瀏覽器與瀏覽器之間(peer-to-peer)的信道,這個信道能夠發送任何數據,而不須要通過服務器。而且WebRTC經過實現MediaStream,經過瀏覽器調用設備的攝像頭、話筒,使得瀏覽器之間能夠傳遞音頻和視頻html

    WebRTC已經在咱們的瀏覽器中

    這麼好的功能,各大瀏覽器廠商天然不會置之不理。如今WebRTC已經能夠在較新版的Chrome、Opera和Firefox中使用了,著名的瀏覽器兼容性查詢網站caniuse上給出了一份詳盡的瀏覽器兼容狀況html5

    這裏寫圖片描述

    另外根據36Kr前段時間的新聞Google 推出支持 WebRTC 及 Web Audio 的 Android 版 Chrome 29Android 版 Opera 開始支持 WebRTC,容許用戶在沒有任何插件的狀況下實現語音和視頻聊天java

    三個接口


    WebRTC實現了三個API,分別是:node

    • MediaStream:經過MediaStream的API可以經過設備的攝像頭及話筒得到視頻、音頻的同步流
    • RTCPeerConnection:RTCPeerConnection是WebRTC用於構建點對點之間穩定、高效的流傳輸的組件
    • RTCDataChannel:RTCDataChannel使得瀏覽器之間(點對點)創建一個高吞吐量、低延時的信道,用於傳輸任意數據

    這裏大體上介紹一下這三個APIpython

    MediaStream(getUserMedia)

    MediaStream API爲WebRTC提供了從設備的攝像頭、話筒獲取視頻、音頻流數據的功能git

    W3C標準

    W3C標準傳送門github

    如何調用

    同門能夠經過調用navigator.getUserMedia(),這個方法接受三個參數:web

    1. 一個約束對象(constraints object),這個後面會單獨講
    2. 一個調用成功的回調函數,若是調用成功,傳遞給它一個流對象
    3. 一個調用失敗的回調函數,若是調用失敗,傳遞給它一個錯誤對象

    瀏覽器兼容性

    因爲瀏覽器實現不一樣,他們常常會在實現標準版本以前,在方法前面加上前綴,因此一個兼容版本就像這樣express

       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    var getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);

    一個超級簡單的例子


    這裏寫一個超級簡單的例子,用來展示getUserMedia的效果:

       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    <!doctype html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>GetUserMedia實例</title> </head> <body> <video id="video" autoplay></video> </body> <script type="text/javascript"> var getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); getUserMedia.call(navigator, { video: true, audio: true }, function(localMediaStream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(localMediaStream); video.onloadedmetadata = function(e) { console.log("Label: " + localMediaStream.label); console.log("AudioTracks" , localMediaStream.getAudioTracks()); console.log("VideoTracks" , localMediaStream.getVideoTracks()); }; }, function(e) { console.log('Reeeejected!', e); }); </script> </html>

    將這段內容保存在一個HTML文件中,放在服務器上。用較新版本的Opera、Firefox、Chrome打開,在瀏覽器彈出詢問是否容許訪問攝像頭和話筒,選贊成,瀏覽器上就會出現攝像頭所拍攝到的畫面了

    注意,HTML文件要放在服務器上,不然會獲得一個NavigatorUserMediaError的錯誤,顯示PermissionDeniedError,最簡單方法就是cd到HTML文件所在目錄下,而後python -m SimpleHTTPServer(裝了python的話),而後在瀏覽器中輸入http://localhost:8000/{文件名稱}.html

    這裏使用getUserMedia得到流以後,須要將其輸出,通常是綁定到video標籤上輸出,須要使用window.URL.createObjectURL(localMediaStream)來創造能在video中使用src屬性播放的Blob URL,注意在video上加入autoplay屬性,不然只能捕獲到一張圖片

    流建立完畢後能夠經過label屬性來得到其惟一的標識,還能夠經過getAudioTracks()getVideoTracks()方法來得到流的追蹤對象數組(若是沒有開啓某種流,它的追蹤對象數組將是一個空數組)

    約束對象(Constraints)

    約束對象能夠被設置在getUserMedia()和RTCPeerConnection的addStream方法中,這個約束對象是WebRTC用來指定接受什麼樣的流的,其中能夠定義以下屬性:

    • video: 是否接受視頻流
    • audio:是否接受音頻流
    • MinWidth: 視頻流的最小寬度
    • MaxWidth:視頻流的最大寬度
    • MinHeight:視頻流的最小高度
    • MaxHiehgt:視頻流的最大高度
    • MinAspectRatio:視頻流的最小寬高比
    • MaxAspectRatio:視頻流的最大寬高比
    • MinFramerate:視頻流的最小幀速率
    • MaxFramerate:視頻流的最大幀速率

    詳情見這裏寫連接內容

    RTCPeerConnection


    WebRTC使用RTCPeerConnection來在瀏覽器之間傳遞流數據,這個流數據通道是點對點的,不須要通過服務器進行中轉。可是這並不意味着咱們能拋棄服務器,咱們仍然須要它來爲咱們傳遞信令(signaling)來創建這個信道。WebRTC沒有定義用於創建信道的信令的協議:信令並非RTCPeerConnection API的一部分

    信令

    既然沒有定義具體的信令的協議,咱們就能夠選擇任意方式(AJAX、WebSocket),採用任意的協議(SIP、XMPP)來傳遞信令,創建信道,好比我寫的demo,就是用的node的ws模塊,在WebSocket上傳遞信令

    須要信令來交換的信息有三種:
    * session的信息:用來初始化通訊還有報錯
    * 網絡配置:好比IP地址和端口啥的
    * 媒體適配:發送方和接收方的瀏覽器可以接受什麼樣的編碼器和分辨率

    這些信息的交換應該在點對點的流傳輸以前就所有完成,一個大體的架構圖以下:
    這裏寫圖片描述

    經過服務器創建信道

    這裏再次重申,就算WebRTC提供瀏覽器之間的點對點信道進行數據傳輸,可是創建這個信道,必須有服務器的參與。WebRTC須要服務器對其進行四方面的功能支持:

    1. 用戶發現以及通訊
    2. 信令傳輸
    3. NAT/防火牆穿越
    4. 若是點對點通訊創建失敗,能夠做爲中轉服務器

    NAT/防火牆穿越技術

    創建點對點信道的一個常見問題,就是NAT穿越技術。在處於使用了NAT設備的私有TCP/IP網絡中的主機之間須要創建鏈接時須要使用NAT穿越技術。以往在VoIP領域常常會遇到這個問題。目前已經有不少NAT穿越技術,但沒有一項是完美的,由於NAT的行爲是非標準化的。這些技術中大多使用了一個公共服務器,這個服務使用了一個從全球任何地方都能訪問獲得的IP地址。在RTCPeeConnection中,使用ICE框架來保證RTCPeerConnection能實現NAT穿越

    ICE,全名叫交互式鏈接創建(Interactive Connectivity Establishment),一種綜合性的NAT穿越技術,它是一種框架,能夠整合各類NAT穿越技術如STUN、TURN(Traversal Using Relay NAT 中繼NAT實現的穿透)。ICE會先使用STUN,嘗試創建一個基於UDP的鏈接,若是失敗了,就會去TCP(先嚐試HTTP,而後嘗試HTTPS),若是依舊失敗ICE就會使用一箇中繼的TURN服務器。

    咱們可使用Google的STUN服器:stun:stun.l.google.com:19302,因而乎,一個整合了ICE框架的架構應該長這個樣子
    這裏寫圖片描述

    瀏覽器兼容

    仍是前綴不一樣的問題,採用和上面相似的方法:

       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    var PeerConnection = (window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection);

    建立和使用

       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    //使用Google的stun服務器 var iceServer = { "iceServers": [{ "url": "stun:stun.l.google.com:19302" }] }; //兼容瀏覽器的getUserMedia寫法 var getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); //兼容瀏覽器的PeerConnection寫法 var PeerConnection = (window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection); //與後臺服務器的WebSocket鏈接 var socket = __createWebSocketChannel(); //建立PeerConnection實例 var pc = new PeerConnection(iceServer); //發送ICE候選到其餘客戶端 pc.onicecandidate = function(event){ socket.send(JSON.stringify({ "event": "__ice_candidate", "data": { "candidate": event.candidate } })); }; //若是檢測到媒體流鏈接到本地,將其綁定到一個video標籤上輸出 pc.onaddstream = function(event){ someVideoElement.src = URL.createObjectURL(event.stream); }; //獲取本地的媒體流,並綁定到一個video標籤上輸出,而且發送這個媒體流給其餘客戶端 getUserMedia.call(navigator, { "audio": true, "video": true }, function(stream){ //發送offer和answer的函數,發送本地session描述 var sendOfferFn = function(desc){ pc.setLocalDescription(desc); socket.send(JSON.stringify({ "event": "__offer", "data": { "sdp": desc } })); }, sendAnswerFn = function(desc){ pc.setLocalDescription(desc); socket.send(JSON.stringify({ "event": "__answer", "data": { "sdp": desc } })); }; //綁定本地媒體流到video標籤用於輸出 myselfVideoElement.src = URL.createObjectURL(stream); //向PeerConnection中加入須要發送的流 pc.addStream(stream); //若是是發送方則發送一個offer信令,不然發送一個answer信令 if(isCaller){ pc.createOffer(sendOfferFn); } else { pc.createAnswer(sendAnswerFn); } }, function(error){ //處理媒體流建立失敗錯誤 }); //處理到來的信令 socket.onmessage = function(event){ var json = JSON.parse(event.data); //若是是一個ICE的候選,則將其加入到PeerConnection中,不然設定對方的session描述爲傳遞過來的描述 if( json.event === "__ice_candidate" ){ pc.addIceCandidate(new RTCIceCandidate(json.data.candidate)); } else { pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp)); } };

    實例

    因爲涉及較爲複雜靈活的信令傳輸,故這裏不作簡短的實例,能夠直接移步到最後

    RTCDataChannel


    既然能創建點對點的信道來傳遞實時的視頻、音頻數據流,爲何不能用這個信道傳一點其餘數據呢?RTCDataChannel API就是用來幹這個的,基於它咱們能夠在瀏覽器之間傳輸任意數據。DataChannel是創建在PeerConnection上的,不能單獨使用

    使用DataChannel

    咱們可使用channel = pc.createDataCHannel(「someLabel」);來在PeerConnection的實例上建立Data Channel,並給與它一個標籤

    DataChannel使用方式幾乎和WebSocket同樣,有幾個事件:

    • onopen
    • onclose
    • onmessage
    • onerror

    同時它有幾個狀態,能夠經過readyState獲取:

    • connecting: 瀏覽器之間正在試圖創建channel
    • open:創建成功,可使用send方法發送數據了
    • closing:瀏覽器正在關閉channel
    • closed:channel已經被關閉了

    兩個暴露的方法:

    • close(): 用於關閉channel
    • send():用於經過channel向對方發送數據

    經過Data Channel發送文件大體思路

    JavaScript已經提供了File API從input[ type= ‘file’]的元素中提取文件,並經過FileReader來將文件的轉換成DataURL,這也意味着咱們能夠將DataURL分紅多個碎片來經過Channel來進行文件傳輸

    一個綜合的Demo


    SkyRTC-demo,這是我寫的一個Demo。創建一個視頻聊天室,並可以廣播文件,固然也支持單對單文件傳輸,寫得還很粗糙,後期會繼續完善

    使用方式

    1. 下載解壓並cd到目錄下
    2. 運行npm install安裝依賴的庫(express, ws, node-uuid)
    3. 運行node server.js,訪問localhost:3000,容許攝像頭訪問
    4. 打開另外一臺電腦,在瀏覽器(Chrome和Opera,還未兼容Firefox)打開{server所在IP}:3000,容許攝像頭和話筒訪問
    5. 廣播文件:在左下角選定一個文件,點擊「發送文件」按鈕
    6. 廣播信息:左下角input框輸入信息,點擊發送
    7. 可能會出錯,注意F12對話框,通常F5能解決

    功能

    視頻音頻聊天(鏈接了攝像頭和話筒,至少要有攝像頭),廣播文件(可單獨傳播,提供API,廣播就是基於單獨傳播實現的,可同時傳播多個,小文件還好說,大文件坐等內存吃光),廣播聊天信息

    參考資料

相關文章
相關標籤/搜索