使用WebRTC搭建前端視頻聊天室——點(diǎn)對(duì)點(diǎn)通信篇

WebRTC給我們帶來了瀏覽器中的視頻、音頻聊天體驗(yàn)。但個(gè)人認(rèn)為,它最實(shí)用的特性莫過于DataChannel——在瀏覽器之間建立一個(gè)點(diǎn)對(duì)點(diǎn)的數(shù)據(jù)通道。在DataChannel之前,瀏覽器到瀏覽器的數(shù)據(jù)傳遞通常是這樣一個(gè)流程:瀏覽器1發(fā)送數(shù)據(jù)給服務(wù)器,服務(wù)器處理,服務(wù)器再轉(zhuǎn)發(fā)給瀏覽器2。這三個(gè)過程都會(huì)帶來相應(yīng)的消耗,占用服務(wù)器帶寬不說,還減緩了消息從發(fā)送到接收的時(shí)間。其實(shí)最理想的方式就是瀏覽器1直接與瀏覽2進(jìn)行通信,服務(wù)器不需要參與其中。WebRTC DataChannel就提供了這樣一種方式。

如果對(duì)WebRTC和DataChannel不太了解的同學(xué),可以先閱讀如下文章:

WebRTC的RTCDataChannel

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

使用WebRTC搭建前端視頻聊天室——入門篇

老劉和老姚

當(dāng)然服務(wù)器完全不參與其中,顯然是不可能的,用戶需要通過服務(wù)器上存儲(chǔ)的信息,才能確定需要和誰建立連接。這里通過一個(gè)故事來講述建立連接的過程:

不如釣魚去

一些背景:

  • 老劉和老姚都住在同一個(gè)小區(qū)但不同的片區(qū),小區(qū)很破舊,沒有電話
  • 片區(qū)相互隔離且片區(qū)門口有個(gè)保安,保安只認(rèn)識(shí)自己片區(qū)的人,遇到不認(rèn)識(shí)的人就需要查詢憑證才能通過,而憑證需要找物業(yè)才能確定
  • 門衛(wèi)老大爺認(rèn)識(shí)小區(qū)里的所有人但是不知道都住哪,有什么消息都可以在出入小區(qū)的時(shí)候代為傳達(dá)

現(xiàn)在,老劉聽說老姚釣魚技術(shù)高超,想和老姚討論釣魚技巧。只要老劉和老姚相互之間知道對(duì)方的門牌號(hào)以及憑證,就可以串門了:

  1. 門衛(wèi)老大爺認(rèn)識(shí)老劉和老姚
  2. 老劉找物業(yè)確定了自己片區(qū)的出入憑證,將憑證、自己的門牌號(hào)以及意圖告訴門衛(wèi)老大爺,讓其轉(zhuǎn)交給老姚
  3. 老姚買菜歸來遇到門衛(wèi)老大爺,門衛(wèi)老大爺將老劉的消息傳達(dá)給老姚。于是老姚知道怎么去老劉家了
  4. 老姚很開心,他也找物業(yè)獲取了自己小區(qū)的憑證,并將憑證、自己的門牌號(hào)等信息交給門衛(wèi)老大爺,希望他傳達(dá)給老劉
  5. 老劉吃早餐回來遇到門衛(wèi)老大爺,老大爺把老姚的小區(qū)憑證、門牌號(hào)等信息告訴老劉,這樣老劉就知道了怎么去老姚家了

老劉和老姚相互之間知道了對(duì)方的門牌號(hào)和小區(qū)出入憑證,他們相互之間有什么需要交流的直接串門就行了,消息不再需要門衛(wèi)老大爺來代為傳達(dá)了

換個(gè)角度

我們把角色做一個(gè)映射:

  • 老劉:瀏覽器1
  • 老姚:瀏覽器2
  • 片區(qū):不同網(wǎng)段
  • 保安:防火墻
  • 片區(qū)憑證:ICE candidate
  • 物業(yè):ICE server
  • 門牌號(hào):session description
  • 門衛(wèi)老大爺:server

于是乎故事就變成了這樣:

  1. 瀏覽器1和瀏覽器2在server上注冊(cè),并保有連接
  2. 瀏覽器1從ice server獲取ice candidate并發(fā)送給server,并生成包含session description的offer,發(fā)送給server
  3. server發(fā)送瀏覽器1的offer和ice candidate給瀏覽器2
  4. 瀏覽器2發(fā)送包含session description的answer和ice candidate給server
  5. server發(fā)送瀏覽器2的answer和ice candidate給瀏覽器1

這樣,就建立了一個(gè)點(diǎn)對(duì)點(diǎn)的信道,流程如下所示:

[圖片上傳失敗...(image-9d81d3-1513063469213)]

禮物

故事

老劉和老姚已經(jīng)可以相互串門了,經(jīng)過一段時(shí)間的交流感情越來越深。老姚的親友送了20斤葡萄給老姚,老姚決定送10斤給老劉。老姚畢竟年事已高,不可能一次帶10斤。于是乎,老姚將葡萄分成了10份,每次去老劉家串門就送一份過去。

