本文由微醫雲技術團隊前端工程師張宇航分享,原題“從0到1打造一個 WebRTC 應用”,有修訂和改動。
1、引言
去年初,突如其來的新冠肺炎疫情讓線下就醫渠道幾乎被切斷,在此背景下,在線問診模式快速解決了大量急需就醫病患的燃眉之急。而作為在線問診中重要的一環——醫患之間的視頻問診正是應用了實時音視頻技術才得以實現。
眾所周之,實時音視頻聊天技術門檻很高,一般的公司要想在短時間內從零補齊這方面的技術短板相當困難,而開源音視頻工程WebRTC提供了這樣一個捷徑(包括筆者公司的產品在內,同樣是基於WebRTC技術才得以達成)。
本文將基於筆者公司開發的在線問診產品中WebRTC技術的實踐經驗,講述的如何基於WebRTC從零開發一個實時音視頻聊天功能。文章會從WebRTC的基本知識、技術原理開始,基於開源技術為你演示如何搭建一個WebRTC實時音視頻聊天功能。
學習交流:
- 即時通訊/推送技術開發交流5群:215477170 [推薦]
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK
(本文同步發布於:http://www.52im.net/thread-3680-1-1.html)
2、本文源碼
完整源碼附件下載:
https://gitee.com/instant_messaging_network/learn-webrtc
cdwebrtc-server
yarn
npm start
cdwebrtc-static
yarn
npm start
3、知識准備
3.1 音視頻理論基礎
在了解WebRTC技術之前,如果你對音視頻技術的基礎理論還不了解的話,建議優先從以下幾篇入門文章先學一學。
- 《零基礎,史上最通俗視頻編碼技術入門》(* 必讀)
- 《寫給小白的實時音視頻技術入門提綱》
- 《零基礎入門:實時音視頻技術基礎知識全面盤點》
- 《實時音視頻面視必備:快速掌握11個視頻技術相關的基礎概念》(* 必讀)
- 《愛奇藝技術分享:輕松詼諧,講解視頻編解碼技術的過去、現在和將來》
3.2 什么是WebRTC

▲ 圖片引用自《了不起的WebRTC:生態日趨完善,或將實時音視頻技術白菜化》
WebRTC(Web Real-Time Communication)是 Google 在 2010 年以 6820 萬美元收購 VoIP 軟件開發商 Global IP Solutions 的 GIPS 引擎,並改名為“WebRTC”於 2011 年將其開源的旨在建立一個互聯網瀏覽器之間的音視頻和數據實時通信的平台。更多WebRTC介紹詳見《了不起的WebRTC:生態日趨完善,或將實時音視頻技術白菜化》,本文不做贅述。
那么 WebRTC 能做些什么呢?
除了我們大家每天都在用的微信、釘釘、qq這類傳統的IM社交軟件中的實時音視頻通話以外,筆者公司產品中涉及醫療領域中的在線問診/遠程門診/遠程會診,還有時下較為流行的互動直播、在線教育等場景。除此之外,伴隨着 5G 的快速建設,WebRTC 也為雲游戲提供了很好的技術支撐。
3.3 WebRTC的學習資源
WebRTC官方資源:
其它WebRTC學習資源:
4、WebRTC技術組成
來自WebRTC 官網的整體技術組成圖:

整個WebRTC大致可以分為以下 3 部分:
- 1)紫色提供給 Web 前端開發使用的 API;
- 2)藍色實線部分提供各大瀏覽器廠商使用的 API;
- 3)藍色虛線部分包含 3 部分:音頻引擎、視頻引擎、網絡傳輸 (Transport),都可以自定義實現。
因篇幅有限,本節不深入討論,有興趣可以讀讀《WebRTC實時音視頻技術的整體架構介紹》。
5、WebRTC的P2P通信原理
5.1 P2P通信的技術難點
P2P通信即點對點通信。
要實現兩個不同網絡環境(具有麥克風、攝像頭設備)的客戶端(可能是不同的 Web 瀏覽器或者手機 App)之間的實時音視頻通信的難點在哪里、需要解決哪些問題?
總結一下,主要是下面這3個問題:
- 1)怎么知道彼此的存在也就是如何發現對方?
- 2)彼此音視頻編解碼能力如何溝通?
- 3)音視頻數據如何傳輸,怎么能讓對方看得自己?
下面我們將逐個討論這3個問題。
5.2 怎么知道彼此的存在(也就是如何發現對方)?
對於問題 1:WebRTC 雖然支持端對端通信,但是這並不意味着 WebRTC 不再需要服務器。
在P2P通信的過程中,雙方需要交換一些元數據比如媒體信息、網絡數據等等信息,我們通常稱這一過程叫做“信令(signaling)”。
對應的服務器即“信令服務器 (signaling server)”,通常也有人將之稱為“房間服務器”,因為它不僅可以交換彼此的媒體信息和網絡信息,同樣也可以管理房間信息。
比如:
- 1)通知彼此 who 加入了房間;
- 2)who 離開了房間
- 3)告訴第三方房間人數是否已滿是否可以加入房間。
為了避免出現冗余,並最大限度地提高與已有技術的兼容性,WebRTC 標准並沒有規定信令方法和協議。在本文后面的實踐章節會利用 Koa 和 Socket.io 技術實現一個信令服務器。
5.3 彼此音視頻編解碼能力如何溝通?
對於問題 2:我們首先要知道的是,不同瀏覽器對於音視頻的編解碼能力是不同的。
比如: Peer-A 端支持 H264、VP8 等多種編碼格式,而 Peer-B 端支持 H264、VP9 等格式。為了保證雙方都可以正確的編解碼,最簡單的辦法即取它們所都支持格式的交集-H264。

