WebRTC實現(xiàn)雙端音視頻聊天功能(Vue3 + SpringBoot )
概述
- 文章描述使用WebRTC技術(shù)實現(xiàn)一對一音視頻通話。
- 由于設(shè)備攝像頭限制(一臺電腦作測試無法在開啟的雙端同時獲取攝像頭數(shù)據(jù)流),導(dǎo)致一臺電腦無法同時測試雙端,因此文章使用mp4音視頻文件模擬攝像頭音視頻數(shù)據(jù)流輸入。
- 使用技術(shù)
- 前端:Vue3,WebRTC相關(guān)API,axios
- 后端信令服務(wù)器實現(xiàn):SpringBoot,WebSocket
相關(guān)概念
- Peer-to-Peer (P2P) 連接:WebRTC主要是基于 P2P 連接的,這意味著通信是直接在兩端的瀏覽器之間進行的,而不需要經(jīng)過中介服務(wù)器(盡管可能會使用服務(wù)器來初始化和協(xié)調(diào)連接)。這種方式降低了延遲并節(jié)省了帶寬。
- SDP**(Session Description Protocol)**:**描述媒體信息(如音頻、視頻編碼格式、傳輸協(xié)議等)**的協(xié)議。例如我們在雙方構(gòu)建連接時,我們需要知道對方使用的音視頻編解碼格式,以確保雙方使用相同編解碼格式。編解碼格式就是定義在SDP信息中的其中之一的信息。
- ICE Candidate:ICE 候選是 WebRTC 在 P2P 連接過程中為尋找最佳傳輸路徑(如 STUN 或 TURN 服務(wù)器)提供的一系列地址和端口。在雙方構(gòu)建連接時需要知道對方的公網(wǎng)IP****地址和端口,以實現(xiàn)P2P連接,Candidate信息中就包含自身的公網(wǎng)IP和端口。
- STUN(Session Traversal Utilities for NAT**)服務(wù)器**:是 NAT 穿透的協(xié)議,用來獲取客戶端的公網(wǎng) IP 地址和端口。我們身處各種局域網(wǎng)中,對方如果想要和我們構(gòu)建P2P連接,就必然要知道我們的公網(wǎng)IP和端口才能和我們連接上,我們可以通過STUN服務(wù)器獲取我們的公網(wǎng)IP和端口。
- TURN(Traversal Using Relays around NAT**)服務(wù)器**:當 STUN 連接不可用時,TURN 服務(wù)器作為中繼服務(wù)器轉(zhuǎn)發(fā)數(shù)據(jù)。當STUN服務(wù)器無法幫助我們獲取公網(wǎng)IP和端口時,我們就可以使用TURN服務(wù)器作為中轉(zhuǎn)站傳遞音視頻流數(shù)據(jù)。
- 信令服務(wù)器:上面介紹了媒體信息SDP和網(wǎng)絡(luò)信息Candidate,這些實際上可以稱為"信令",我們?nèi)绻胍c對端連接,那么我們就需要知道對端的媒體信息和網(wǎng)絡(luò)信息來構(gòu)建連接,信令服務(wù)器就是幫助我們實現(xiàn)兩端的信息交換的。本文中信令服務(wù)器就是我們自己編寫的SpringBoot后端,來幫助兩端互傳連接信息。
雙端連接整體實現(xiàn)步驟概述
在大致知道了上面介紹的WebRTC基本概念之后,我們以雙端音視頻互聯(lián)的整體過程。
假設(shè)存在A端(發(fā)起端)和B端(接收端)。
1. 創(chuàng)建RTC連接對象(new RTCPeerConnection),此對象存在構(gòu)建連接時所需的API。
2. A端和B端分別連接后端WebSocket(信令服務(wù)器),以為接下來信息互傳奠定基礎(chǔ)。
3. A端創(chuàng)建媒體信息SDP(createOffer)保存到本地(setLocalDescription),將A端SDP信息通過WebSocket發(fā)送給B端。
4. B端接收到A端的SDP信息,設(shè)置為遠端媒體信息(setRemoteDescription),然后B端創(chuàng)建應(yīng)答媒體信息(實際上就是B端的媒體信息)SDP(createAnswer)保存到本地(setLocalDescription),并將B端創(chuàng)建的應(yīng)答媒體信息SDP通過WebSocket發(fā)送給A端。
5. A端收到B端發(fā)送的應(yīng)答媒體信息SDP后,保存為遠端媒體信息(setRemoteDescription)。
6. 至此,A端和B端媒體信息SDP交換完畢。
7. 開始交換網(wǎng)絡(luò)信息Candidate,我們在創(chuàng)建RTC連接對象時(步驟1)監(jiān)聽網(wǎng)絡(luò)信息的獲?。╫nicecandidate),當我們調(diào)用setRemoteDescription函數(shù)設(shè)置了遠端媒體信息之后,會觸發(fā)onicecandidate并給予condidate網(wǎng)絡(luò)信息。
8. 我們將監(jiān)聽到的網(wǎng)絡(luò)信息candidate通過WebSocket發(fā)送給對端,對端收到后將對方的網(wǎng)絡(luò)信息配置上(addIceCandidate)以實現(xiàn)連接。
9. 當媒體信息SDP和網(wǎng)絡(luò)信息Candidate互相交換并設(shè)置上之后,就可以開始音視頻流數(shù)據(jù)互傳顯示了。
10. 通過addTrack發(fā)送本地流數(shù)據(jù),通過ontrack監(jiān)聽對端音視頻流數(shù)據(jù)的發(fā)送,監(jiān)聽到就顯示對端音視頻。
媒體協(xié)商和網(wǎng)絡(luò)協(xié)商時序圖:
**總結(jié):**在視頻互傳之前重要的就是交換媒體SDP信息和網(wǎng)絡(luò)Candidate信息(媒體和網(wǎng)絡(luò)協(xié)商),當雙方都獲取到對方的媒體和網(wǎng)絡(luò)信息之后。就能夠成功構(gòu)建連接并傳遞音視頻數(shù)據(jù)了。
文章代碼實現(xiàn)注意點
在最開始的概述中有提到,本文提供的1對1音視頻聊天代碼示例中沒有真實調(diào)用用戶攝像頭獲取音視頻流數(shù)據(jù),因為作者只有一臺電腦,為了可以更方便的在一臺電腦上開啟兩端并測試,因此使用了MP4音視頻作為音視頻流數(shù)據(jù)輸入作為測試。
這實際上并不會和真實開啟攝像頭獲取音視頻數(shù)據(jù)流有很大的區(qū)別。僅僅是獲取流數(shù)據(jù)的方式不同罷了。
在真實的場景下,可以使用API:getUserMedia去獲取攝像頭音視頻流數(shù)據(jù)即可。
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
STUN和TURN服務(wù)器的搭建
為了能夠獲取到我們本地的公網(wǎng)IP和端口去和對端創(chuàng)建連接,我們可以嘗試去搭建STUN服務(wù)器和TURN中繼服務(wù)器。
**注:**此步驟不是一定需要做,因為Google給我們提供了一個免費公用的STUN服務(wù)器地址:stun:stun.l.google.com:19302,如果你發(fā)現(xiàn)用不了,或需要搭建復(fù)雜的音視頻通話應(yīng)用,還是推薦自己搭建一下STUN/TURN服務(wù)器。
我們直接搭建開源的Coturn服務(wù)器即可,因為Coturn 同時支持 TURN 和 STUN 協(xié)議。
下面會介紹在CentOS8中搭建Coturn服務(wù)器步驟:
1. 安裝所需依賴包
yum install -y make gcc cc gcc-c++ wget openssl-devel libevent libevent-devel openssl
2. yum直接一鍵下載安裝
sudo yum install coturn # (驗證安裝)安裝程序結(jié)束后執(zhí)行如下命令查看是否正確輸出turnserver路徑 which turnserver
3. 配置Coturn相關(guān)屬性,找到配置文件路徑:
find / -name turnserver.conf
4. 獲取服務(wù)器內(nèi)網(wǎng)IP和公網(wǎng)IP
# 輸入命令查看Ip ifconfig
找到自己啟用的網(wǎng)絡(luò)下的內(nèi)網(wǎng)IP,公網(wǎng)IP就是你連接服務(wù)器的IP地址。
5. 使用openSSL生成cert和pkey配置的自簽名證書
openssl req -x509 -newkey rsa:2048 -keyout /turn_server_pkey.pem -out /turn_server_cert.pem -days 999 -nodes
輸入上面命令后,填寫一下證書的一些信息(城市,地區(qū)等),隨便填一下回車回車!就行。
上面的/turn_server_pkey.pem和/turn_server_cert.pem 請自己設(shè)置好保存證書的路徑,上面默認放到了根路徑下。
6. 編輯剛才找到的配置文件
將下面的配置部分修改后替換掉原配置文件的所有內(nèi)容。
# 網(wǎng)卡名 relay-device=eth0 #內(nèi)網(wǎng)IP listening-ip=172.24.52.189 listening-port=3478 #內(nèi)網(wǎng)IP,加密訪問配置 relay-ip=172.24.52.189 tls-listening-port=5349 # 外網(wǎng)IP external-ip=自己的外網(wǎng)IP relay-threads=500 #打開密碼驗證 lt-cred-mech cert=/turn_server_cert.pem pkey=/turn_server_pkey.pem min-port=40000 max-port=65535 #設(shè)置用戶名和密碼,創(chuàng)建IceServer時使用 user=user:123456 # 外網(wǎng)IP綁定的域名 realm=你自己IP綁定的域名 # 服務(wù)器名稱,用于OAuth認證,默認和realm相同,部分瀏覽器本段不設(shè)可能會引發(fā)cors錯誤。 server-name=你自己IP綁定的域名 # 認證密碼,和前面設(shè)置的密碼保持一致 cli-password=123456
7. 開啟端口訪問
7.1 開啟云服務(wù)器安全組端口
開啟4000-65535端口的原因:外部客戶端與 TURN 服務(wù)器的通信使用動態(tài)端口。通常,操作系統(tǒng)會為每個連接分配一個臨時端口(通常是大于 1024 的端口),而 40000 到 65535 端口 作為 高端端口,是常用的臨時端口范圍。因此,為了確保 TURN 服務(wù)器能夠處理大量的并發(fā)連接,并為每個連接分配一個端口,需要確保 TURN 服務(wù)器的端口范圍足夠大。
7.2開啟本地防火墻端口
#開放端口 firewall-cmd --zone=public --add-port=3478/udp --permanent firewall-cmd --zone=public --add-port=3478/tcp --permanent #重啟防火墻 firewall-cmd --reload
8. 啟動Coturn服務(wù)器
turnserver -o -a -f
9. 測試啟動狀態(tài)
訪問測試網(wǎng)站:Trickle ICE
開發(fā)過程描述
如下僅展示關(guān)鍵性代碼解釋說明,具體代碼請到文章最后獲取Gitee源碼地址。
后端開發(fā)流程
- websocket連接成功后維護用戶連接信息并廣播join消息。數(shù)據(jù)攜帶用戶ID列表。
// 后端維護Session連接的數(shù)據(jù)結(jié)構(gòu)
private final HashMap<String, WebSocketSession> userMap = new HashMap<>();
- 編寫接收信息通用接口,dto對象包含userID,type,data(JSON序列化字符串),接口根據(jù)傳入userId取出session,給session發(fā)送消息對象。
前端開發(fā)流程
- 日志系統(tǒng),監(jiān)聽ice狀態(tài)及日志打印。
- 創(chuàng)建隨機ID,連接ws。
- 協(xié)商函數(shù):協(xié)商前創(chuàng)建peerConnection對象并監(jiān)聽candidate,當雙方都連接成功后調(diào)用,判斷本地offerFlag狀態(tài),如果為true,創(chuàng)建offer設(shè)置本地并發(fā)送消息給對端。
// STUN 服務(wù)器 const iceServers = [ { urls: “stun:stun.l.google.com:19302” // Google公開的STUN 服務(wù)器 }, { urls: “stun:自己的STUN服務(wù)器IP:3478” // 自己的Stun服務(wù)器 }, { urls: “turn:自己的TRUN服務(wù)器IP:3478”, // 自己的TURN服務(wù)器 username: “userName”, credential: “Password” } ]; // 創(chuàng)建RTC連接對象并監(jiān)聽和獲取condidate信息 function createPeerConnection() { wlog(“開始創(chuàng)建PC對象…”) peerConnection = new RTCPeerConnection(iceServers); wlog(“創(chuàng)建PC對象成功”) // 創(chuàng)建RTC連接對象后連接websocket initWebSocket(); // 監(jiān)聽網(wǎng)絡(luò)信息(ICE Candidate) peerConnection.onicecandidate = (event) => { if (event.candidate) { candidateInfo = event.candidate; wlog(“candidate信息變化…”); // 將candidate信息發(fā)送給遠端 setTimeout(()=>{ sendCandidate(event.candidate); }, 150) } }; // 監(jiān)聽遠端音視頻流 peerConnection.ontrack = (event) => { nextTick(() => { wlog(“> 收到遠端數(shù)據(jù)流 <=”) if (!remoteVideo.value.srcObject) { remoteVideo.value.srcObject = event.streams[0]; remoteVideo.value.play(); // 強制播放 } }); // remoteVideo.value.srcObject = event.streams[0]; }; // 監(jiān)聽ice連接狀態(tài) peerConnection.oniceconnectionstatechange = () => { wlog(RTC連接狀態(tài)改變:${peerConnection.iceConnectionState}); }; // 添加本地音視頻流到 PeerConnection localStream.getTracks().forEach(track => { peerConnection.addTrack(track, localStream); }); }
- candidate監(jiān)聽:當監(jiān)聽到candidate后判斷雙方是否已連接,如果已連接,構(gòu)造并發(fā)送candidate給對端。
- 解析消息處理器
- 解析join:type為join取出userId列表,如果為一個代表僅自己在線,標識為創(chuàng)建offer端,日志打印相關(guān)信息,如果有兩個者取出對方ID保存,代表雙方都上線成功,日志打印,調(diào)用協(xié)商函數(shù),開始媒體協(xié)商和網(wǎng)絡(luò)協(xié)商。
- 解析offer:type為offer,說明收到發(fā)起端offer,將offer設(shè)置為遠端信息,然后創(chuàng)建answer設(shè)置到本地,構(gòu)建answer消息發(fā)送給對端。
- 解析answer:type為answer,說明收到接收端應(yīng)答,取出answer設(shè)置為遠端消息。
- 解析candidate:type為candidate,說明收到對端的網(wǎng)絡(luò)信息,取出設(shè)置到本地。
// 消息處理器 - 解析器 function handleSignalingMessage(message) { wlog(“收到ws消息,開始解析…”) wlog(message) let parseMsg = JSON.parse(message); wlog(解析結(jié)果:${parseMsg}); if (parseMsg.type == “join”) { joinHandle(parseMsg.data); } else if (parseMsg.type == “offer”) { wlog(“收到發(fā)起端offer,開始解析…”); offerHandle(parseMsg.data); } else if (parseMsg.type == “answer”) { wlog(“收到接收端的answer,開始解析…”); answerHandle(parseMsg.data); }else if(parseMsg.type == “candidate”){ wlog(“收到遠端candidate,開始解析…”); candidateHandle(parseMsg.data); } } // 遠端Candidate處理器 async function candidateHandle(candidate){ peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate))); wlog(“+++++++ 本端candidate設(shè)置完畢 ++++++++”); } // 接收端的answer處理 async function answerHandle(answer) { wlog(“將answer設(shè)置為遠端信息”); peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(answer))); // 設(shè)置遠端SDP } // 發(fā)起端offer處理器 async function offerHandle(offer) { wlog(“將發(fā)起端的offer設(shè)置為遠端媒體信息”); await peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(offer))); wlog(“創(chuàng)建Answer 并設(shè)置到本地”); let answer = await peerConnection.createAnswer() await peerConnection.setLocalDescription(answer); wlog(“發(fā)送answer給發(fā)起端”); // 構(gòu)造answer消息發(fā)送給對端 let paramObj = { userId: oppositeUserId, type: “answer”, data: JSON.stringify(answer) } // 執(zhí)行發(fā)送 const res = await axios.post(${BaseUrl}/rtcs/sendMessage, paramObj); } // 加入處理器 function joinHandle(userIds) { // 判斷連接的用戶個數(shù) if (userIds.length == 1 && userIds[0] == userId) { wlog(“標識為發(fā)起端,等待對方加入房間…”) isRoomEmpty.value = true; // 存在一個連接并且是自身,標識我們是發(fā)起端 offerFlag = true; } else if (userIds.length > 1) { // 對方加入了 wlog(“對方已連接…”) isRoomEmpty.value = false;
// 取出對方ID for (let id of userIds) { if (id != userId) { oppositeUserId = id; } } wlog(`對端ID: ${oppositeUserId}`) // 開始交換SDP和Candidate swapVideoInfo() } }
效果演示
初始狀態(tài)
發(fā)起端加入房間
接收端加入房間
Gitee源碼地址
源碼地址:點擊訪問Gitee項目源代碼。
到此這篇關(guān)于WebRTC實現(xiàn)雙端音視頻聊天(Vue3 + SpringBoot )的文章就介紹到這了,更多相關(guān)WebRTC雙端音視頻聊天內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot中使用@Transactional注解事物不生效的坑
這篇文章主要介紹了springboot中使用@Transactional注解事物不生效的原因,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01劍指Offer之Java算法習(xí)題精講數(shù)組與列表的查找及字符串轉(zhuǎn)換
跟著思路走,之后從簡單題入手,反復(fù)去看,做過之后可能會忘記,之后再做一次,記不住就反復(fù)做,反復(fù)尋求思路和規(guī)律,慢慢積累就會發(fā)現(xiàn)質(zhì)的變化2022-03-03springBoot Junit測試用例出現(xiàn)@Autowired不生效的解決
這篇文章主要介紹了springBoot Junit測試用例出現(xiàn)@Autowired不生效的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09SpringBoot實現(xiàn)EMQ設(shè)備的上下線告警
EMQX?的上下線系統(tǒng)消息通知功能在客戶端連接成功或者客戶端斷開連接,需要實現(xiàn)設(shè)備的上下線狀態(tài)監(jiān)控,所以本文給大家介紹了如何通過SpringBoot實現(xiàn)EMQ設(shè)備的上下線告警,文中有詳細的代碼示例,需要的朋友可以參考下2023-10-10Python中scrapy框架的ltem和scrapy.Request詳解
這篇文章主要介紹了Python中scrapy框架的ltem和scrapy.Request詳解,Item是保存爬取數(shù)據(jù)的容器,它的使用方法和字典類似,不過,相比字典,Item提供了額外的保護機制,可以避免拼寫錯誤或者定義字段錯誤,需要的朋友可以參考下2023-09-09