WebRTC 這麼火,前端靚仔,請收下這篇入門教程

本文是針對小白的 WebRTC 快速入門課,如果你還之前還不瞭解 WebRTC,希望你能認真閱讀本文,實現對 WebRTC 的零的突破 💪。如果感興趣,不妨動動手指跟着我一起實踐下。

👉🏻 完整代碼地址 https://github.com/wang1xiang/webrtc-tutorial/tree/master/04-one-to-one

什麼是 WebRTC

WebRTC(Web Real-Time Communications)是一項實時通訊技術,它允許網絡應用或者站點,在不借助中間媒介的情況下,建立瀏覽器之間點對點(Peer-to-Peer)的連接,實現視頻流和(或)音頻流或者其他任意數據的傳輸。WebRTC 包含的這些標準使用戶在無需安裝任何插件或者第三方的軟件的情況下,創建點對點(Peer-to-Peer)的數據分享和電話會議成爲可能。

實時通信和即時通信的區別

IM 即時通信,就是通過文字聊天、語音消息發送、文件傳輸等方式通信,考慮的是 「可靠性」;

RTC 實時通信:音視頻通話、電話會議,考慮的是 「低延時」。

WebRTC 發展史

2011 年開始, Google 先後收購 GIPS 和 On2,組成 GIPS 音視頻引擎 + VPx 系列視頻編解碼器,並將其代碼開源,WebRTC 項目應運而生。

2012 年,Google 將 WebRTC 集成到 Chrome 瀏覽器中。於是我們就可以愉快的在瀏覽器之間進行音視頻通信。

當前除了 IE 之外的瀏覽器都已支持 WebRTC。

WebRTC 應用場景

WebRTC 的能力使其適用於各種實時通信場景:

  1. 點對點通訊:WebRTC 支持瀏覽器之間進行音視頻通話,例如語音通話、視頻通話等;

  2. 電話會議:WebRTC 可以支持多人音視頻會議,例如騰訊會議、釘釘會議等;

  3. 屏幕共享:WebRTC 不僅可以傳輸音視頻流,還可以用於實時共享屏幕;

  4. 直播:WebRTC 可以用於構建實時直播,用戶可以通過瀏覽器觀看直播內容。

WebRTC 組成部分

在瞭解 WebRTC 通信過程前,我們需要先來了解下 WebRTC 的組成部分,這可以幫助我們快速建立 WebRTC 的知識體系。

WebRTC 主要由三部分組成:「瀏覽器 API」、「音視頻引擎」和「網絡 IO」。

瀏覽器 API

用於 「採集攝像頭和麥克風」 生成媒體流,並處理音視頻通信相關的 「編碼、解碼、傳輸」 過程,可以使用以下 API 在瀏覽器中創建實時通信應用程序。

音視頻引擎

有了 WebRTC,我們可以很方便的實現音視頻通信;而如果沒有 WebRTC 的情況下,我們想要實現音視頻通信,就需要去了解音視頻編碼器相關技術。

WebRTC 內置了強大的音視頻引擎」,可以對媒體流進行編解碼、回聲消除、降噪、防止視頻抖動等處理,我們使用者大可不用去關心如何實現 。主要使用的音視頻編解碼器有:

還有像回聲消除AEC(Acoustic Echo Chancellor)、背景噪音抑制ANS(Automatic Noise Suppression)和Jitter buffer用來防止視頻抖動,這些問題在 WebRTC 中也提供了非常成熟、穩定的算法,並且提供圖像增加處理,例如美顏,貼圖,濾鏡處理等。

網絡 I/O

WebRTC 傳輸層用的是 「UDP」 協議,因爲音視頻傳輸對 「及時性」 要求更高,如果使用 TCP 當傳輸層協議的話,如果發生丟包的情況下,因爲 TCP 的可靠性,就會嘗試重連,如果第七次之後仍然超時,則斷開 TCP 連接。而如果第七次收到消息,那麼傳輸的延遲就會達到 2 分鐘。在延遲高的情況下,想做到正常的實時通訊顯然是不可能的,此時 TCP 的可靠性反而成了弊端。