在 WebRTC 中:有一個專門的協議,稱為Session Description Protocol(SDP),可以用於描述上述這類信息。
因此:參與音視頻通訊的雙方想要了解對方支持的媒體格式,必須要交換 SDP 信息。而交換 SDP 的過程,通常稱之為媒體協商。
5.4 音視頻數據如何傳輸,怎么能讓對方看得自己?
對於問題 3:其本質上就是網絡協商的過程,即參與音視頻實時通信的雙方要了解彼此的網絡情況,這樣才有可能找到一條相互通訊的鏈路。
理想的網絡情況是每個瀏覽器的電腦都有自己的私有公網 IP 地址,這樣的話就可以直接進行點對點連接。
但實際上:出於網絡安全和 IPV4 地址不夠的考慮,我們的電腦與電腦之間或大或小都是在某個局域網內,需要NAT(“Network Address Translation,” 中文譯為“網絡地址轉換”)。在 WebRTC 中我們使用 ICE 機制建立網絡連接。
那么何為 ICE?
ICE (Interactive Connecctivity Establishment, 交互式連接建立),ICE 不是一種協議,而是整合了 STUN 和 TURN 兩種協議的框架。
其中:STUN(Sesssion Traversal Utilities for NAT, NAT 會話穿越應用程序),它允許位於 NAT(或多重 NAT)后的客戶端找出自己對應的公網 IP 地址和端口,也就是俗稱的P2P“打洞”。
但是:如果 NAT 類型是對稱型的話,那么就無法打洞成功。這時候 TURN 就派上用場了,TURN(Traversal USing Replays around NAT)是 STUN/RFC5389 的一個拓展協議在其基礎上添加了 Replay(中繼)功能。
簡單來說:其目的就是解決對稱 NAT 無法穿越的問題,在 STUN 分配公網 IP 失敗后,可以通過 TURN 服務器請求公網 IP 地址作為中繼地址。
在 WebRTC 中有三種類型的 ICE 候選者,它們分別是:
- 1)主機候選者:表示的是本地局域網內的 IP 地址及端口。它是三個候選者中優先級最高的,也就是說在 WebRTC 底層,首先會嘗試本地局域網內建立連接;
- 2)反射候選者:表示的是獲取 NAT 內主機的外網 IP 地址和端口。其優先級低於 主機候選者。也就是說當 WebRTC 嘗試本地連接不通時,會嘗試通過反射候選者獲得的 IP 地址和端口進行連接;
- 3)中繼候選者:表示的是中繼服務器的 IP 地址與端口,即通過服務器中轉媒體數據。當 WebRTC 客戶端通信雙方無法穿越 P2P NAT 時,為了保證雙方可以正常通訊,此時只能通過服務器中轉來保證服務質量了。

