一塊兒來學習 WebRTC (篇一)| 掘金技術徵文

前言

做爲一個認爲啥都想懂一點的小開發,一直都對WebRTC很感興趣,這個興趣來源於幾年前公司但願作一個即時通信的小功能在APP上,不過最終因爲項目最終需求更改而擱置。雖然如此,可是我仍是瞭解了一些關於該技術的技術背景,例如P2P通信、內網打洞等等。經過幾個晚上的學習和實驗,大致上瞭解WebRTC的原理和使用方法,如今分享一下個人學習過程吧。javascript

準備工做

做爲一個文檔黨,歷來都要先看官方文檔和文章,這樣才能保證本身拿到最新,最好的一手信息。WebRTC官網文檔也還算是比較全面,不過貌似都很久沒更新了。推測是,大概好久沒有作功能升級了吧。我此次學習,參考了一些官方例子,加上了本身的理解。有錯誤的地方你們能夠指出來呀,一塊兒學習。參考的文章會在文章結尾加上。廢話很少說了,開始吧。html

打開咱們的攝像頭

WebRTC是谷歌開發的,目標是創造一個高質量的、可靠的通信框架,從字面的意咱們能夠拆分爲了WebRTC兩部分,Web很好理解啊,就是基於網絡,而RTC全稱爲Real Time Communications(實時通信),所以它的做用就是讓咱們能夠利用瀏覽器(也能用於APP),進行實時的通信的一個框架。既然是通信媒介固然是多種的,包括視頻,語音,文本等多種多媒體信息,甚至你還能利用它來傳輸各類文件。下面,咱們用最直觀的,視頻通信來開始咱們的學習吧。前端

用瀏覽器打開攝像頭很簡單,咱們能夠直接調用JS API 實現。java

  • HTML
<!DOCTYPE html>
<html lang="en">
<head>
    ...
</head>
<body>
    <h1>得到視頻流</h1>

    <!-- 設置自動播放 -->
    <video autoplay playsinline></video>
    <script src="js/main.js"></script>
</body>
</html>
複製代碼
  • JavaScript
// 媒體流配置
const mediaStreamConstraints = {
    video: true
};

// 得到 video 標籤元素
const localVideo = document.querySelector("video");

// 媒體流對象
let localStream;

// 回調保存視頻流對象並把流傳到 video 標籤
function gotLocalMediaStream(mediaStream) {
    localStream = mediaStream;
    localVideo.srcObject = mediaStream;
}

// handle 錯誤信息
function handleLocalMediaStreamError(error) {
    console.log("打開本地視頻流錯誤: ", error)
}

// fire!!
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
    .then(gotLocalMediaStream)
    .catch(handleLocalMediaStreamError);
複製代碼

代碼主要分2步git

  1. navigator.mediaDevices.getUserMedia 中得到視頻設備。
  2. then 的回調中把視頻流傳到 video 標籤。

很是簡單吧github

得到攝像頭

值得注意的是,我用的是Chrome 瀏覽器,新版本的Chrome增強了獲取設備的安全策略。若是你想要打開攝像頭等設備,你的域名若是不是本地文件或者 localhost 那必須經過https 訪問。web

使用 RTC 進行 P2P 傳輸

既然視頻流咱們獲得了,第二步,咱們來使用WebRTCRTCPeerConnection 來進行本地傳輸吧。這個Demo 不是真實的使用場景,由於不涉及到真實世界的網絡傳輸,咱們僅僅是在同一個頁面,打開了兩個 RTCPeerConnection 把一個的內容傳輸到另外一個,從而進行通信。在貼代碼以前,咱們先來簡單的描述一下建立鏈接的過程吧。瀏覽器

假設如今是A想跟B視頻。他們的 offer/answer (申請?/ 應答?), 機制是這樣的:安全

1. `A `建立了一個 `RTCPeerConnection` 對象

2. `A` 利用`RTCPeerConnection` 的 `createOffer()` 方法建立了一個 `offer` (一個` SDP` 的會話描述)

3. `A` 在 `offer` 的回調中使用 `setLocalDescription()` 方法存儲他的 `offer` 

4. `A` 把他的 `offer` 字符串化,而後經過某一種信令機制發給 `B`

5. `B` 收到 `A` 的 `offer` 後用`setRemoteDescription()` 存起來,如此一來他的 `RTCPeerConnection` 就知道了 `A` 的配置。

6. `B` 調用 `createAnswer()` 並用他的成功回調的傳送他的本地會話描述:這就是 `B` 的`answer`