而 UDP 則正好相反,它只負責有消息就傳輸,不管有沒有收到,這裏從底層來看是滿足 WebRTC 的需求的,所以 WebRTC 是採用 UDP 來當它的傳輸層協議的。

這裏主要用到以下幾種協議 / 技術:

除了以上三部分,WebRTC 還需要一個 「信令服務」 做會話管理,但 WebRTC 規範裏沒有包含信令協議,需要自行實現。

WebRTC 通信過程

基於以上,我們來思考下 WebRTC 實現一對一通信需要哪些基本條件?

通信過程如下:

  1. 本地(WebRTC 終端)啓動後,檢測設備可用性,如果可用後開始進行音視頻採集工作;

  2. 本地就緒後,發送 “加入房間” 信令到 Signal 服務器;

  3. Signal 服務器創建房間,等待加入;

  4. 對端(WebRTC 終端)同樣操作,加入房間,並通知另一端;

  5. 雙端創建媒體連接對象RTCPeerConnection,進行媒體協商;

  6. 雙端進行連通性測試,最終建立連接;

  7. 將採集到的音視頻數據通過RTCPeerConnection對象進行編碼,最終通過 P2P 傳送給對端 / 本地,再進行解碼、展示。

❝

第 6 步在建立連接進行 P2P 穿越時很有可能失敗。當 P2P 穿越失敗時,爲了保障音視頻數據仍然可以互通,則需要通過 TURN 服務器進行音視頻數據中轉。後面會講到 TURN 服務是什麼,以及如何搭建 TURN 服務。

❞

接下來,我們按照通信過程,來一一講解每一步要做的事情。

第一步:音視頻採集

採集音視頻數據是 WebRTC 通信的前提,我們可以使用瀏覽器提供的 getUserMedia API 進行音視頻採集。

const constraints = { video: true, audio: true }
const localStream = navigator.mediaDevices.getUserMedia(constraints)

getUserMedia 接受參數 constraints 用於指定 MediaStream 中包含哪些類型的媒體軌(音頻軌、視頻軌),並對媒體軌做設置(如設置視頻的寬高、幀率等)。

返回一個 promise 對象,成功後會獲得流媒體對象 MediaStream(包含從音視頻設備中獲取的音視頻數據);使用 getUserMedia 時,瀏覽器會詢問用戶,開啓音頻和視頻權限。如果用戶拒絕或無權限時,則返回 error。

Demo 展示

通過getUserMedia成功回調拿到媒體流之後,通過將媒體流掛載到videoDOM.srcObject即可顯示在頁面上。

效果如下(帥照 🤵 自動馬賽克):

其他相關 API

MediaDeviceInfo

用於表示每個媒體輸入 / 輸出設備的信息,包含以下 4 個屬性:

可以在瀏覽器控制檯直接輸入navigator.mediaDevices.enumerateDevices()返回如下所示:

MediaDevices

該接口提供訪問連接媒體輸入的設備(如攝像頭、麥克風)以及獲取屏幕共享等方法。而我們需要獲取可用的音視頻設備列表,就是通過該接口中的方法來實現的,如前面提到的getUserMedia方法。

方法:

「WebRTC 相關的 API 需要 Https(或者 localhost)環境支持,因爲在瀏覽器上通過 HTTP 請求下來的 JavaScript 腳本是不允話訪問音視頻設備的,只有通過 HTTPS 請求的腳本才能訪問音視頻設備。」

第二 / 三 / 四步:信令交互

什麼是信令服務器

信令可以簡單理解爲消息,在協調通訊的過程中,爲了建立一個 webRTC 的通訊過程,「在通信雙方彼此連接、傳輸媒體數據之前,它們要通過信令服務器交換一些信息,如加入房間、離開房間及媒體協商」 等,而這個過程在 webRTC 裏面是沒有實現的,需要自己搭建信令服務。

使用 Node 搭建信令服務器

可以使用 Socket.io 來實現 WebRTC 信令服務器,Socket.io 已經內置了房間的概念,所以非常適合用於信令服務器的創建。

以下使用 Socket.io 的過程中需要用到的知識點:

