<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                &emsp;&emsp;WebRTC 在創建點對點(P2P)的連接之前,會先通過信令服務器交換兩端的 SDP 和 ICE Candidate,取兩者的交集,決定最終的音視頻參數、傳輸協議、NAT 打洞方式等信息。 &emsp;&emsp;在完成媒體協商,并且兩端網絡連通之后,就可以開始傳輸數據了。 &emsp;&emsp;本文示例代碼已上傳至[Github](https://github.com/pwstrick/webrtc),有需要的可以隨意下載。 ## 一、術語 &emsp;&emsp;在實現一個簡單的視頻通話之前,還需要了解一些相關術語。 **1)SDP** &emsp;&emsp;SDP(Session Description Protocal)是一個描述會話元數據(Session Metadata)、網絡(Network)、流(Stream)、安全(Security)和服務質量(Qos,Grouping)的[WebRTC協議](https://www.ietf.org/archive/id/draft-nandakumar-rtcweb-sdp-08.txt),下圖是 SDP 各語義和字段之間的包含關系。 &emsp;&emsp;換句話說,它就是一個用文本描述各端能力的協議,這些能力包括支持的音視頻編解碼器、傳輸協議、編解碼器參數(例如音頻通道數,采樣率等)等信息。 :-: ![](https://img.kancloud.cn/33/39/3339b2be05120ff6b96954e74fb9d3ad_1140x1934.png =600x) &emsp;&emsp;下面是一個典型的[SDP](https://developer.mozilla.org/zh-CN/docs/Glossary/SDP)信息示例,其中 RTP(Real-time Transport Protocol)是一種網絡協議,描述了如何以實時方式將各種媒體從一端傳輸到另一端。 ~~~ =================會話描述====================== v=0 o=alice 2890844526 2890844526 IN IP4 host.anywhere.com s=- =================網絡描述====================== c=IN IP4 host.anywhere.com t=0 0 ================音頻流描述===================== m=audio 49170 RTP/AVP 0 a=rtpmap:0 PCMU/8000 ================視頻流描述===================== m=video 51372 RTP/AVP 31 a=rtpmap:31 H261/90000 ~~~ &emsp;&emsp;RTP 協議的作用是讓數據包有序,它是應用層傳輸協議的一種,與 HTTP/HTTPS 同級。 &emsp;&emsp;除了 RTP 之外,還有一種 RTCP 協議,與 RTP 處于同一級,并且可對其做丟包控制。 **2)ICE Candidate** &emsp;&emsp;ICE 候選者描述了 WebRTC 能夠與遠程設備通信所需的協議、IP、端口、優先級、候選者類型(包括 host、srflx 和 relay)等連接信息。 &emsp;&emsp;host 是本機候選者,srflx 是從 STUN 服務器獲得的候選者,relay 是從 TURN 服務器獲得的中繼候選者。 &emsp;&emsp;在每一端都會提供許多候選者,例如有兩塊網卡,那么每塊網卡的不同端口都是一個候選者。 &emsp;&emsp;WebRTC 會按照優先級倒序的進行連通性測試,當連通性測試成功后,通信的雙方就建立起了連接。 **3)NAT打洞** &emsp;&emsp;在收集到候選者信息后,WebRTC 會判斷兩端是否在同一個局域網中,若是,則可以直接建立鏈接。 &emsp;&emsp;若不是,那么 WebRTC 就會嘗試 NAT 打洞。WebRTC 將 NAT 分為 4 種類型:完全錐型、IP 限制型、端口限制型和對稱型。 &emsp;&emsp;前文候選者類型中曾提到 STUN 和 TURN 兩種協議,接下來會對它們做簡單的說明。 &emsp;&emsp;STUN(Session Traversal Utilities for NAT,NAT會話穿越應用程序)是一種網絡協議,允許位于 NAT 后的客戶端找出自己的公網地址,當前 NAT 類型和 NAT 為某一個本地端口所綁定的公網端口。 &emsp;&emsp;這些信息讓兩個同時處于 NAT 路由器之后的主機之間建立 UDP 通信,STUN 是一種 Client/Server 的協議,也是一種 Request/Response 的協議。 &emsp;&emsp;下圖描繪了通過 STUN 服務器獲取公網的 IP 地址,以及通過信令服務器完成媒體協商的簡易過程。 :-: ![](https://img.kancloud.cn/71/45/7145c9f8d8f4c39caf7fb5b4ce98d8b1_651x619.jpeg =600x) &emsp;&emsp;TURN(Traversal Using Relay NAT,通過 Relay 方式穿越 NAT),是一種數據傳輸協議,允許通過 TCP 或 UDP 穿透 NAT。 &emsp;&emsp;TURN 也是一個 Client/Server 協議,其穿透方法與 STUN 類似,但終端必須在通訊開始前與 TURN 服務器進行交互。 &emsp;&emsp;下圖描繪了通過 TURN 服務器實現 P2P 數據傳輸。 :-: ![](https://img.kancloud.cn/dd/ea/ddea4f9ca4f0d4067e805a52b4cc455f_2684x1574.jpeg =800x) &emsp;&emsp;CoTurn 是一款免費開源的 TURN 和 STUN 服務器,可以到[GitHub](https://github.com/coturn/coturn)上下載源碼編譯安裝。 ## 二、信令服務器 &emsp;&emsp;通信雙方彼此是不知道對方的,但是它們可以先與信令服務器(Signal Server)連接,然后通過它來互傳信息。 &emsp;&emsp;可以將信令服務器想象成一個中間人,由他來安排兩端進入一個房間中,然后在房間中可以他們就能隨意的交換手上的情報了。 &emsp;&emsp;本文會通過 Node.js 和[socket.io](https://socket.io/)實現一個簡單的信令服務器,完成的功能僅僅是用于實驗,保存在 server.js 文件中。 &emsp;&emsp;如果對 socket.io 不是很熟悉,可以參考我之前分享的一篇[博文](https://www.cnblogs.com/strick/p/16358972.html),對其有比較完整的說明。 **1)HTTP 服務器** &emsp;&emsp;為了實現視頻通話的功能,需要先搭建一個簡易的 HTTP 服務器,掛載靜態頁面。 &emsp;&emsp;注意,在實際場景中,這塊可以在另一個項目中執行,本處只是為了方便演示。 ~~~ const http = require('http'); const fs = require('fs'); const { Server } = require("socket.io"); // HTTP服務器 const server = http.createServer((req, res) => { // 實例化 URL 類 const url = new URL(req.url, 'http://localhost:1234'); const { pathname } = url; // 路由 if(pathname === '/') { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(fs.readFileSync('./index.html')); }else if(pathname === '/socket.io.js') { res.writeHead(200, { 'Content-Type': 'application/javascript' }); res.end(fs.readFileSync('./socket.io.js')); }else if(pathname === '/client.js') { res.writeHead(200, { 'Content-Type': 'application/javascript' }); res.end(fs.readFileSync('./client.js')); } }); // 監控端口 server.listen(1234); ~~~ &emsp;&emsp;在上面的代碼中,實現了最簡易的路由分發,當訪問 http://localhost:1234 時,讀取 index.html 靜態頁面,結構如下所示。 ~~~html <video id="localVideo"></video> <button id="btn">開播</button> <video id="remoteVideo" muted="muted"></video> <script src="./socket.io.js"></script> <script src="./client.js"></script> ~~~ &emsp;&emsp;socket.io.js 是官方的 socket.io 庫,client.js 是客戶端的腳本邏輯。 &emsp;&emsp;在 remoteVideo 中附帶 muted 屬性是為了避免[報錯](https://stackoverflow.com/questions/49930680/how-to-handle-uncaught-in-promise-domexception-play-failed-because-the-use):DOMException: The play() request was interrupted by a new load request。 &emsp;&emsp;最后就可以通過 node server.js 命令,開啟 HTTP 服務器。 **2)長連接** &emsp;&emsp;為了便于演示,指定了一個房間,當與信令服務器連接時,默認就會被安排進 living room。 &emsp;&emsp;并且只提供了一個 message 事件,這是交換各端信息的關鍵代碼,將一個客戶端發送來的消息中繼給其他各端。 ~~~ const io = new Server(server); const roomId = 'living room'; io.on('connection', (socket) => { // 指定房間 socket.join(roomId); // 發送消息 socket.on('message', (data) => { // 發消息給房間內的其他人 socket.to(roomId).emit('message', data); }); }); ~~~ &emsp;&emsp;因為默認是在本機演示,所以也不會安裝 CoTurn,有興趣的可以自行實現。 ## 三、客戶端 &emsp;&emsp;在之前的 HTML 結構中,可以看到兩個 video 元素和一個 button 元素。 ~~~ const btn = document.getElementById('btn'); // 開播按鈕 const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); const size = 300; ~~~ &emsp;&emsp;在兩個 video 元素中,第一個是接收本地的音視頻流,第二個是接收遠端的音視頻流。 **1)媒體協商** &emsp;&emsp;在下圖中,Alice 和 Bob 通過信令服務器在交換 SDP 信息。 :-: ![](https://img.kancloud.cn/2b/68/2b68d0bbb2ddcb07c187a48d551da591_1058x370.png =600x) &emsp;&emsp;Alice 先調用[createOffer()](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/createOffer)創建一個 Offer 類型的 SDP,然后調用[setLocalDescription()](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription)配置本地描述。 &emsp;&emsp;Bob 接收發送過來的 Offer,調用[setRemoteDescription()](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/setRemoteDescription)配置遠端描述。 &emsp;&emsp;再調用[createAnswer()](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer)創建一個 Answer 類型的 SDP,最后調用 setLocalDescription() 配置本地描述。 &emsp;&emsp;而 Bob 也會接收 Answer 并調用 setRemoteDescription() 配置遠端描述。后面的代碼會實現上述過程。 **2)RTCPeerConnection** &emsp;&emsp;在 WebRTC 中創建連接,需要先初始化[RTCPeerConnection](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection)類,其構造函數可以接收 STUN/TURN 服務器的配置信息。 ~~~ // STUN/TURN Servers const pcConfig = { // 'iceServers': [{ // 'urls': '', // 'credential': "", // 'username': "" // }] }; // 實例化 RTCPeerConnection const pc = new RTCPeerConnection(pcConfig); ~~~ &emsp;&emsp;然后注冊 icecandidate 事件,將本機的網絡信息發送給信令服務器,sendMessage() 函數后面會介紹。 ~~~ pc.onicecandidate = function(e) { if(!e.candidate) { return; } // 發送 ICE Candidate sendMessage({ type: 'candidate', label: e.candidate.sdpMLineIndex, id: e.candidate.sdpMid, candidate: e.candidate.candidate }); }; ~~~ &emsp;&emsp;最后注冊 track 事件,接收遠端的音視頻流。 ~~~ pc.ontrack = function(e) { remoteVideo.srcObject = e.streams[0]; remoteVideo.play(); }; ~~~ **3)長連接** &emsp;&emsp;在客戶端中,已經引入了 socket.io 庫,所以只需要調用 io() 函數就能建立長連接。 &emsp;&emsp;sendMessage() 函數就是發送信息給服務器的 message 事件。 ~~~ const socket = io("http://localhost:1234"); // 發送消息 function sendMessage(data){ socket.emit('message', data); } ~~~ &emsp;&emsp;本地也有個 message 事件,會接收從服務端發送來的消息,其實就是那些轉發的消息。 &emsp;&emsp;data 對象有個 type 屬性,可創建和接收遠端的 Answer 類型的 SDP 信息,以及接收遠端的 ICE 候選者信息。 ~~~ socket.on("message", function (data) { switch (data.type) { case "offer": // 配置遠端描述 pc.setRemoteDescription(new RTCSessionDescription(data)); // 創建 Answer 類型的 SDP 信息 pc.createAnswer().then((desc) => { pc.setLocalDescription(desc); sendMessage(desc); }); break; case "answer": // 接收遠端的 Answer 類型的 SDP 信息 pc.setRemoteDescription(new RTCSessionDescription(data)); break; case "candidate": // 實例化 RTCIceCandidate const candidate = new RTCIceCandidate({ sdpMLineIndex: data.label, candidate: data.candidate }); pc.addIceCandidate(candidate); break; } }); ~~~ &emsp;&emsp;在代碼中,用[RTCSessionDescription](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCSessionDescription)描述 SDP 信息,用[RTCIceCandidate](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate)描述 ICE 候選者信息。 **4)開播** &emsp;&emsp;為開播按鈕注冊點擊事件,在事件中,首先通過 getUserMedia() 獲取本地的音視頻流。 ~~~ btn.addEventListener("click", function (e) { // 獲取音視頻流 navigator.mediaDevices .getUserMedia({ video: { width: size, height: size }, audio: true }) .then((stream) => { localVideo.srcObject = stream; localStream = stream; // 將 Track 與 RTCPeerConnection 綁定 stream.getTracks().forEach((track) => { pc.addTrack(track, stream); }); // 創建 Offer 類型的 SDP 信息 pc.createOffer({ offerToRecieveAudio: 1, offerToRecieveVideo: 1 }).then((desc) => { // 配置本地描述 pc.setLocalDescription(desc); // 發送 Offer 類型的 SDP 信息 sendMessage(desc); }); localVideo.play(); }); btn.disabled = true; }); ~~~ &emsp;&emsp;然后在 then() 方法中,讓 localVideo 接收音視頻流,并且將 Track 與 RTCPeerConnection 綁定。 &emsp;&emsp;這一步很關鍵,沒有這一步就無法將音視頻流推給遠端。 &emsp;&emsp;然后創建 Offer 類型的 SDP 信息,配置本地描述,并通過信令服務器發送給遠端。 &emsp;&emsp;接著可以在兩個瀏覽器(例如 Chrome 和 Edge)中分別訪問 http://localhost:1234,在一個瀏覽器中點擊開播,如下圖所示。 :-: ![](https://img.kancloud.cn/e9/62/e962803c60b6f47658bf1bdaee042dfd_1368x666.png =600x) &emsp;&emsp;在另一個瀏覽器的 remoteVideo 中,就可以看到推送過來的畫面。 :-: ![](https://img.kancloud.cn/78/e7/78e79040f8e4dde6e29d3b9ec228cf1b_1368x666.png =600x) &emsp;&emsp;下面用一張時序圖來完整的描述整個連接過程,具體內容不再贅述。 :-: ![](https://img.kancloud.cn/3f/83/3f83595949cefee5bea3117071b5d4f6_836x957.png) 參考資料: [What is WebRTC and How to Setup STUN/TURN Server for WebRTC Communication?](https://medium.com/av-transcode/what-is-webrtc-and-how-to-setup-stun-turn-server-for-webrtc-communication-63314728b9d0) [WebRTC音視頻傳輸基礎:NAT穿透](https://blog.jianchihu.net/webrtc-av-transport-basis-nat-traversal.html) ***** > 原文出處: [博客園-HTML躬行記](https://www.cnblogs.com/strick/category/1770829.html) [知乎專欄-HTML躬行記](https://zhuanlan.zhihu.com/c_1250826149041238016) 已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。 ![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200) 推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看