7. `B` 用 `setLocalDescription()` 設置了他的 `answer` 到本地的會話描述

8. 而後 `B` 用某一種信令機制把他的 `answer` 字符串化以後返回給 `A`

9. `A` 把 `B` 的 `answer` 利用`setRemoteDescription()`方法存取爲遠程會話描述
複製代碼

過程看上去很麻煩,不過其實他們就作了個事情服務器

  1. 建立會話描述(SDP
  2. 交換會話描述(SDP
  3. 存儲本身跟對方的會話描述

有關 SDP的格式,能夠參看文章後面的連接

下面讓咱們看代碼,走起

  • HTML
<!DOCTYPE html>
<html lang="en">
<head>
    ...
</head>
<body>
    <h1>RTCPeerConnection 傳輸視頻流</h1>
    <!-- 設置自動播放 -->
    <video autoplay playsinline id="localVideo"></video>
    <video autoplay playsinline id="remoteVideo"></video>
    <div>
        <button id="startBtn">開始</button>
        <button id="callBtn">撥打</button>
        <button id="hangupBtn">掛機</button>
    </div>

    <!-- 墊片,用於統一瀏覽器 API -->
    <script src="js/adapter.js"></script>
    <script src="js/main.js"></script>
</body>
</html>
複製代碼

HTML 代碼比較簡單,咱們建立了兩個 video,一個顯示遠程一個顯示本地,而且加入了三個按鈕進行模擬撥打。細心的同窗可能已經發現了,咱們引入了一個墊片adapter.js。常常寫前端的同窗對墊片可能熟悉不過了,由於世界上不只僅只有谷歌的瀏覽器,還有各類各樣別的。而後命名,API也是各類各樣,因此咱們會利用各類墊片,統一咱們的API。再也不忍受兼容之苦。adapter.js就是這樣的存在。他是谷歌官方提供給咱們的。引入它咱們即可以用統一套API操做。

  • JavaScript

因爲代碼比較長,就只貼關鍵代碼了。所有代碼連接我會在文章後面貼上。

// 開始按鈕,打開本地媒體流
function startAction() {
    startButton.disabled = true;
    navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
        .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
    trace('本地媒體流打開中...');
}
複製代碼

這是響應開始按鈕的函數。跟第一個例子同樣,主要是用來打開攝像頭,而且把視頻流傳到idlocalVideo的視頻標籤。

// 撥打按鈕, 建立 peer connection
function callAction() {
    callButton.disabled = true;
    hangupButton.disabled = false;

    trace("開始撥打...");
    startTime = window.performance.now();
    
    // ...

    const servers = null;  // RTC 服務器配置

    // 建立 peer connetcions 並添加事件
    localPeerConnection = new RTCPeerConnection(servers);
    trace("建立本地 peer connetcion 對象");

    localPeerConnection.addEventListener('icecandidate', handleConnection);
    localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);

    remotePeerConnection = new RTCPeerConnection(servers);
    trace("建立遠程 peer connetcion 對象");

    remotePeerConnection.addEventListener('icecandidate', handleConnection);
    remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
    remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);

    // 添加本地流到鏈接中並建立鏈接
    localPeerConnection.addStream(localStream);
    trace("添加本地流到本地 PeerConnection");

    trace("開始建立本地 PeerConnection offer");
    localPeerConnection.createOffer(offerOptions)
        .then(createdOffer).catch(setSessionDescriptionError);
}
複製代碼

這部份是撥打按鈕的響應函數。在這個方法中,咱們作了個事情。

  1. 建立了用於通信的一對RTCPeerConnection對象,localPeerConnectionremotePeerConnection

  2. 分別給兩個RTCPeerConnection對象註冊了icecandidate(重要)iceconnectionstatechange 事件的響應函數

  3. remotePeerConnection註冊了addstream事件的響應。

  4. 把本地視頻流添加到localPeerConnection

  5. localPeerConnection建立offer

這裏有一個上面沒有說起的東西ICE CandidateICE是啥呢?哈哈,他的全稱是 Interactive Connectivity Establishment交互式鏈接的創建。他是一個規範,說白了就是創建鏈接用的規範,因爲咱們的WebRTC是要進行P2P鏈接的,而咱們的網絡是很是複雜的,並且大部分都是在內網(須要打洞或者穿越防火牆)。因此咱們須要一個機制來創建內網鏈接。這個我會在後面的文章詳細來講說。如今,簡單理解成就是創建鏈接用的就行了。而icecandidate 的響應方法,則是當網絡可用的狀況下,用於存儲和交換各類網絡信息。