搭建信令服務器過程如下:

  1. Socket.io 分爲服務端和客戶端兩部分。服務端由 Node.js 加載後偵聽某個服務端口;

    let app = express()
    let http_server = http.createServer(app)
    http_server.listen(80)
        
    let io = new IO(http_server, {
      path: '/',
      cors: {
        origin: '*',
      },
    })
    http_server.on('listening')
  2. 客戶端要想與服務端相連,首先要加載 Socket.io 的客戶端庫,然後調用 io.connect();

    socket = io('http://localhost:80', {
      query: { username, room },
    }).connect()
  3. 此時,服務端會接收到connection消息,在此消息中註冊接受 / 發送消息的事件;

    // 監聽連接
    io.on('connection', (socket) => {
        const { query } = socket.handshake
        // 獲取socket連接參數 username和room
        const { username, room } = query
        ...
    })
  4. 客戶端同樣註冊接受 / 發送消息的事件,雙方開始通信。

    socket.on('message', (room, data) => {
      socket.to(room).emit('message', room, data)
    })
        
    socket.on('leave', (room, username) => {
      socket.leave(room)
      socket.emit('leave', room, socket.id)
    })

最後,看一下效果如下:

順便看一下日誌信息:

第五步:RTCPeerConnection 對象 媒體協商

RTCPeerConnection 是一個由本地計算機到遠端的 WebRTC 連接,該接口提供 「創建,保持,監控,關閉連接」 的方法的實現,可以簡單理解爲功能強大的 socket 連接。

通過new RTCPeerConnection即可創建一個 RTCPeerConnection 對象,此對象主要負責與 「各端建立連接(NAT 穿越),接收、發送音視頻數據」,並保障音視頻的服務質量,接下來要說的端到端之間的媒體協商,也是基於 RTCPeerConnection 對象來實現的。

至於它是如何保障端與端之間的連通性,如何保證音視頻的服務質量,又如何確定使用的是哪個編解碼器等問題,作爲應用者的我們大可不必關心,因爲所有的這些問題都已經在 RTCPeerConnection 對象的底層實現好了 👍。

const localPc = new RTCPeerConnection(rtcConfig)
// 將音視頻流添加到 RTCPeerConnection 對象中
localStream.getTracks().forEach((track) => {
  localPc.addTrack(track, localStream)
})

❝

在第一步獲取音視頻流後,需要將流添加到創建的 RTCPeerConnection 對象中,當 RTCPeerConnection 對象獲得音視頻流後,就可以開始與對端進行媒協體協商。

❞

什麼是媒體協商

媒體協商的作用是 「找到雙方共同支持的媒體能力」,如雙方各自支持的編解碼器,音頻的參數採樣率,採樣大小,聲道數、視頻的參數分辨率,幀率等等。

就好比兩人相親,通過介紹人男的知道了女的身高、顏值、身材,女的理解了男的家庭、財富、地位,然後找到你們的共同點 “窮”,你倆覺得 “哇竟然這麼合適”,趕緊見面深入交流一下 💓。

上述說到的這些音頻 / 視頻的信息都會在 「SDP(Session Description Protocal:即使用文本描述各端的 “能力”)」 中進行描述。

❝

一對一的媒體協商大致如下:首先自己在 SDP 中記錄自己支持的音頻 / 視頻參數和傳輸協議,然後進行信令交互,交互的過程會同時傳遞 SDP 信息,另一方接收後與自己的 SDP 信息比對,並取出它們之間的交集,這個交集就是它們協商的結果,也就是它們最終使用的音視頻參數及傳輸協議。

❞

媒體協商過程

一對一通信中,發起方發送的 SDP 稱爲Offer(提議),接收方發送的 SDP 稱爲Answer(應答)。

每端保持兩個描述:描述本身的本地描述LocalDescription,描述呼叫的遠端的遠程描述RemoteDescription。

當通信雙方 RTCPeerConnection 對象創建完成後,就可以進行媒體協商了,大致過程如下:

  1. 發起方創建 Offer 類型的 SDP,保存爲本地描述後再通過信令服務器發送到對端;

  2. 接收方接收到 Offer 類型的 SDP,將 Offer 保存爲遠程描述;

  3. 接收方創建 Answer 類型的 SDP,保存爲本地描述,再通過信令服務器發送到發起方,此時接收方已知道連接雙方的配置;

  4. 發起方接收到 Answer 類型的 SDP 後保存到遠程描述,此時發起方也已知道連接雙方的配置;

  5. 整個媒體協商過程處理完畢。

