WebRTC
什么是WebRTC
WebRTC(Web Real-Time Communication)是一項(xiàng)由 W3C 和 IETF 推動(dòng)的開源項(xiàng)目拆座,旨在為瀏覽器和移動(dòng)應(yīng)用提供實(shí)時(shí)通信(RTC)功能主巍。
WebRTC 支持音頻、視頻和數(shù)據(jù)在點(diǎn)對(duì)點(diǎn)(P2P)連接中直接傳輸挪凑,而無(wú)需中間服務(wù)器孕索。也支持配合SFU(Selective Forwarding Unit),MCU(Multipoint Control Unit)技術(shù)躏碳,構(gòu)建多對(duì)多的傳輸通信搞旭。
特點(diǎn)
- 實(shí)時(shí)通信: 支持音頻、視頻和數(shù)據(jù)的實(shí)時(shí)傳輸,延遲低肄渗,適用于實(shí)時(shí)互動(dòng)場(chǎng)景镇眷。
- 點(diǎn)對(duì)點(diǎn)連接: 通過(guò) P2P 連接,數(shù)據(jù)直接在客戶端之間傳輸翎嫡,減少了服務(wù)器的負(fù)載和延遲欠动。
- 跨平臺(tái)支持: 支持主流瀏覽器(如 Chrome、Firefox惑申、Safari翁垂、Edge)和移動(dòng)平臺(tái)(如 Android、iOS)硝桩。
- 安全性: 默認(rèn)使用 SRTP(Secure Real-time Transport Protocol)加密音視頻流,DTLS(Datagram Transport Layer Security)加密數(shù)據(jù)通道枚荣,確保通信安全碗脊。
- 開源和標(biāo)準(zhǔn)化: WebRTC 是一個(gè)開源項(xiàng)目,并且由 W3C 和 IETF 標(biāo)準(zhǔn)化橄妆,確保廣泛的兼容性和支持衙伶。
誰(shuí)在用
可以看出 基于 網(wǎng)絡(luò)語(yǔ)音/視頻通話
的場(chǎng)景,尤其是類似 實(shí)時(shí)網(wǎng)絡(luò) 語(yǔ)音電話這種害碾。
各大語(yǔ)音app (whats app, Facebook, Google系軟件) 都有基于webrtc或者參考webrtc的思路進(jìn)行實(shí)現(xiàn)矢劲。refs: https://telnyx.com/resources/5-applications-that-demonstrate-the-power-of-webrtc-and-sip
OPENAPI 也推出了實(shí)時(shí)流視頻接口,Realtime API with WebRTC https://platform.openai.com/docs/guides/realtime-websocket
快速構(gòu)建
速覽圖
一圖速覽慌随,可以看出 建立WebRTC的通信芬沉,整體分 建立網(wǎng)絡(luò)連接 和 推流 兩大步。
而網(wǎng)絡(luò)連接 又分P2P模式 和 中繼模式阁猜。
構(gòu)建一個(gè)完整的WebRTC丸逸,可以滿足不同環(huán)境,以及網(wǎng)絡(luò)情況下的端到端通信 剃袍,則我們需要構(gòu)建
- 信令服務(wù)
- STUN服務(wù)
- TURN服務(wù)
此外演示的代碼跑在瀏覽器上黄刚,訪問(wèn)瀏覽器展示頁(yè)面,創(chuàng)建WebRTC Client民效,所以還需要一個(gè)Web服務(wù)
- Web HTTP服務(wù)
廢話不多說(shuō)憔维,逐一部署,一個(gè)個(gè)擊破畏邢。接下來(lái)业扒,上家伙 (完整代碼見附錄)
1. 部署 Web HTTP 服務(wù) + Signaling 信令服務(wù)
構(gòu)建一個(gè)Web Http 服務(wù)器,可以返回 前端頁(yè)面所需數(shù)據(jù)舒萎,
...
const server = http.createServer((req, res) => {
console.log(`request url: ${req.url}, method: ${req.method}`);
// 提供靜態(tài)文件服務(wù)
// 返回主頁(yè)
if (req.method === "GET" && req.url === "/") {
console.log("request index.html");
const filePath = path.join(
__dirname,
"static/my-webrtc-client/index.html"
);
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 Not Found");
return;
}
// 根據(jù)文件擴(kuò)展名設(shè)置正確的 Content-Type
const ext = path.extname(filePath).toLowerCase();
const contentType = mimeTypes[ext] || "application/octet-stream";
res.writeHead(200, { "Content-Type": contentType });
res.end(data);
});
} else if (req.method === "GET" && isRequestFile(req)) {
// 返回 js / css 等文件
const dirpath = __dirname + "/static/my-webrtc-client";
const filePath = path.join(dirpath, req.url);
console.log(`request ${filePath}`);
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 Not Found");
return;
}
// 根據(jù)文件擴(kuò)展名設(shè)置正確的 Content-Type
const ext = path.extname(filePath).toLowerCase();
const contentType = mimeTypes[ext] || "application/octet-stream";
res.writeHead(200, { "Content-Type": contentType });
res.end(data);
});
} else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 Not Found");
}
});
...
構(gòu)建一個(gè)Signaling server凶赁,用于兩端數(shù)據(jù)的交換。
io.on("connection", (sock) => {
console.log("連接成功...");
// 向客戶端發(fā)送連接成功的消息
sock.emit("connectionSuccess");
// 監(jiān)聽客戶端event
sock.on("offer", (event) => {
console.log(`receive offer from device : ${event.fromDeviceId}`);
// 向其余客戶端發(fā)送offer
sock.broadcast.emit("offer", event);
});
sock.on("candidate", (event) => {
console.log(`receive candidate from device : ${event.fromDeviceId}`);
// 向其余客戶端發(fā)送offer
sock.broadcast.emit("candidate", event);
});
sock.on("answer", (event) => {
console.log(
`receive answer from deviceId : ${event.fromDeviceId}, to deviceId : ${event.toDeviceId}`
);
// 向其余客戶端發(fā)送offer
sock.broadcast.emit("answer", event);
});
...
});
2. 部署 STUN服務(wù) + TURN服務(wù)
此處選用coturn服務(wù),部署安裝
安裝
apt install coturn
修改coturn config, 參考 【W(wǎng)ebRTC - STUN/TURN服務(wù) - COTURN配置】
vim /etc/turnserver.conf
啟動(dòng)coturn服務(wù)
turnserver --log-file stdout
3. 使用 WebRTC Client進(jìn)行連接
構(gòu)建webrtc client 虱肄, webrtc場(chǎng)景下致板, 端到端通信,A為 Caller咏窿,B為 Called斟或。WebRTC Client A → WebRTC Client B 。
- Caller 和 Called 兩端分別初始化
caller / called = new RTCPeerConnection({
encodedInsertableStreams: true, // needed by chrome shim
iceServers: [
{
// default "stun:stun.l.google.com:19302",
urls: STUN_URL,
},
{
urls: TURN_URL,
username: TURN_USERNAME,
credential: TURN_CREDENTIAL,
},
],
});
- Caller 請(qǐng)求對(duì)端 集嵌,創(chuàng)建offer然后存儲(chǔ)在本地萝挤,然后發(fā)送offer
// 創(chuàng)建offer
const offer = await pc.createOffer(offerOptions);
// 存儲(chǔ)在本地
caller.setLocalDescription(offer)
// 發(fā)送到對(duì)端
sock.emit("offer", {
offer: offer,
...
});
- Called接收到offer,存儲(chǔ)在本地根欧,然后創(chuàng)建answer 然后回復(fù)answer給Caller
// 收到offer
// 存儲(chǔ)offer在本地
called.setRemoteDescription(event.offer)
// 創(chuàng)建answer
answer = await called.createAnswer();
// 存儲(chǔ)answer在本地
await called.setLocalDescription(answer);
- Caller在收到answer以后怜珍,存儲(chǔ)在本地
await pc.setRemoteDescription(event.answer);
- 同時(shí) 在無(wú)論是Caller 還是 Called 在調(diào)用setLocalDescription后,會(huì)觸發(fā)瀏覽器請(qǐng)求STUN服務(wù)器獲取candidate信息凤粗,Caler / Called 需要獲取到candidate 然后存在本地
// 收到candidate
caller/called.addEventListener("icecandidate", (e) => {
// 發(fā)給對(duì)端
sock.emit("candidate", {
candidate: candidate,
...
});
})
// 對(duì)端收到后 酥泛,存儲(chǔ)在本地
await caller/called.addIceCandidate(candidate);
當(dāng)兩端互換candidate 協(xié)商后,根據(jù)candidate的type (host嫌拣, relay 等) 嘗試連通柔袁,最終決定是p2p通信,還是 relay通信异逐。至此便完成了兩端網(wǎng)絡(luò)連通捶索。
當(dāng)網(wǎng)絡(luò)連通后,Caller獲取本地音視頻數(shù)據(jù)后 灰瞻,用剛剛打通的網(wǎng)絡(luò)腥例,便可進(jìn)行數(shù)據(jù)傳輸。Called接收數(shù)據(jù)酝润。
Caller
// 獲取 音視頻數(shù)據(jù)
stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});
// 傳輸
stream.getTracks().forEach((track) => {
caller.addTrack(track, stream);
});
Called
// 收到后院崇, 將數(shù)據(jù)放到 video element上,進(jìn)行播放
called.addEventListener("track", (e) => {
const remoteVideo = document.getElementById("remoteVideo");
remoteVideo.srcObject = e.streams[0];
});
WebRTC - 展示
p2p 模式:兩個(gè)設(shè)備都在同一個(gè)局域網(wǎng)內(nèi) 袍祖,
打開兩個(gè)網(wǎng)頁(yè)端底瓣,分別是Caller 和 Called。
Caller 點(diǎn)擊 Start 獲取媒體流蕉陋,然后點(diǎn)擊Call 和 對(duì)端Called連通網(wǎng)絡(luò)后捐凭,便可以推流到對(duì)端。
中繼模式: 兩個(gè)移動(dòng)端設(shè)備在公網(wǎng)環(huán)境 凳鬓,一個(gè)筆記本電腦 茁肠,一個(gè)手機(jī)
筆記本視角:
手機(jī)端視角:
注意:
- 如果兩端不在同一局域網(wǎng)內(nèi),大概率是需要中繼服務(wù)的缩举,則此時(shí)垦梆,信令服務(wù)匹颤,TURN/STUN服務(wù)需要部署在有公網(wǎng)地址的機(jī)器上。
- STUN有免費(fèi)公共的可以使用托猩,但是TURN由于帶寬費(fèi)用高印蓖,沒(méi)有免費(fèi)公共的,所以必須要自部署京腥。
附錄
代碼
示例代碼赦肃,包括 前后端代碼,信令服務(wù) 公浪。
名詞解釋
ICE 候選者 (Candidate)
ICE 候選者包含關(guān)于如何連接到對(duì)等方的網(wǎng)絡(luò)信息他宛。每個(gè)候選者提供一個(gè)可能的網(wǎng)絡(luò)路徑。具體信息包括:
- IP 地址: 設(shè)備的公共或私有 IP 地址欠气。
- 端口: 用于通信的端口號(hào)厅各。
- 優(yōu)先級(jí): 候選者的優(yōu)先級(jí),用于選擇最佳路徑预柒。
- 類型: 候選者的類型(如 host队塘、srflx、relay)卫旱。
- 協(xié)議: 使用的傳輸協(xié)議(如 UDP、TCP)围段。
candidate 示例
candidate:842163049 1 udp 1677729535 192.168.1.2 56143 typ srflx raddr 10.0.0.1 rport 8998 generation 0 ufrag EEtu network-id 3 network-cost 10
SDP (Session Description Protocol)
SDP 是一種格式化的文本顾翼,用于描述多媒體會(huì)話的參數(shù)。它包含的信息包括:
- 會(huì)話描述: 包括會(huì)話的名稱奈泪、時(shí)間适贸、參與者等。
- 媒體描述: 包括媒體類型(音頻涝桅、視頻)拜姿、編解碼器、比特率等冯遂。
- 連接信息: 包括 IP 地址和端口號(hào)蕊肥。
- 媒體格式: 支持的媒體格式和編解碼器。
- 帶寬信息: 會(huì)話的帶寬要求蛤肌。
- 屬性: 其他會(huì)話屬性壁却,如方向(發(fā)送、接收)裸准。
sdp 示例
v=0
o=- 46117327 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104
c=IN IP4 192.168.1.2
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:EEtu
a=ice-pwd:asd88fgpdd777uzjYhagZg
a=mid:audio
a=sendrecv
a=rtpmap:111 opus/48000/2
m=video 9 UDP/TLS/RTP/SAVPF 100 101
c=IN IP4 192.168.1.2
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:EEtu
a=ice-pwd:asd88fgpdd777uzjYhagZg
a=mid:video
a=sendrecv
a=rtpmap:100 VP8/90000