// 定義 RTC peer connection
function handleConnection(event) {
    const peerConnection = event.target;
    const iceCandidate = event.candidate;

    if (iceCandidate) {
        const newIceCanidate = new RTCIceCandidate(iceCandidate);
        const otherPeer = getOtherPeer(peerConnection);

        otherPeer.addIceCandidate(newIceCanidate)
            .then(() => {
                handleConnectionSuccess(peerConnection);
            }).catch((error) => {
             handleConnectionFailure(peerConnection, error);
            });

        trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
            `${event.candidate.candidate}.`);
    }
}
複製代碼

這段代碼正是體現了網絡信息(ICE candidate),的保存和交換過程。而保存Candidate是經過調用RTCPeerConnection對象的addIceCandidate方法。這裏可能你們有疑問,這裏就交換了Candidate信息了嗎?是的getOtherPeer方法其實就是用於得到對方的RTCPeerConnection對象,由於咱們的 Demo 是在同一頁面建立的。因此不需經過其餘載體交換。

好的,說完鏈接建立,咱們接着說建立offer。在建立offer前,咱們已經留意到,其實已經把本地的視頻流添加到RTCPeerConnection對象中了,所以offer所帶的SDP會話描述,已經帶有相關信息。咱們先來createOffer 成功後的回調方法。

// 建立 offer
function createdOffer(description) {
    trace(`Offer from localPeerConnection:\n${description.sdp}`);

    trace('localPeerConnection setLocalDescription 開始.');
    localPeerConnection.setLocalDescription(description)
        .then(() => {
            setLocalDescriptionSuccess(localPeerConnection);
        }).catch(setSessionDescriptionError);

    trace('remotePeerConnection setRemoteDescription 開始.');
    remotePeerConnection.setRemoteDescription(description)
        .then(() => {
            setRemoteDescriptionSuccess(remotePeerConnection);
        }).catch(setSessionDescriptionError);

    trace('remotePeerConnection createAnswer 開始.');
    remotePeerConnection.createAnswer()
        .then(createdAnswer)
}
   
複製代碼

簡單明瞭,對於localPeerConnection來講是本地,因此就是調用 setLocalDescriptionoffer信息存儲。而對於對方就是遠程remotePeerConnection就是用setRemoteDescription進行存儲了。這裏跟我章節前說的第4步說的不同,這裏沒有轉成字符串。聰明的同窗可能猜到爲何了,由於這裏是同一個頁面,不須要傳輸呀。

緊接着立刻remotePeerConnection就調用createAnswer建立了一個 answer,讓咱們繼續看,

// 建立 answer
function createdAnswer(description) {
    trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

    trace('remotePeerConnection setLocalDescription 開始.');
    remotePeerConnection.setLocalDescription(description)
        .then(() => {
            setLocalDescriptionSuccess(remotePeerConnection);
        }).catch(setSessionDescriptionError);

    trace('localPeerConnection setRemoteDescription 開始.');
    localPeerConnection.setRemoteDescription(description)
        .then(() => {
            setRemoteDescriptionSuccess(localPeerConnection);
        }).catch(setSessionDescriptionError);
}
複製代碼

這裏跟上面的createOffer回調作的差很少,把answer存儲到雙方對應的描述中。

到這裏爲止雙方的鏈接建好,offeranswer也存儲穩當。因爲remotePeerConnection在以前已經已經註冊好addStream的響應方法了gotRemoteMediaStream,而正如前文說的,由於建立offer的時候已經把視頻流帶上了,因此gotRemoteMediaStream此刻會回調,經過這個方法,把視頻流顯示在remoteVideo標籤中。

// 回調保存遠程媒體流對象並把流傳到 video 標籤
function gotRemoteMediaStream(event) {
    const mediaStream = event.stream;
    remoteVideo.srcObject = mediaStream;
    remoteStream = mediaStream;
    trace("遠程節點連接成功,接收遠程媒體流中...");
}
複製代碼

如今,咱們應該能夠看到兩個如出一轍的畫面了。注意哦,右邊那個是經過RTC 傳輸過來的。撒花~

RTC transport

這一篇先到這裏吧,咱們下一篇繼續。下一篇會繼續繼續深刻WebRTC架構和ICEsignling之類的內容。謝謝你們的閱讀,畢竟我也是個初學者,若是文中有不對的地方,你們能夠評論一下,而後一塊兒探討。再次謝過。

代碼和參考文檔

Agora SDK 使用體驗徵文大賽 | 掘金技術徵文,徵文活動正在進行中

相關文章
相關標籤/搜索