更詳細的步驟請參考 MDN 中對會話描述講解。

代碼實現媒體協商過程

通過 MDN 先了解下我們需要用到的 API:

發起方創建 RTCPeerConnection

// 配置
export const rtcConfig = null
const localPc = new RTCPeerConnection(rtcConfig)

發起方 / 接收方創建 Offer 保存爲本地描述

let offer = await localPc.createOffer()
// 保存爲本地描述
await localPc.setLocalDescription(offer)
// 通過信令服務器發送到對端
socket.emit('offer', offer)

接受 Offer 後 創建 Answer 併發送

socket.on('offer', offer) => {
  // 將 Offer 保存爲遠程描述;
  remotePc = new RTCPeerConnection(rtcConfig)
  await remotePc.setRemoteDescription(offer)
  let remoteAnswer = await remotePc.createAnswer()
  await remotePc.setLocalDescription(remoteAnswer)
  socket.emit('answer', remoteAnswer)
});

接受 Answer 存儲爲遠程描述

// 4. 發起方接收到 Answer 類型的 SDP 後保存到遠程描述,此時發起方也已知道連接雙方的配置;
socket.on('answer', answer) => {
  // 將 Answer 保存爲遠程描述;
  await localPc.setRemoteDescription(answer);
});

至此,媒體協商結束,緊接着在 WebRTC 底層會收集Candidate,並進行連通性檢測,最終在通話雙方之間建立起一條鏈路來。

第六步:端與端建立連接

媒體協商結束後,雙端統一了傳輸協議、編解碼器等,此時就需要建立連接開始音視頻通信了。

但 WebRTC 既要保持音視頻通信的 「質量」,又要保證 「聯通性」。所有,當同時存在多個有效連接時,它首先選擇傳輸質量最好的線路,如能用內網連通就不用公網,優先 P2P 傳輸,如果 P2P 不通才會選擇中繼服務器(relay),因爲中繼方式會增加雙端傳輸的時長。

什麼是 Candidate

第五步最後,我們提到了媒體協商結束後,開始收集 Candidate,那麼我們來了解下什麼是 Candidate、以及它的作用是什麼?

ICE Candidate(ICE 候選者):表示 WebRTC 與遠端通信時使用的協議、IP 地址和端口,結構如下:

{
  address: xxx.xxx.xxx.xxx, // 本地IP地址
  port: number, // 本地端口號
  type: 'host/srflx/relay', // 候選者類型
  priority: number, // 優先級
  protocol: 'udp/tcp', // 傳輸協議
  usernameFragment: string // 訪問服務的用戶名
  ...
}

WebRTC 在進行連接測試後時,通信雙端會提供衆多候選者,然後按照優先級進行連通性測試,測試成功就會建立連接。

候選者 Candidate 類型,即 type 分爲三種類型:

如何收集 Candidate

我們已經瞭解了 Candidate 的三種類型以及各自的優先級,那麼我們看下雙端是如何收集 Candidate 的。

host 類型

host 類型的 Candidate 是最好收集的,就是本機的 ip 地址 和端口。

srflx 和 relay 類型

srflx 類型的 Candidate 就是內網通過 NAT(Net Address Translation,作用是進行內外網的地址轉換,位於內網的網關上)映射後的外網地址。

如:訪問百度時 NAT 會將主機內網地址轉換爲外網地址,發送請求到百度的服務器,服務器返回到公網地址和端口,在通過 NAT 轉到內網的主機上。

那 WebRTC 是怎麼處理 NAT 的呢?

沒錯,就是我們上面提到的 「STUN」 和 「TURN」。

STUN 協議

全稱 Session Traversal Utilities for NAT(NAT 會話穿越應用程序),是一種網絡協議,它允許位於 NAT 後的客戶端找出自己的公網地址,也就是 「遵守這個協議就可以拿到自己的公網 IP」。

STUN 服務可以直接使用 google 提供的免費服務 stun.l.google.com:19302,或者自己搭建。

TURN 協議

