介紹
WebRTC (Web Real-Time Communications) 是一個實時通訊技術,也是實時音視頻技術的標準和框架。簡單來說WebRTC是一個集大成的實時音視頻技術集,包含了各種客戶端api、音視頻編/解碼lib、流媒體傳輸協議、回聲消除、安全傳輸等。對于開發者來說可以借助webrtc非常方便的實現低延時視頻通話能力。目前大多主流的直播系統、會議系統基本都是基于WebRTC來實現。
三種架構
WebRTC針對不同場景以及性能考慮提供了三種架構Mesh架構、MCU、FSU。
Mesh架構
Mesh架構,需要所有參與連接的peer建立與所有其他peer的媒體連接(兩兩連接)。該架構需要n-1個上下行,以此帶來的帶寬消耗(流量)、編/解碼消耗(設備性能)成線性增長。該架構只能適用3-4個人的小型會議場景。
MCU架構
所有參與連接的peer將本地媒體流推到遠程媒體服務器,由媒體服務器進行混流,然后再推到所有連接的peer端。該架構的優點就是只需要1路上下行,隨著peer人數不斷增加,依然不會對用戶造成帶寬、手機性能影響。該架構將壓力轉嫁到服務端,由專用媒體服務器來完成混流,轉推等功能。
SFU架構
相對于MCU來說SFU只做轉發,媒體服務器壓力有限。與Mesh架構相比,只需要n-1個下行,1個上行,減少了服務器壓力。在大規模的場合該架構具有伸縮性。
點對點視頻連接
根據上面,我們對基本WebRTC有了最基本的認識,下面就從點對點實際例子來從代碼角度進一步了解其原理。
先從下圖來看看使用MCU來實現點對點需要哪些東西
在介紹流程之前,先簡單介紹下上圖中出現的名詞代表什么意思:
- Peer:通信雙方設備。
- Signaling Server: 信令服務器,用于交互連接雙方的信令數據(SDP、ICE等),以保證通信的對等連接建立。
- NAT:處理私有網絡和公共網絡之間的地址轉換問題(因為大多數設置都處于內網中,需要轉換為公共網絡才能進行外網訪問)
- STUN:用于發現設備的公共地址(通過NAT轉換的公網地址),輔助穿越NAT進行點對點連接。
- TURN:在無法建立直接連接時提供數據中繼,確保通信的可靠性。對等連接異常時的兜底方案。
- SDP:會話描述協議,用于描述和協商媒體會話的協議,它定義了會話的所有技術細節,包括媒體格式、編解碼器、網絡地址等。,
- ICE:用于發現和選擇最優網絡路徑的框架,確保在各種網絡環境下都能成功建立和維持連接。
代碼實現
實現點對點連接主要是兩點:1、信令數據交互 2、對等連接建立
在代碼中使用到了socket.io來將設備和信令服務器通信,使用了simple-peer來建立對等連接,由于該demo在本地運行所以沒有使用STUN/TRUN服務器,有興趣的可以使用Chrome提供的公共服務器stun:stun.l.google.com:19302
主要步驟如下:
- 1、和信令服務器建立連接,并獲取自身的socketId作為唯一標識
- 2、申請方將信令(由simple-peer生成)通過信令服務器到達接受方
- 3、接受方接受,將發起方的信令保存到對等連接peer中,并且將自己的信令通過信令服務器給到發送方
- 4、發送方將接受方的信令數據保存到對等連接peer中,至此發送方-接受方對等連接建立完成
- 5、在發送方和接受方監聽peer的stream,來獲取視頻流,然后展示在頁面
和信令服務器建立連接
新建一個server,js使用node+express搭建的簡易信令服務器,用于交換雙方信令。通過create-react-app來創建一個前端頁面。
信令服務器代碼如下:
const express = require("express");
const http = require("http");
const cors = require("cors");const app = express();
const server = http.createServer(app);
app.use(cors);
const io = require("socket.io")(server, {cors: {origin: "*",methods: ["POST", "GET"],},
});server.listen(5001, () => {console.log("listening on 5000 ...");
});io.on("connection", (socket) => {// 分發socket idsocket.emit("offer", socket.id);// 發送發起方的信令數據別answersocket.on("callUser", (data) => {io.to(data.answerId).emit("callUser", data);});// 發送接收放信令給申請方socket.on("answerSignalInfo", (data) => {io.to(data.to).emit("answerSignalInfo", data);});socket.on("disconnect", () => {socket.broadcast.emit("callEnded", socket);});
});
// frontend
// 通過socket.io和服務器進行連接
const socket = io("http://localhost:5001");// 獲取自身的socket id
socket.on("offer", (offerId) => {console.log("offer socket ID", offerId);setOfferId(offerId);getLocalStream(); // 獲取本地視頻流
});
傳遞信令數據
// 通過simple-peer 交換信令數據 offer -> 信令服務器 -> answer
const peer = new Peer({initiator: true, // 是否是發起方stream: localStream, // 傳遞的視頻流trickle: false, // 點對點傳輸,獲取單個信號// 設置STUN服務器,Chrome提供的公共服務器config: {iceServers: [{ urls: "stun:stun.l.google.com:19302" }],},
});
peer.on("signal", (data: any) => {socket.emit("callUser", {singleData: data, // 發送通話方的信令數據answerId: answerId, // 需要和誰通話from: offerId, // 誰申請通話});
});
接收信令數據
接收方接收發起方的信令數據,并保存到Peer中,然后將自身的信令數據返回給發起方
const peer = new Peer({initiator: false,stream: localStream,trickle: false,config: {iceServers: [{ urls: "stun:stun.l.google.com:19302" }],},
});
peer.on("signal", (data) => {socket.emit("answerSignalInfo", {answerSignalInfo: data,to: offerUserInfo?.id,from: offerId,});
});if (offerUserInfo?.singleData) {peer.signal(offerUserInfo.singleData);
}
對等連接建立,獲取雙方視頻流
交互信令之后,通過simple-peer成功建立對等連接,監聽stream視頻流然后顯示在頁面上
// 監聽通過對等連接傳遞的stream
peer.on("stream", (stream) => {if (remoteVideoRef.current) {remoteVideoRef.current.srcObject = stream;remoteVideoRef.current.play();}
});
完整頁面代碼:CSS樣式文件省略
import React, { useCallback, useEffect, useRef, useState } from "react";
import { io } from "socket.io-client";
import Peer from "simple-peer";
import "./App.css";const socket = io("http://localhost:5001");type UserInfo = {singleData: any;id: string;
};function App() {// 用于引用 DOM 元素const localVideoRef = useRef<HTMLVideoElement>(null);const remoteVideoRef = useRef<HTMLVideoElement>(null);// 用于管理狀態const [localStream, setLocalStream] = useState<MediaStream | undefined>();const [offerId, setOfferId] = useState("");const [answerId, setAnswerId] = useState("");const [offerUserInfo, setOfferUserInfo] = useState<UserInfo>();// 獲取本地視頻流const getLocalStream = useCallback(async () => {try {const stream = await navigator.mediaDevices.getUserMedia({video: {width: { ideal: 200 }, // 理想的寬度height: { ideal: 200 }, // 理想的高度},audio: false,});console.log("local media", stream);setLocalStream(stream);if (localVideoRef.current) {localVideoRef.current.srcObject = stream;}} catch (error) {console.error("Error accessing media devices.", error);}}, []);// 手動設置通話方idconst onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {console.log("onChange call id", e);setAnswerId(e.target.value);}, []);// 獲取信令牌服務器發送的socket idconst init = useCallback(() => {socket.on("offer", (offerId) => {console.log("offer socket ID", offerId);setOfferId(offerId);getLocalStream(); // 獲取本地視頻流});// 監聽信令服務器發送的通話申請方的信令牌數據socket.on("callUser", ({ singleData, answerId, from }) => {console.log(`${from}發起通話`, from);setOfferUserInfo({singleData: singleData,id: from,});});}, [getLocalStream]);// 創建和發送 offerconst startCall = useCallback(async () => {// 通過simple-peer 交換信令數據 offer -> 信令服務器 -> answerconst peer = new Peer({initiator: true,stream: localStream,trickle: false,// 設置STUN服務器,Chrome提供的公共服務器config: {iceServers: [{ urls: "stun:stun.l.google.com:19302" }],},});peer.on("signal", (data: any) => {socket.emit("callUser", {singleData: data, // 發送通話方的信令數據answerId: answerId, // 需要和誰通話from: offerId, // 誰申請通話});});// 獲取到接收方的信令數據socket.on("answerSignalInfo", (data) => {console.log(`${data.from}已經接受通話`, data, peer);peer.signal(data.answerSignalInfo);});// 監聽通過對等連接傳遞的streampeer.on("stream", (stream) => {if (remoteVideoRef.current) {remoteVideoRef.current.srcObject = stream;remoteVideoRef.current.play();}});// setPeer(peer);}, [answerId, localStream, offerId, remoteVideoRef]);const acceptCall = useCallback(() => {const peer = new Peer({initiator: false,stream: localStream,trickle: false,config: {iceServers: [{ urls: "stun:stun.l.google.com:19302" }],},});peer.on("signal", (data) => {socket.emit("answerSignalInfo", {answerSignalInfo: data,to: offerUserInfo?.id,from: offerId,});});if (offerUserInfo?.singleData) {peer.signal(offerUserInfo.singleData);}// 監聽通過對等連接傳遞的streampeer.on("stream", (stream) => {if (remoteVideoRef.current) {remoteVideoRef.current.srcObject = stream;remoteVideoRef.current.play();}});}, [localStream, offerUserInfo, offerId, remoteVideoRef]);useEffect(() => {init();}, [init]);return (<div className="App"><video autoPlay muted ref={localVideoRef} className="video" /><video autoPlay muted ref={remoteVideoRef} className="video" /><input value={answerId} onChange={onChange} placeholder="call id" /><button onClick={startCall}>發起通話</button><button onClick={acceptCall}>同意通話</button></div>);
}export default App;
至此可以啟動項目,并本地瀏覽器打開兩個tab即可體驗點對點視頻服務。
總結
點對點通信,主要就是信令數據的交換,知道通信雙方具體的配置信息(通信參數、IP地址等)以保證對等連接的成功建立,然后傳遞視頻流在頁面展示。
其中信令服務器僅用于對等連接前的信令交換,不會進行數據傳輸。NAT是將設備內網地址轉換為外網公共地址。STUN來獲取設置的公網地址。TURN服務器是用于對等連接異常時的兜底方案,可進行數據傳輸。