從上圖我們可以看出:在非本地局域網內 WebRTC 通過 STUN server 獲得自己的外網 IP 和端口,然后通過信令服務器與遠端的 WebRTC 交換網絡信息,之后雙方就可以嘗試建立 P2P 連接了。當 NAT 穿越不成功時,則會通過 Relay server (TURN)中轉。
值得一提的是:在 WebRTC 中網絡信息通常用candidate來描述,而上述圖中的 STUN server 和 Replay server 也都可以是同一個 server。在文末的實踐章節即是采用了集成了 STUN(打洞)和 TURN(中繼)功能的開源項目 coturn。
綜上對三個問題的解釋,我們可以用下圖來說明 WebRTC 點對點通信的基本原理。

簡而言之:就是通過 WebRTC 提供的 API 獲取各端的媒體信息 SDP 以及 網絡信息 candidate ,並通過信令服務器交換,進而建立了兩端的連接通道完成實時視頻語音通話。
PS:有關P2P的相關知識,可以深入學習一下文章:
- 《P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介》
- 《P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解(基本原理篇)》
- 《P2P技術詳解(三):P2P中的NAT穿越(打洞)方案詳解(進階分析篇)》
- 《P2P技術詳解(四):P2P技術之STUN、TURN、ICE詳解》
- 《通俗易懂:快速理解P2P技術中的NAT穿透原理》
6、WebRTC的幾個重要的API
6.1 音視頻采集 API
音視頻采集 API,即 MediaDevices.getUserMedia()。
示例代碼:
const constraints = {
video: true,
audio: true
};
// 非安全模式(非https/localhost)下 navigator.mediaDevices 會返回 undefined
try{
const stream = await navigator.mediaDevices.getUserMedia(constraints);
document.querySelector('video').srcObject = stream;
} catch(error) {
console.error(error);
}
6.2 獲取音視頻設備輸入輸出列表
獲取音視頻設備輸入輸出列表API,即 MediaDevices.enumerateDevices()。
示例代碼:
try{
const devices = await navigator.mediaDevices.enumerateDevices();
this.videoinputs = devices.filter(device => device.kind === 'videoinput');
this.audiooutputs = devices.filter(device => device.kind === 'audiooutput');
this.audioinputs = devices.filter(device => device.kind === 'audioinput');
} catch(error) {
console.error(error);
}
6.3 RTCPeerConnection
RTCPeerConnection 作為創建點對點連接的 API,是我們實現音視頻實時通信的關鍵。
在本文的實踐章節中,主要運用到了以下方法。
媒體協商方法:
重要事件:
在上個章節的描述中可以知道 P2P 通信中最重要的一個環節就是交換媒體信息。
媒體協商原理:

從上圖不難發現,整個媒體協商過程可以簡化為三個步驟對應上述四個媒體協商方法。
具體是:
- 1)呼叫端 Amy 創建 Offer(createOffer)並將 offer 消息(內容是呼叫端 Amy 的 SDP 信息)通過信令服務器傳送給接收端 Bob,同時調用 setLocalDesccription 將含有本地 SDP 信息的 Offer 保存起來;
- 2)接收端 Bob 收到對端的 Offer 信息后調用 setRemoteDesccription 方法將含有對端 SDP 信息的 Offer 保存起來,並創建 Answer(createAnswer)並將 Answer 消息(內容是接收端 Bob 的 SDP 信息)通過信令服務器傳送給呼叫端 Amy;
- 3)呼叫端 Amy 收到對端的 Answer 信息后調用 setRemoteDesccription 方法將含有對端 SDP 信息的 Answer 保存起來。
經過上述三個步驟,則完成了 P2P 通信過程中的媒體協商部分。
實際上:在呼叫端以及接收端調用 setLocalDesccription 同時也開始了收集各端自己的網絡信息(candidate),然后各端通過監聽事件 onicecandidate 收集到各自的 candidate 並通過信令服務器傳送給對端,進而打通 P2P 通信的網絡通道,並通過監聽 onaddstream 事件拿到對方的視頻流進而完成了整個視頻通話過程。
7、動手編碼實踐
提示:本節所涉及的完整源碼,請從本文“2、本文源碼”一節的附件下載。
7.1 coturn 服務器的搭建
注意:如果只是本地局域網測試則無需搭建 [url=%5Burl=https://github.com/coturn/%5Dcoturn[/url]]coturn[/url] 服務器,如果需要外網訪問在搭建 coturn 服務器之前你需要購買一台雲主機以及綁定支持 https 訪問的域名。以下是筆者自己搭建的過程,感興趣的可以參照着自已實踐一次。
coturn 服務器的搭建主要是為了解決 NAT 無法穿越的問題。
其安裝也較為簡單:
1. git clone [url=https://github.com/coturn/coturn.git]https://github.com/coturn/coturn.git[/url]
2. cdcoturn/
3. ./configure--prefix=/usr/local/coturn
4. make-j 4
5. makeinstall
//生成 key
6. openssl req -x509 -newkey rsa:2048 -keyout /etc/turn_server_pkey.pem -out /etc/turn_server_cert.pem -days 99999 -nodes
7.2 coturn 服務配置
我的配置如下:
vim /usr/local/coturn/etc/turnserver.conf
listening-port=3478
external-ip=xxx.xxx //你的主機公網 IP
user=xxx:xxx //賬號: 密碼
realm=xxx.com //你的域名
7.3 啟動 coturn 服務
我的啟動過程:
1. cd/usr/local/coturn/bin/
2. ./turnserver-c ../etc/turnserver.conf
//注意:雲主機內的 TCP 和 UDP 的 3478 端口都要開啟
7.4 實踐代碼
在編寫代碼之前,結合上述章節 WebRTC 點對點通信的基本原理,可以得出以下流程圖。

從圖中不難看出,假設 PeerA 為發起方,PeerB 為接收方要實現 WebRTC 點對點的實時音視頻通信,信令(Signal)服務器是必要的,以管理房間信息以及轉發網絡信息和媒體信息的。
在本文中是利用 koa 及 socket.io 搭建的信令服務器:
// server 端 server.js
const Koa = require('koa');
const socket = require('socket.io');
const http = require('http');
const app = newKoa();
const httpServer = http.createServer(app.callback()).listen(3000, ()=>{});
socket(httpServer).on('connection', (sock)=>{
// ....
});
// client 端 socket.js
import io from 'socket.io-client';
const socket = io.connect(window.location.origin);
export defaultsocket;
在搭建好信令服務器后,結合流程圖,有以下步驟。
步驟1:PeerA 和 PeerB 端分別連接信令服務器,信令服務器記錄房間信息:
// server 端 server.js
socket(httpServer).on('connection', (sock)=>{
// 用戶離開房間
sock.on('userLeave',()=>{
// ...
});
// 檢查房間是否可加入
sock.on('checkRoom',()=>{
// ...
});
// ....
});
// client 端 Room.vue
import socket from '../utils/socket.js';
// 服務端告知用戶是否可加入房間
socket.on('checkRoomSuccess',()=>{
// ...
});
// 服務端告知用戶成功加入房間
socket.on('joinRoomSuccess',()=>{
// ...
});
//....
步驟2:A 端作為發起方向接收方 B 端發起視頻邀請,在得到 B 同意視頻請求后,雙方都會創建本地的 RTCPeerConnection,添加本地視頻流,其中發送方會創建 offer 設置本地 sdp 信息描述,並通過信令服務器將自己的 SDP 信息發送給對端
socket.on('answerVideo', async (user) => {
VIDEO_VIEW.showInvideoModal();
// 創建本地視頻流信息
const localStream = await this.createLocalVideoStream();
this.localStream = localStream;
document.querySelector('#echat-local').srcObject = this.localStream;
this.peer = newRTCPeerConnection();
this.initPeerListen();
this.peer.addStream(this.localStream);
if(user.sockId === this.sockId) {
// 接收方
} else{
// 發送方 創建 offer
const offer = await this.peer.createOffer(this.offerOption);
await this.peer.setLocalDescription(offer);
socket.emit('receiveOffer', { user: this.user, offer });
}
});
步驟3:前面提起過其實在調用 setLocalDescription 的同時,也會開始收集自己端的網絡信息(candidate),如果在非局域網內或者網絡“打洞”不成功,還會嘗試向 Stun/Turn 服務器發起請求,也就是收集“中繼候選者”,因此在創建 RTCPeerConnection 我們還需要監聽 ICE 網絡候選者的事件:
init PeerListen () {
// 收集自己的網絡信息並發送給對端
this.peer.onicecandidate = (event) => {
if(event.candidate) { socket.emit('addIceCandidate', { candidate: event.candidate, user: this.user }); }
};
// ....
}
步驟4:當接收方 B 端通過信令服務器拿到對端發送方 A 端的含有 SDP 的 offer 信息后則會調用 setRemoteDescription 存儲對端的 SDP 信息,創建及設置本地的 SDP 信息,並通過信令服務器傳送含有本地 SDP 信息的 answer:
socket.on('receiveOffer', async (offer) => {
await this.peer.setRemoteDescription(offer);
const answer = await this.peer.createAnswer();
await this.peer.setLocalDescription(answer);
socket.emit('receiveAnsewer', { answer, user: this.user });
});
步驟5:當發起方 A 通過信令服務器接收到接收方 B 的 answer 信息后則也會調用 setRemoteDescription,這樣雙方就完成了 SDP 信息的交換:
socket.on('receiveAnsewer', (answer) => {
this.peer.setRemoteDescription(answer);
});
步驟6:當雙方 SDP 信息交換完成並且監聽 icecandidate 收集到網絡候選者通過信令服務器交換后,則會拿到彼此的視頻流:
socket.on('addIceCandidate', async (candidate) => {
await this.peer.addIceCandidate(candidate);
});
this.peer.onaddstream = (event) => {
// 拿到對方的視頻流
document.querySelector('#remote-video').srcObject = event.stream;
};
7.5 運行效果
8、本文小結
經過上個章節的6個步驟,即可完成一個基於WebRTC的完整 P2P 視頻實時通話功能(代碼可通過:本節所涉及的完整源碼,請從本文“2、本文源碼”一節的附件下載)。
值得一提的是:代碼中的 VIDEO_VIEW 是專注於視頻UI層的JS SDK,包含了發起視頻 Modal、接收視頻 Modal、視頻中 Modal,其是從筆者線上 Web 視頻問診產品所使用的 JS SDK 抽離出來的。
本文只是簡單地介紹了WebRTC P2P的通信基本原理以及簡單的代碼實踐,事實上我們生產環境所使用的 SDK 不僅支持點對點通信,還支持多人視頻通話,屏幕共享等功能這些都是基於WebRTC實現的。
9、參考資料
[1] WebRTC標准API在線文檔
[2] WebRTC in the real world: STUN, TURN and signaling
[3] WebRTC 信令控制與 STUN/TURN 服務器搭建
[4] 了不起的WebRTC:生態日趨完善,或將實時音視頻技術白菜化
[5] 開源實時音視頻技術WebRTC在Windows下的簡明編譯教程
[7] 良心分享:WebRTC 零基礎開發者教程(中文)[附件下載]
[8] P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解(基本原理篇)
[9] P2P技術詳解(四):P2P技術之STUN、TURN、ICE詳解
附錄:更多實時音視頻技術資料
[1] 開源實時音視頻技術WebRTC的文章:
《訪談WebRTC標准之父:WebRTC的過去、現在和未來》
《良心分享:WebRTC 零基礎開發者教程(中文)[附件下載]》
《新手入門:到底什么是WebRTC服務器,以及它是如何聯接通話的?》
《[觀點] WebRTC應該選擇H.264視頻編碼的四大理由》
《基於開源WebRTC開發實時音視頻靠譜嗎?第3方SDK有哪些?》
《開源實時音視頻技術WebRTC中RTP/RTCP數據傳輸協議的應用》
《開源實時音視頻技術WebRTC在Windows下的簡明編譯教程》
《網頁端實時音視頻技術WebRTC:看起來很美,但離生產應用還有多少坑要填?》
《了不起的WebRTC:生態日趨完善,或將實時音視頻技術白菜化》
《騰訊技術分享:微信小程序音視頻與WebRTC互通的技術思路和實踐》
《融雲技術分享:基於WebRTC的實時音視頻首幀顯示時間優化實踐》
《零基礎入門:基於開源WebRTC,從0到1實現實時音視頻聊天功能》
>> 更多同類文章 ……
[2] 實時音視頻開發的其它精華資料:
《即時通訊音視頻開發(五):認識主流視頻編碼技術H.264》
《即時通訊音視頻開發(九):實時語音通訊的回音及回音消除概述》
《即時通訊音視頻開發(十):實時語音通訊的回音消除技術詳解》
《即時通訊音視頻開發(十一):實時語音通訊丟包補償技術詳解》
《即時通訊音視頻開發(十三):實時視頻編碼H.264的特點與優勢》
《即時通訊音視頻開發(十五):聊聊P2P與實時音視頻的應用情況》
《即時通訊音視頻開發(十六):移動端實時音視頻開發的幾個建議》
《即時通訊音視頻開發(十七):視頻編碼H.264、VP8的前世今生》
《即時通訊音視頻開發(十八):詳解音頻編解碼的原理、演進和應用選型》
《即時通訊音視頻開發(十九):零基礎,史上最通俗視頻編碼技術入門》
《學習RFC3550:RTP/RTCP實時傳輸協議基礎知識》
《基於RTMP數據傳輸協議的實時流媒體技術研究(論文全文)》
《還在靠“喂喂喂”測試實時語音通話質量?本文教你科學的評測方法!》
《實現延遲低於500毫秒的1080P實時音視頻直播的實踐分享》
《技術揭秘:支持百萬級粉絲互動的Facebook實時視頻直播》
《理論聯系實際:實現一個簡單地基於HTML5的實時視頻直播》
《首次披露:快手是如何做到百萬觀眾同場看直播仍能秒開且不卡頓的?》
《騰訊音視頻實驗室:使用AI黑科技實現超低碼率的高清實時視頻聊天》
《七牛雲技術分享:使用QUIC協議實現實時視頻直播0卡頓!》
《實時視頻直播客戶端技術盤點:Native、HTML5、WebRTC、微信小程序》
《微信多媒體團隊訪談:音視頻開發的學習、微信的音視頻技術和挑戰等》
《以網游服務端的網絡接入層設計為例,理解實時通信的技術挑戰》
《騰訊技術分享:微信小程序音視頻與WebRTC互通的技術思路和實踐》
《愛奇藝技術分享:輕松詼諧,講解視頻編解碼技術的過去、現在和將來》
《實時音視頻面視必備:快速掌握11個視頻技術相關的基礎概念》
《實時音視頻開發理論必備:如何省流量?視頻高度壓縮背后的預測技術》
>> 更多同類文章 ……
本文已同步發布於“即時通訊技術圈”公眾號。