全稱 Traversal Using Relays around NAT(使用中繼穿透 NAT),STUN 的中繼擴展。簡單的說,TURN 與 STUN 的共同點都是通過修改應用層中的私網地址達到 NAT 穿透的效果,異同點是 TURN 是通過兩方通訊的 “中間人” 方式實現穿透。

❝

上面提到的 relay 服務就是通過 TURN 協議實現的,所以 relay 服務器和 TURN 服務器是同一個意思,都是中繼服務器。

❞

relay 類型的 Candidate 獲取是通過 TURN 協議完成,它的 「連通率是所有候選者中連通率最高的」,優先級也是最低的。

WebRTC 首會先使用 STUN 服務器去找出自己的 NAT 環境,然後試圖找出打 “洞” 的方式,最後試圖創建點對點連接。當它嘗試過不同的穿透方式都失敗之後,爲保證通信成功率會啓用 TURN 服務器進行中轉,此時所有的流量都會通過 TURN 服務器。這時如果 TURN 服務器配置不好或帶寬不夠時,通信質量就會變差。

「重點:STUN 服務器是用來獲取外網地址進行 P2P;而 TURN 服務器是在 P2P 失敗時進行轉發的」

NAT 打洞 / P2P 穿越

NAT 解決了 IPv4 地址不夠用的情況,但因爲有了 NAT,端與端之間的網絡連接變得複雜,也就需要 NAT 穿越等技術。

收集完 Candidate 後,WebRTC 就按照優先級順序進行連通性檢測。如果雙方位於同一個局域網,就會直接建立連接,如果不在同一個局域網內,WebRTC 就會嘗試 NAT 打洞,即 P2P 穿越了。

ICE

全稱 Interactive Connectivity Establishment(交互式連通建立方式),ICE 協議通過一系列的技術(如 STUN、TURN 服務器)幫助通信雙方發現和協商可用的公共網絡地址,從而實現 NAT 穿越,也就是上面說的獲取所有候選者類型的過程,即:在本機收集所有的 host 類型的 Candidate,通過 STUN 協議收集 srflx 類型的 Candidate,使用 TURN 協議收集 relay 類型的 Candidate。

代碼部分

當 Candidate 被收集之後,會觸發icecandidate事件,所以需要在代碼中監聽此事件,以對收集到的 Candidate 做處理。

localPc.onicecandidate = function (event) {
  // 回調時,將自己candidate發給對方,對方可以直接addIceCandidate(candidate)添加可以獲取流
  if (event.candidate) socket.emit('candidate', event.candidate)
}

打印出的 Candidate 如下所示:

與我們上面提到的 Candidate 結構一致,其中type字段爲host,即本機候選者。

對端接收到發送的 candidate 後,再調用 RTCPeerConnection 對象的addIceCandidate()方法將收到的 Candidate 保存起 來,然後按照 Candidate 的優先級進行連通性檢測。

await remotePc.addIceCandidate(candidate)

如果 Candidate 連通性檢測完成,那麼端與端之間就建立了物理連接,這時媒體數據就可能通這個物理連接源源不斷地傳輸了 🎉🎉🎉Ï。

第七步:顯示遠端流

通信雙方通過 RTCPeerConnection 建立連接後,本地的音視頻數據源源不斷的傳輸,要想在遠端展示出來,就需要將 RTCPeerConnection 對象與<video>或<audio>進行綁定。

當遠端創建好 RTCPeerConnection 對象後,會爲 RTCPeerConnection 綁定ontrack事件,當有音視頻數據流到來時,輸入參數 event 中包含了遠端的音視頻流,即 MediaStream 對象,此時將此對象賦值給<video>或<audio>的srcObject字段,這樣 RTCPeerConnection 對象就與<video>或<audio>進行了綁定,音頻或視頻就能展示出來。

remotePc.ontrack = (e) => {
  video.srcObject = e.streams[0]
  video.oncanplay = () => video.play()
}

至此,一個完整的 WebRTC 通信過程就結束了。

希望各位 jy 能有所收穫。

最後

本文主要是針對小白的 WebRTC 掃盲教程,接下來會詳細講解一對一的音視頻聊天,多人聊天,以及使用 Livekit 快速搭建多人音視頻聊天系統。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pTGkFqAnGkBmE08nOa4RCw