WebRTC的學(xué)習(xí)
1. 相關(guān)地址
1.1 文檔教學(xué)
? WebRTC中文網(wǎng):http://webrtc.p2hp.com/#google_vignette
? WebRTC中文社區(qū):https://webrtc.org.cn/
? WebRTC英文官網(wǎng):https://webrtc.org/
? WebRTC安全相關(guān):http://webrtc-security.github.io/
? coturn開(kāi)源地址:https://github.com/coturn/coturn
? stun、trun測(cè)試網(wǎng)站:https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
? NAT知識(shí):
? ?? P2P知識(shí):https://zhuanlan.zhihu.com/p/488135992
? ?? P2P技術(shù)詳解(一):NAT詳解——詳細(xì)原理、P2P簡(jiǎn)介https://www.cnblogs.com/mlgjb/p/8243646.htm
? ?? P2P技術(shù)詳解(二):P2P中的NAT穿越(打洞)方案詳解 https://www.jianshu.com/p/9bfbcbee0abb
? ?? P2P技術(shù)詳解(三):P2P技術(shù)之STUN、TURN、ICE詳解 https://www.jianshu.com/p/258e7d8be2ba
? ?? 詳解P2P技術(shù)中的NAT穿透原理 https://www.jianshu.com/p/f71707892eb2
? WebRTC 代碼相關(guān)博客:
? ?? https://www.bbsmax.com/A/B0zqLrWNdv/
? ?? https://www.an.rustfisher.com/webrtc/web-samples/getUserMedia-open-camera/
? ?? https://github.com/shushushv/webrtc-p2p
?? WebRTC原理簡(jiǎn)述:https://www.jianshu.com/p/476f39de86ed
?? WebRTC介紹及簡(jiǎn)單使用:https://zhuanlan.zhihu.com/p/490239698
? 后續(xù)補(bǔ)充…
? 注意:
? - 如果只是初期入門只需要看第一個(gè)中文官網(wǎng)就行了,如果追求最新的文檔去英文官網(wǎng)。
? - 我這里的案例實(shí)現(xiàn)代碼時(shí)使用了大佬的代碼(代碼相關(guān)博客里的第一個(gè)博客)。
? - 代碼實(shí)現(xiàn)時(shí)強(qiáng)力推薦去看其他相關(guān)博客,寫(xiě)的很不錯(cuò)。
?文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-538913.html
1.2 視頻教學(xué)
? b站學(xué)習(xí)地址:https://www.bilibili.com/video/BV1D14y1W7qp?p=1&vd_source=d6cd8b3f892acbf22f02da2bfa7d95fe
我本人也是看該視頻進(jìn)行入門的,非常感謝該UP和講師,講的挺好的,缺點(diǎn)是視頻不夠清晰。
?
2. 簡(jiǎn)述
2.1 什么是WebRTC?
? WebRTC (Web Real-Time Communications) 是一項(xiàng)實(shí)時(shí)通訊技術(shù),它允許網(wǎng)絡(luò)應(yīng)用或者站點(diǎn),在不借助中間媒介的情況下,建立瀏覽器之間點(diǎn)對(duì)點(diǎn)(Peer-to-Peer)的連接,實(shí)現(xiàn)視頻流和(或)音頻流或者其他任意數(shù)據(jù)的傳輸。WebRTC 包含的這些標(biāo)準(zhǔn)使用戶在無(wú)需安裝任何插件或者第三方的軟件的情況下,創(chuàng)建點(diǎn)對(duì)點(diǎn)(Peer-to-Peer)的數(shù)據(jù)分享和電話會(huì)議成為可能。
? 簡(jiǎn)單來(lái)說(shuō),就是可以實(shí)現(xiàn)例如微信電話(實(shí)時(shí)通訊)的技術(shù),并且它不需要任何第三方插件的和軟件的限制,在瀏覽器里就可以實(shí)現(xiàn)視頻通話。
?
2.2 WebRtc可以做什么?
? WebRTC 有許多不同的用例,從使用攝像頭或麥克風(fēng)的基本 Web 應(yīng)用程序到更高級(jí)的視頻通話應(yīng)用程序和屏幕共享。我們收集了許多代碼示例,以更好地說(shuō)明該技術(shù)的工作原理以及您可以使用它的用途.
? WebRTC提供了視頻會(huì)議的核心技術(shù),包括音視頻的采集、編解碼、網(wǎng)絡(luò)傳輸、展示等功能,并且還支持跨平臺(tái),包括linux、windows、mac、android等。
? 這句話是WebRTC中文網(wǎng)內(nèi)的,里面還有很多小例子,示例。
? 我建議先把通信原理看了之后再去寫(xiě)例子。
?
2.3 WebRTC的架構(gòu)
?
2.3.1 WebRTC架構(gòu)組件介紹
Your Web App
? Web開(kāi)發(fā)者開(kāi)發(fā)的程序,Web開(kāi)發(fā)者可以基于集成WebRTC的瀏覽器提供的web API開(kāi)發(fā)基于視頻、音頻的實(shí)時(shí)通信 應(yīng)用。
Web API
? 面向第三方開(kāi)發(fā)者的WebRTC標(biāo)準(zhǔn)API(Javascript),使開(kāi)發(fā)者能夠容易地開(kāi)發(fā)出類似于網(wǎng)絡(luò)視頻聊天的web應(yīng)用, 最新的標(biāo)準(zhǔn)化進(jìn)程可以查看這里。
?
WebRTC Native C++ API
? 本地C++ API層,使瀏覽器廠商容易實(shí)現(xiàn)WebRTC標(biāo)準(zhǔn)的Web API,抽象地對(duì)數(shù)字信號(hào)過(guò)程進(jìn)行處理。
?
Transport / Session
? 傳輸/會(huì)話層
? 會(huì)話層組件采用了libjingle庫(kù)的部分組件實(shí)現(xiàn),無(wú)須使用xmpp/jingle協(xié)議 。
?
VoiceEngine
? 音頻引擎是包含一系列音頻多媒體處理的框架。
? PS:VoiceEngine是WebRTC極具價(jià)值的技術(shù)之一,是Google收購(gòu)GIPS公司后開(kāi)源的。在VoIP上,技術(shù)業(yè)界領(lǐng)先。
? Opus:支持從6 kbit/s到510 kbit/s的恒定和可變比特率編碼,幀大小從2.5 ms到60 ms,各種采樣率從8 kHz(4 kHz 帶寬)到48 kHz(20 kHz帶寬,可復(fù)制人類聽(tīng)覺(jué)系統(tǒng)的整個(gè)聽(tīng)力范圍)。由IETF RFC 6176定義。
? NetEQ 模塊是Webrtc語(yǔ)音引擎中的核心模塊 ,一種動(dòng)態(tài)抖動(dòng)緩沖和錯(cuò)誤隱藏算法,用于隱藏網(wǎng)絡(luò)抖動(dòng)和數(shù)據(jù)包丟失 的負(fù)面影響。保持盡可能低的延遲,同時(shí)保持最高的語(yǔ)音質(zhì)量。
?
VideoEngine
? WebRTC視頻處理引擎
? VideoEngine是包含一系列視頻處理的整體框架,從攝像頭采集視頻到視頻信息網(wǎng)絡(luò)傳輸再到視頻顯示整個(gè)完整過(guò)程 的解決方案。
? VP8 視頻圖像編解碼器,是WebRTC視頻引擎的默認(rèn)的編解碼器 。
? VP8適合實(shí)時(shí)通信應(yīng)用場(chǎng)景,因?yàn)樗饕轻槍?duì)低延時(shí)而設(shè)計(jì)的編解碼器。
?
2.4 WebRTC的原理
? 首先思考的問(wèn)題:兩個(gè)不同網(wǎng)絡(luò)環(huán)境的(具備攝像頭/麥克風(fēng)多媒體設(shè)備的)瀏覽器,要實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn) 的實(shí)時(shí)音視頻對(duì) 話,難點(diǎn)在哪里?
? 1. 了解對(duì)方的媒體格式、支持的最大分辨率和其他媒體信息?
? 2. 要了解彼此的網(wǎng)絡(luò),就有可能找到一條通信鏈路?
? 3. 兩個(gè)終端還沒(méi)有建立連接時(shí),如何交換“媒體信息”和“網(wǎng)絡(luò)信息”呢?
?
2.4.1 媒體協(xié)商(sdp)
? 彼此要了解對(duì)方支持的媒體格式?
? 為了保證兩端都有正確的編碼和解碼,最簡(jiǎn)單的方法就是取它們的交集H264
? 注意:有一種特殊的協(xié)議叫做 Session Description protocol (SDP) ,可以用來(lái)描述上述信息。
? 媒體協(xié)商:在webrtc中,參與視頻通信的雙方必須首先交換SDP信息,這樣雙方才能知根知底,而交換SDP的過(guò)程,也稱為"媒體協(xié)商"。
?
2.4.2 網(wǎng)絡(luò)協(xié)商(candidate)
? 同樣,在復(fù)雜的網(wǎng)絡(luò)環(huán)境中,要在兩端之間建立連接,必須有一個(gè)雙方都可以訪問(wèn)的鏈路。
? 理想的網(wǎng)絡(luò)情況是每個(gè)瀏覽器的電腦都是私有公網(wǎng)IP,可以直接進(jìn)行點(diǎn)對(duì)點(diǎn)連接。
?
? 實(shí)際情況是我們的電腦和電腦之前或大或小都是在某個(gè)局域網(wǎng)中,需要NAT(Network Address Translation,網(wǎng)絡(luò)地址轉(zhuǎn)換)。
? 注意:如果是在同一個(gè)局域網(wǎng)中,那么直接使用相同的內(nèi)網(wǎng)網(wǎng)段就可以了,但是一般情況下都是不同的。
? 在中國(guó)的網(wǎng)絡(luò)環(huán)境下,據(jù)統(tǒng)計(jì),至少有一半的網(wǎng)絡(luò)不能直接連接。我個(gè)人認(rèn)為根本原因是:在互聯(lián)網(wǎng)發(fā)展的早期,絕大多數(shù)IP4地址資源都被國(guó)外所占據(jù)。當(dāng)輪到中國(guó)等發(fā)展中國(guó)家使用IP地址時(shí),大多數(shù)計(jì)算機(jī)沒(méi)有公網(wǎng)IP地址,只能通過(guò)路由器和交換機(jī)進(jìn)行NAT轉(zhuǎn)換,相當(dāng)一部分NAT是對(duì)稱的?;旧?,沒(méi)有辦法播放它。
? 那么我們就需要使用NAT進(jìn)行轉(zhuǎn)換。
?
? 所以在解決 WebRTC 使用過(guò)程中的上述問(wèn)題的時(shí)候,我們需要用到 STUN 和 TURN 。
?
2.4.2.1 STUN
? STUN(Session Traversal Utilities for NAT,NAT會(huì)話穿越應(yīng)用程序)是一種網(wǎng)絡(luò)協(xié)議,它允許位于NAT(或多重 NAT)后的客戶端找出自己的公網(wǎng)地址,查出自己位于哪種類型的NAT之后以及NAT為某一個(gè)本地端口所綁定的 Internet端端口。這些信息被用來(lái)在兩個(gè)同時(shí)處于NAT路由器之后的主機(jī)之間創(chuàng)建UDP通信。該協(xié)議由 RFC 5389 定 義。
? 在遇到上述情況的時(shí)候,我們可以建立一個(gè)STUN服務(wù)器,這個(gè)服務(wù)器做什么用的呢?主要是給無(wú)法在公網(wǎng)環(huán)境下的 視頻通話設(shè)備分配公網(wǎng)IP用的。這樣兩臺(tái)電腦就可以在公網(wǎng)IP中進(jìn)行通話。
? 使用一句話說(shuō)明 STUN 做的事情就是:告訴我你的 公網(wǎng)IP地址+端口 是什么?
? 搭建STUN服務(wù)器很簡(jiǎn)單,媒體流傳輸是按照 P2P 的方式。
? 那么問(wèn)題來(lái)了, STUN 并不是每次都能成功的為需要 NAT 的通話設(shè)備分配 IP 地址的,P2P 在傳輸媒體流時(shí),使用的本地帶寬,在多人視頻通話的過(guò)程中,通話質(zhì)量的好壞往往需要根據(jù)使用者本地的帶寬確定。
? 那么怎么辦? TURN 可以 很好的解決這個(gè)問(wèn)題。
?
2.4.2.2 TURN
? TURN 的全稱為 Traversal Using Relays around NAT ,是 STUN/RFC5389 的一個(gè)拓展,主要添加了 Relay 功能。如果終端在 NAT 之后, 那么在特定的情景下,有可能使得終端無(wú)法和其對(duì)等端(peer)進(jìn)行直接的通信,這時(shí)就需要公網(wǎng) 的服務(wù)器作為一個(gè) 中繼 , 對(duì)來(lái)往的數(shù)據(jù)進(jìn)行轉(zhuǎn)發(fā)。
? 這個(gè)轉(zhuǎn)發(fā)的協(xié)議就被定義為TURN。
? 在 STUN 分配公網(wǎng)IP 失敗 后,可以通過(guò) TURN服務(wù)器 請(qǐng)求公網(wǎng)IP地址作為中繼地址。
? 這種方式的帶寬由服務(wù)器端承擔(dān),在多人視頻聊天的時(shí)候,本地帶寬壓力較小,并且根據(jù) Google 的說(shuō)明,TURN協(xié)議 可以使用在所有的環(huán)境中。
?
2.4.2.3 總結(jié)
? (單向數(shù)據(jù)200kbps 一對(duì)一通話)
? 以上是 WebRTC 中經(jīng)常用到的2個(gè)協(xié)議,STUN 和 TURN 服務(wù)器我們使用 coturn開(kāi)源項(xiàng)目 來(lái)搭建。
? 補(bǔ)充:ICE 跟 STUN 和 TURN 不一樣,ICE不是一種協(xié)議,而是一個(gè)框架(Framework),它整合了STUN 和 TURN 。
? coturn開(kāi)源項(xiàng)目 集成了 STUN 和 TURN的功能。
? 在WebRTC中用來(lái)描述網(wǎng)絡(luò)信息的術(shù)語(yǔ)叫 candidate 。
? - 媒體協(xié)商 sdp
? - 網(wǎng)絡(luò)協(xié)商 candidate
?
2.4.2 媒體協(xié)商+網(wǎng)絡(luò)協(xié)商數(shù)據(jù)的交換通道
? 從上面 [2.4.1 媒體協(xié)商(sdp)](#2.4.1 媒體協(xié)商(sdp)) 和 [2.4.2 網(wǎng)絡(luò)協(xié)商(candidate)](2.4.2 網(wǎng)絡(luò)協(xié)商(candidate)) ,我們知道了2個(gè)客戶端,那怎么去交換?是不是需要一個(gè)中間商去做交換?
? 所以 我們需要一個(gè) 信令服務(wù)器( Signal server )轉(zhuǎn)發(fā)彼此的媒體信息和網(wǎng)絡(luò)信息。
? 如上圖,我們?cè)诨?WebRTC API 開(kāi)發(fā)應(yīng)用(APP)時(shí),可以將彼此的APP連接到信令服務(wù)器(
? Signal Server ,一般 搭建在公網(wǎng),或者兩端都可以訪問(wèn)到的局域網(wǎng)),借助信令服務(wù)器,就可以實(shí)現(xiàn)上面提到的 SDP 媒體信息及 Candidate 網(wǎng)絡(luò)信息交換。
? 我簡(jiǎn)單理解為如下:
? 你給朋友寫(xiě)信,信寫(xiě)好了需要寄給朋友,把信放到信箱由郵局進(jìn)行派送,郵遞員交到朋友手上,但是每個(gè)地方這么大,不可能是一個(gè)郵遞員送,肯定是由當(dāng)?shù)氐泥]局進(jìn)行分發(fā)派送。
? 那么,在這個(gè)過(guò)程中郵局(郵遞員)充當(dāng)了信令服務(wù)器(Signal Server)的作用,你寫(xiě)的信就是sdp,Candidate 就是你填寫(xiě)的朋友地址,這個(gè)地址可能因?yàn)楹艽?,?dǎo)致需要根據(jù)地址(NAT/Relay)進(jìn)行郵局分發(fā),例如市郵局 -> 縣郵局 -> 鄉(xiāng)鎮(zhèn)郵局 ->郵遞員。
? NAT: 你主動(dòng)根據(jù)郵局地址去拿。
? Relay:讓郵遞員送上門。
?
? 交換SDP的過(guò)程:
?
? 1. Amy(假設(shè)一個(gè)人的名字)通過(guò)setLocalDescription方法保存自己的SDP信息,然后通過(guò)offer方法發(fā)送給信令服務(wù)器。
? 2. 信息服務(wù)器將Amy的SDP轉(zhuǎn)發(fā)給另一端的Bob(另一個(gè)虛構(gòu)的名字),Bob將首先調(diào)用setremotedescription來(lái)保存Amy的SDP。
? 3. 然后Bob調(diào)用setLocalDescription方法來(lái)保存他的SDP,然后使用answer方法通過(guò)信令服務(wù)器將他的SDP發(fā)送給Amy。
? 4. Amy收到Bob的SDP后,調(diào)用setRemoteDescription進(jìn)行保存,雙方完成SDP交換,找到交集。如果他們能達(dá)成協(xié)議,他們就可以建立一個(gè)p2p連接并開(kāi)始通信。
?
2.5 WebRTC如何查看APIs?
? 英文網(wǎng)址:[媒體設(shè)備入門 | WebRTC中文網(wǎng) (p2hp.com)]
? 如果看英文吃力,可以選擇如下地址學(xué)習(xí)部分示例:
? WebRTC打開(kāi)本地?cái)z像頭 - RustFisher 安卓|Java|設(shè)計(jì)模式|WebRTC|Python|NestJS|PyQt
?
3. 安裝Coturn穿透和轉(zhuǎn)發(fā)服務(wù)器
3.1 安裝依賴
ubuntu系統(tǒng)
sudo apt-get install libssl-dev
sudo apt-get install libevent-dev
?
centos系統(tǒng)
sudo yum install openssl-devel
sudo yum install libevent-devel
?
3.2 下載源碼進(jìn)行編譯安裝Coturn
# 本次所需完整執(zhí)行代碼
git clone https://github.com/coturn/coturn
cd coturn
./configure
make
sudo make install
?
進(jìn)行 ./configure 的時(shí)候報(bào)錯(cuò),如下圖:
?
查閱后表示需要安裝 g++
sudo apt-get install g++
?
缺少 pkg-config
sudo apt-get install pkg-config
?
再次進(jìn)行:./configure
?
make
?
sudo make install
3.3 啟動(dòng)并測(cè)試
啟動(dòng)
# 啟動(dòng)
sudo nohup turnserver -L 0.0.0.0 -a -u lqf:123456 -v -f -r nort.gov &
?
測(cè)試
? 測(cè)試網(wǎng)站:Trickle ICE (webrtc.github.io)
?
? 測(cè)試stun:
? 注意:stun不需要username和password,我這里是多寫(xiě)了。
?
測(cè)試turn:
? 注意:測(cè)試trun需要username和password,且 IceTransports value 選擇為relay。
? 我這里不知道為什么是701,查了一些資料也沒(méi)有頭緒,因?yàn)樵品?wù)器過(guò)期了,就使用的本地虛擬機(jī),所以不知道是不是因?yàn)檫@個(gè),到時(shí)候再試試。
?
4. 案例實(shí)現(xiàn)
4.1 一對(duì)一通話
4.1.1 一對(duì)一通話場(chǎng)景
? 在一對(duì)一通話場(chǎng)景中,每個(gè) Peer 均創(chuàng)建有一個(gè) PeerConnection 對(duì)象,由一方主動(dòng)發(fā) Offer SDP,另一方則應(yīng)答 AnswerSDP,最后雙方交換 ICE Candidate 從而完成通話鏈路的建立。但是在中國(guó)的網(wǎng)絡(luò)環(huán)境中,據(jù)一些統(tǒng)計(jì)數(shù)據(jù)顯示,至少1半的網(wǎng)絡(luò)是無(wú)法直接穿透打通,這種情況下只能借助 TURN 服務(wù)器中轉(zhuǎn)。
?
4.1.2 一對(duì)一通話場(chǎng)景代碼實(shí)現(xiàn)
技術(shù)選型
? ? 前端 -> html、css、js、WebRTC、websocket
? ? 后端 -> springboot + websocket
?
整體執(zhí)行流程
?
頁(yè)面代碼
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>WebRTC + WebSocket</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<style>
html,
body {
margin: 0;
padding: 0;
}
#main {
position: absolute;
width: 370px;
height: 550px;
}
#localVideo {
position: absolute;
background: #757474;
top: 10px;
right: 10px;
width: 100px;
height: 150px;
z-index: 2;
}
#remoteVideo {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background: #222;
}
#buttons {
z-index: 3;
bottom: 20px;
left: 90px;
position: absolute;
}
#toUser {
border: 1px solid #ccc;
padding: 7px 0px;
border-radius: 5px;
padding-left: 5px;
margin-bottom: 5px;
}
#toUser:focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6)
}
#call {
width: 70px;
height: 35px;
background-color: #00BB00;
border: none;
margin-right: 25px;
color: white;
border-radius: 5px;
}
#hangup {
width: 70px;
height: 35px;
background-color: #FF5151;
border: none;
color: white;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="main">
<video id="remoteVideo" playsinline autoplay></video>
<video id="localVideo" playsinline autoplay muted></video>
<div id="buttons">
<span id="myName" style="color: red;"></span>
<input id="toUser" placeholder="輸入在線好友賬號(hào)" /><br />
<button id="call">視頻通話</button>
<button id="hangup">掛斷</button>
</div>
</div>
</body>
<!-- 可引可不引 -->
<!--<script th:src="@{/js/adapter-2021.js}"></script>-->
<script type="text/javascript" th:inline="javascript">
// 生成一個(gè)隨機(jī)的用戶名
let username = '' + Math.floor(Math.random() * (100 - 1) + 1);
if(username){
document.getElementById("myName").innerText = username
}
let localVideo = document.getElementById('localVideo');
let remoteVideo = document.getElementById('remoteVideo');
let websocket = null;
let peer = null;
WebSocketInit();
ButtonFunInit();
/* WebSocket */
function WebSocketInit() {
//判斷當(dāng)前瀏覽器是否支持WebSocket
if ('WebSocket' in window) {
// 使用192.168.8.57的目的是打包成app的時(shí)候可以通過(guò)局域網(wǎng)訪問(wèn)
url = "ws://192.168.8.57:8080/webrtc/" + username
// url = "ws://127.0.0.1:8080/webrtc/" + username
websocket = new WebSocket(url);
} else {
alert("當(dāng)前瀏覽器不支持WebSocket!");
}
//連接發(fā)生錯(cuò)誤的回調(diào)方法
websocket.onerror = function (e) {
console.log(e)
alert("WebSocket連接發(fā)生錯(cuò)誤!");
};
//連接關(guān)閉的回調(diào)方法
websocket.onclose = function () {
console.error("WebSocket連接關(guān)閉");
};
//連接成功建立的回調(diào)方法
websocket.onopen = function () {
console.log("WebSocket連接成功");
};
//接收到消息的回調(diào)方法
websocket.onmessage = async function (event) {
let { type, fromUser, msg, sdp, iceCandidate } = JSON.parse(event.data.replace(/\n/g, "\\n").replace(/\r/g, "\\r"));
console.log(type);
if (type === 'hangup') {
console.log(msg);
document.getElementById('hangup').click();
return;
}
if (type === 'call_start') {
// msg = 0 表示拒絕,1表示同意
let msg = "0"
if (confirm(fromUser + "發(fā)起視頻通話,確定接聽(tīng)嗎") == true) {
document.getElementById('toUser').value = fromUser;
WebRTCInit();
msg = "1"
}
websocket.send(JSON.stringify({
type: "call_back",
toUser: fromUser,
fromUser: username,
msg: msg
}));
return;
}
if (type === 'call_back') {
if (msg === "1") {
console.log(document.getElementById('toUser').value + "同意視頻通話");
//創(chuàng)建本地視頻并發(fā)送offer
let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
localVideo.srcObject = stream;
stream.getTracks().forEach(track => {
peer.addTrack(track, stream);
});
let offer = await peer.createOffer();
await peer.setLocalDescription(offer);
let newOffer = offer.toJSON();
newOffer["fromUser"] = username;
newOffer["toUser"] = document.getElementById('toUser').value;
websocket.send(JSON.stringify(newOffer));
} else if (msg === "0") {
alert(document.getElementById('toUser').value + "拒絕視頻通話");
document.getElementById('hangup').click();
} else {
alert(msg);
document.getElementById('hangup').click();
}
return;
}
if (type === 'offer') {
let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localVideo.srcObject = stream;
stream.getTracks().forEach(track => {
peer.addTrack(track, stream);
});
await peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }));
let answer = await peer.createAnswer();
let newAnswer = answer.toJSON();
newAnswer["fromUser"] = username;
newAnswer["toUser"] = document.getElementById('toUser').value;
websocket.send(JSON.stringify(newAnswer));
await peer.setLocalDescription(answer);
return;
}
if (type === 'answer') {
peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }));
return;
}
if (type === '_ice') {
peer.addIceCandidate(iceCandidate);
return;
}
}
}
/* WebRTC */
function WebRTCInit() {
// RTCPeerConnection 的配置,內(nèi)網(wǎng)時(shí)不需要開(kāi)啟stun、turn
const defaultConfiguration = {
bundlePolicy: "max-bundle",
rtcpMuxPolicy: "require",
iceTransportPolicy: "all", // relay:如果使用了turn建議使用relay
// ice
iceServers: [
{
"urls": [
"turn:192.168.147.122:3478?transport=udp",
"turn:192.168.147.122:3478?transport=tcp"
],
"username": 'lqf',
"credential": "123456"
},
{
"urls": [
"stun:192.168.147.122:3478"
]
}
]
}
var Rtc = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
if(!Rtc){
alert("不支持WebRTC")
}
// peer = new RTCPeerConnection(defaultConfiguration);
peer = new RTCPeerConnection();
//ice
peer.onicecandidate = function (e) {
if (e.candidate) {
websocket.send(JSON.stringify({
type: '_ice',
toUser: document.getElementById('toUser').value,
fromUser: username,
iceCandidate: e.candidate
}));
}
};
//track
peer.ontrack = function (e) {
if (e && e.streams) {
remoteVideo.srcObject = e.streams[0];
}
};
}
/* 按鈕事件 */
function ButtonFunInit() {
//視頻通話
document.getElementById('call').onclick = function (e) {
document.getElementById('toUser').style.visibility = 'hidden';
let toUser = document.getElementById('toUser').value;
if (!toUser) {
alert("請(qǐng)先指定好友賬號(hào),再發(fā)起視頻通話!");
return;
}
if (peer == null) {
WebRTCInit();
}
websocket.send(JSON.stringify({
type: "call_start",
fromUser: username,
toUser: toUser,
}));
}
//掛斷
document.getElementById('hangup').onclick = function (e) {
document.getElementById('toUser').style.visibility = 'unset';
if (localVideo.srcObject) {
const videoTracks = localVideo.srcObject.getVideoTracks();
videoTracks.forEach(videoTrack => {
videoTrack.stop();
localVideo.srcObject.removeTrack(videoTrack);
});
}
if (remoteVideo.srcObject) {
const videoTracks = remoteVideo.srcObject.getVideoTracks();
videoTracks.forEach(videoTrack => {
videoTrack.stop();
remoteVideo.srcObject.removeTrack(videoTrack);
});
//掛斷同時(shí),通知對(duì)方
websocket.send(JSON.stringify({
type: "hangup",
fromUser: username,
toUser: document.getElementById('toUser').value,
}));
}
if (peer) {
peer.ontrack = null;
peer.onremovetrack = null;
peer.onremovestream = null;
peer.onicecandidate = null;
peer.oniceconnectionstatechange = null;
peer.onsignalingstatechange = null;
peer.onicegatheringstatechange = null;
peer.onnegotiationneeded = null;
peer.close();
peer = null;
}
localVideo.srcObject = null;
remoteVideo.srcObject = null;
}
}
</script>
</html>
?
后端代碼
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.blacktea</groupId>
<artifactId>wbrtc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>webrtc</name>
<description>springboot整合webrtc的demo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 引入WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
?
配置類
package com.blacktea.webrtc.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import java.text.SimpleDateFormat;
/**
* @description:
* @author: black tea
* @date: 2023/3/15 19:37
*/
@Configuration
// 掃描cn.hutool.extra.spring包下所有類并注冊(cè)之
@ComponentScan(basePackages={"cn.hutool.extra.spring"})
public class MyWebSocketConfig{
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
@Bean
public ObjectMapper mapper(){
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
}
?
WebSocket服務(wù)類
package com.blacktea.webrtc.server;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @description: WebRtc的 WebSocket 服務(wù)
* @author: black tea
* @date: 2023/3/15 18:30
*/
@Slf4j
@Component
@ServerEndpoint(value = "/webrtc/{username}")
@Import(cn.hutool.extra.spring.SpringUtil.class)
public class WebRtcWebSocketServer {
/**
* 連接集合
*/
private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
/**
* 連接建立成功時(shí)的調(diào)用方法
* @param session 會(huì)話對(duì)象
* @param username 用戶
*/
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
log.info("ws client 連接成功,username={}, session={}", username, session);
sessionMap.put(username, session);
}
@OnClose
public void onClose(Session session){
Set<Map.Entry<String, Session>> entries = sessionMap.entrySet();
for(Map.Entry<String, Session> entry : entries){
if (entry.getValue() == session){
String username = entry.getKey();
log.info("ws client 關(guān)閉成功,username={}, session={}", username, session);
sessionMap.remove(username);
break;
}
}
}
@OnError
public void onError(Session session, Throwable error){
log.error("ws 出現(xiàn)異常,", error);
}
/**
* 服務(wù)器接收到客戶端消息時(shí)調(diào)用的方法
*/
@OnMessage
public void onMessage(String message, Session session) {
try{
log.info("receive message:{}", message);
ObjectMapper mapper = SpringUtil.getBean(ObjectMapper.class);
//JSON字符串轉(zhuǎn) HashMap
HashMap hashMap = mapper.readValue(message, HashMap.class);
//消息類型
String type = (String) hashMap.get("type");
//to user
String toUser = (String) hashMap.get("toUser");
Session toUserSession = sessionMap.get(toUser);
String fromUser = (String) hashMap.get("fromUser");
//msg
String msg = (String) hashMap.get("msg");
//sdp
String sdp = (String) hashMap.get("sdp");
//ice
Map iceCandidate = (Map) hashMap.get("iceCandidate");
HashMap<String, Object> map = new HashMap<>();
map.put("type",type);
//呼叫的用戶不在線
if(toUserSession == null){
toUserSession = session;
map.put("type","call_back");
map.put("fromUser","系統(tǒng)消息");
map.put("msg","Sorry,呼叫的用戶不在線!");
send(toUserSession,mapper.writeValueAsString(map));
return;
}
//對(duì)方掛斷
if ("hangup".equals(type)) {
map.put("fromUser",fromUser);
map.put("msg","對(duì)方掛斷!");
}
//視頻通話請(qǐng)求
if ("call_start".equals(type)) {
map.put("fromUser",fromUser);
map.put("msg","1");
}
//視頻通話請(qǐng)求回應(yīng)
if ("call_back".equals(type)) {
map.put("fromUser",toUser);
map.put("msg",msg);
}
//offer
if ("offer".equals(type)) {
map.put("fromUser",toUser);
map.put("sdp",sdp);
}
//answer
if ("answer".equals(type)) {
map.put("fromUser",toUser);
map.put("sdp",sdp);
}
//ice
if ("_ice".equals(type)) {
map.put("fromUser",toUser);
map.put("iceCandidate",iceCandidate);
}
send(toUserSession,mapper.writeValueAsString(map));
}catch(Exception e){
log.error("onMessage,異常:", e);
}
}
/**
* 封裝一個(gè)send方法,發(fā)送消息到前端
*/
private void send(Session session, String message) {
try {
log.info("send message:{}",message);
session.getBasicRemote().sendText(message);
} catch (Exception e) {
log.error("message,待發(fā)送的數(shù)據(jù):{},異常:", message, e);
}
}
}
? 注意:因?yàn)槲椰F(xiàn)在沒(méi)有公網(wǎng)服務(wù)器,所以現(xiàn)在我的所有測(cè)試都只進(jìn)行了內(nèi)網(wǎng)測(cè)試。
?
4.1.3 效果圖
4.13.1 瀏覽器效果圖
?
4.13.2 安卓效果圖
電腦瀏覽器效果圖
?
手機(jī)瀏覽器效果圖
? app的話可以使用 Hbuilder X 進(jìn)行打包。
? 注意:打包的時(shí)候記得修改自己的websocket服務(wù)的ip和端口。
?
5. 公網(wǎng)部署
因?yàn)楝F(xiàn)在沒(méi)有公網(wǎng)服務(wù)器,所以暫時(shí)不弄。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-538913.html
到了這里,關(guān)于WebRTC的學(xué)習(xí)(java版本信令服務(wù))的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!