這里可以做如下類比:
1. 10斤葡萄:一個(gè)文件(盡管文件分片沒有意義,葡萄分開還可以單獨(dú)吃,但是實(shí)在找不到啥好的比喻了)
2. 分成10份:將文件分片,轉(zhuǎn)成多個(gè)chunk
3. 老姚一次只能帶一斤:datachannel每次傳輸?shù)臄?shù)據(jù)量不宜太大(找到最合適的大小

這其實(shí)就是通過datachannel傳輸文件的方式,首先將文件分片,然后逐個(gè)發(fā)送,最后再統(tǒng)一的進(jìn)行組合成一個(gè)新的文件

分片

通過HTML5的File API可以將type為file的input選中的文件讀取出來,并轉(zhuǎn)換成data url字符串。這也就為我們提供了很方便的分片方式:

var reader = new window.FileReader(file);
reader.readAsDataURL(file);
reader.onload = function(event, text) {
    chunkify(event.target.result);//將數(shù)據(jù)分片
};

組合

通過datachannel發(fā)送的分片數(shù)據(jù),我們需要將其進(jìn)行組合,由于是data url字符串,在接收到所有包之后進(jìn)行拼接就可以了。拼接完成后就得到了一個(gè)文件完整的data url字符串,那么我們?nèi)绾螌⑦@個(gè)字符串轉(zhuǎn)換成文件呢?

方案一:直接跳轉(zhuǎn)下載

既然是個(gè)dataurl,我們直接將其賦值給window.location.href自然可以下載,但是這樣下載是沒法設(shè)定下載后的文件名的,這想一想都蛋疼

方案二:通過a標(biāo)簽下載

這個(gè)原理和跳轉(zhuǎn)下載類似,都是使用dataurl本身的特性,通過創(chuàng)建一個(gè)a標(biāo)簽,將dataurl字符串賦值給href屬性,然后使用download確定下載后的文件名,就可以完成下載了。但是很快又有新問題了,稍微大一點(diǎn)的文件下載的時(shí)候頁(yè)面崩潰了。這是因?yàn)閐ataurl有大小限制

方案三:blob

其實(shí)可以通過給a標(biāo)簽創(chuàng)建blob url的方式來進(jìn)行下載,這個(gè)沒有大小限制。但是我們手上是dataurl,所以需要先進(jìn)行轉(zhuǎn)換:

function dataURItoBlob(dataURI, dataTYPE) {
    var binary = atob(dataURI.split(',')[1]),
        array = [];
    for (var i = 0; i < binary.length; i++) array.push(binary.charCodeAt(i));
    return new Blob([new Uint8Array(array)], {
        type: dataTYPE
    });
}

獲得blob后,我們就可以通過URL API來下載了:

var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
var blob = dataURItoBlob(data, 'octet/stream');
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = filename;
a.click();
!moz && window.URL.revokeObjectURL(url);
a.parentNode.removeChild(a);

這里有幾個(gè)點(diǎn):
1. datachannel其實(shí)是可以直接傳送blob的,但是只有ff支持,所以傳data url
2. chrome下載是直接觸發(fā)的,不會(huì)進(jìn)行詢問,firefox會(huì)先詢問后下載,在詢問過程中如果執(zhí)行了revokeObjectURL,下載就會(huì)取消,囧

升級(jí)

如我們所知,WebRTC最有特點(diǎn)的地方其實(shí)是可以傳輸getUserMedia獲得的視頻、音頻流,來實(shí)現(xiàn)視頻聊天。但事實(shí)上我們的使用習(xí)慣來看,一般人不會(huì)一開始就打開視頻聊天,而且視頻聊天時(shí)很消耗內(nèi)存的(32位機(jī)上一個(gè)連接至少20M左右好像,也有可能有出入)。所以常見的需求是,先建立一個(gè)包含datachannel的連接用于傳輸數(shù)據(jù),然后在需要時(shí)升級(jí)成可以傳輸視頻、音頻。

看看我們之前傳輸?shù)膕ession description,它其實(shí)來自Session Description Protocol。可以看到wiki上的介紹:

The Session Description Protocol (SDP) is a format for describing streaming media initialization parameters.

這意味著什么呢?我們之前建立datachannel是沒有加視頻、音頻流的,而這個(gè)流的描述是寫在SDP里面的?,F(xiàn)在我們需要傳輸視頻、音頻,就需要添加這些描述。所以就得重新獲得SDP,然后構(gòu)建offer和answer再傳輸一次。傳輸?shù)牧鞒毯椭耙粯?,沒什么區(qū)別。但這一次,我們不需要傳輸任何的ice candidate,這里我曾經(jīng)遇到了坑,經(jīng)過國(guó)外大大的點(diǎn)撥才明白過來。

from mattm: You do not need to send ICE candidates on an already established peer connection. The ICE candidates are to make sure the two peers can establish a connection through their potential NAT and firewalls. If you can already send data on the peer connection, ICE candidates will not do anything.

Peertc

我將datachannel和websocket組合,實(shí)現(xiàn)了一個(gè)構(gòu)建點(diǎn)對(duì)點(diǎn)連接的庫(kù)Peertc,它提供非常簡(jiǎn)潔的方式來建立連接和發(fā)送數(shù)據(jù)、文件和視頻/音頻流,詳情見github。走過路過的記得star一下哦,有什么bug也非常希望能夠提出來。

最后

WebRTC的點(diǎn)對(duì)點(diǎn)方式能夠運(yùn)用在很多場(chǎng)景:

  • 如web qq這種Web IM工具,這就不說了
  • 如象棋這種雙人對(duì)戰(zhàn)游戲,每一步的數(shù)據(jù)服務(wù)器時(shí)不關(guān)心的,所以完全可以點(diǎn)對(duì)點(diǎn)發(fā)送
  • 一對(duì)一在線面試、在線教育,這其實(shí)是即時(shí)通信的一個(gè)業(yè)務(wù)方向
  • 視頻裸(),當(dāng)我沒說
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容