赞
踩
情况一:AB之间可以直接通信
1.信令交换:
设备A和设备B首先通过信令服务器交换SDP(Session Description Protocol)信息和候选者(candidates)。SDP包含有关会话的信息,包括设备的媒体能力和网络地址。
2.ICE框架协商:
设备A和设备B使用ICE框架收集本地候选者(本地网络地址),然后交换候选者信息,包括通过STUN服务器获取的公共IP地址和端口号。
3.连接建立:
根据ICE框架收集的候选者信息,设备A和设备B尝试直接建立点对点连接。根据候选者优先级排序,选择最佳的候选者进行连接。
4.直接通信:
如果ICE框架成功建立了连接,设备A和设备B之间可以直接进行实时通信,例如音频、视频或数据传输。
情况二:AB之间不能直接通信
1.信令交换:
设备A和设备B通过信令服务器交换SDP信息和候选者。
2.ICE框架协商:
设备A和设备B分别收集本地候选者,并将候选者信息发送给对方。
3.无法直接建立连接:
如果ICE框架无法直接建立连接(例如由于双方都位于NAT环境或防火墙后),则ICE框架会返回无法建立连接的错误或超时。
4.使用TURN服务器:
在无法直接建立连接的情况下,设备A和设备B将使用TURN服务器作为中继器来中转数据流。设备A和设备B分别连接到TURN服务器,并将数据流通过TURN服务器进行中转,从而实现对等通信。
双方位于不同的私有网络:如果设备A和设备B都处于不同的私有网络(例如家庭网络),则无法直接访问对方的局域网地址。
AB之间无法直接建立点对点连接的情况通常是由于网络地址转换(NAT)或防火墙的存在,导致设备无法直接接收对方发送的数据包。具体情况包括:
NAT类型限制:如果设备A或设备B位于对称NAT或受限NAT网络中,会导致UDP包的发送和接收出现问题,从而无法建立直接连接。
防火墙限制:防火墙可以阻止对UDP或特定端口的访问,这会影响设备之间的直接通信。
公共IP地址不可用:有些设备可能没有公共IP地址,而是通过NAT路由器共享局域网IP地址。
在这些情况下,WebRTC利用ICE框架和STUN/TURN服务器来实现网络穿透,确保设备之间可以建立可靠的实时通信连接。通过STUN服务器获取公共网络地址,通过TURN服务器进行数据中转,解决了设备间无法直接通信的问题。
附一下个人直播间搭建部分代码
<html> <head> <title>简单直播间</title> <style type="text/css"> body { background: #888888 center center no-repeat; color: white; } button { cursor: pointer; user-select: none; } .room { border: 1px solid black; cursor: pointer; user-select: none; text-align: center; background: rgba(0, 0, 0, 0.5); } video { width: 75%; border: 2px solid black; border-image: linear-gradient(#F80, #2ED) 20 20; } #chat { position: fixed; top: 5px; right: 5px; width: calc(25% - 30px); height: calc(75% - 10px); background: rgba(0, 0, 0, 0.5); } .content { height: calc(100% - 66px); margin-top: 5px; margin-bottom: 5px; overflow-y: scroll; } #chatSend { white-space: nowrap; } #chatSend>input { width: calc(100% - 90px); } #chatSend>button { width: 80px; } #chatTag { margin-left: 10px; line-height: 30px; } .chatKuang { border: 1px solid white; margin: 3px; border-radius: 5px; } .hintKuang { margin: 3px; text-align: center; } </style> </head> <body> <div id="create"> 名称:<input id="name" type="text" /> <button onclick="createRoom()">创建直播间</button> <span id="count"></span> </div> <video id="localVideo" autoplay controls="controls"></video> <br /> <div id="chat"> <div id="chatTag"> <button onclick="changeTag(0)">房间列表</button> <button onclick="changeTag(1)">房间聊天</button> </div> <div id="roomContent" class="content"></div> <div id="chatContent" class="content" style="display: none"></div> <div id="chatSend"> <input type="text" /> <button onclick="chatSend()">发送</button> </div> </div> <script type="text/javascript"> // 背景图片 (function() { var img = new Image(); img.addEventListener("load", function() { document.querySelector("body").style.background = "url('" + this.src + "') center center no-repeat"; let _img = this; calculateBackgroundImageScale(_img); window.onresize = function(_img) { calculateBackgroundImageScale(_img); } }); img.src = "https://parva.cool/share/sky043.jpg"; })(); //计算背景图片缩放(自适应窗口大小) function calculateBackgroundImageScale(img) { let w1 = document.body.clientWidth; let h1 = document.body.clientHeight; let w2 = img.width; let h2 = img.height; let scale1 = w1 / w2; let scale2 = h1 / h2; let scale = scale1 > scale2 ? scale1 : scale2; document.querySelector("body").style.backgroundSize = Math.ceil(w2 * scale) + "px " + Math.ceil(h2 * scale) + "px"; } // 存储本地媒体流 var localStream; // 与服务器的websocket通信 var socket = new WebSocket("wss://parva.cool/rtc"); // 判断自己是否正在直播 var isMe; // 当前的标签页(0:房间列表, 1:房间聊天) var tag = 0; // 发送聊天信息 function chatSend() { let input = document.querySelector("#chatSend input"); let msg = input.value; input.value = ""; if (Object.keys(pcs).length == 0) return; if (isMe) { socket.send(JSON.stringify({ event: "chatSend", msg: msg })); } else { socket.send(JSON.stringify({ event: "chatSend", msg: msg, roomName: Object.keys(pcs)[0] })); } } // 切换标签 function changeTag(t) { tag = t; document.querySelector("#roomContent").style.display = "none"; document.querySelector("#chatContent").style.display = "none"; if (tag == 0) { document.querySelector("#roomContent").style.display = "block"; } else if (tag == 1) { document.querySelector("#chatContent").style.display = "block"; } } // 创建直播间 function createRoom() { let roomName = document.querySelector("#name").value; if (!localStream) { navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }) .then((stream) => { localStream = stream; socket.send(JSON.stringify({ event: "createRoom", roomName: roomName })); }); } else { socket.send(JSON.stringify({ event: "createRoom", roomName: roomName })); } } // 关闭直播间 function closeRoom() { socket.send(JSON.stringify({ event: "closeRoom" })); document.querySelector("input").disabled = false; document.querySelector("button").innerHTML = "创建直播间"; document.querySelector("button").setAttribute("onclick", "createRoom()"); document.querySelector("video").srcObject = null; pcs = {}; let content = document.querySelector("#chatContent"); content.innerHTML = ""; changeTag(0); let count = document.querySelector("#count"); count.innerHTML = ""; localStream.getTracks()[0].stop(); } // 进入直播间 var 防止双击; var 防止双击setTimeout; function joinRoom(roomName) { if (防止双击 == roomName) return; else 防止双击 = roomName; clearTimeout(防止双击setTimeout); 防止双击setTimeout = setTimeout(function() { 防止双击 = ""; }, 800); let name = document.querySelector("#name").value; socket.send(JSON.stringify({ event: "joinRoom", name: name, roomName: roomName })); } // 接收服务器的消息 socket.onmessage = function(event) { let json = JSON.parse(event.data); // 接收聊天信息 if (json.event === "chat") { let chatName = document.createElement("span"); chatName.innerHTML = json.name + " : "; chatName.setAttribute("class", "chatName"); let chatMessage = document.createElement("span"); chatMessage.innerHTML = json.msg; chatName.setAttribute("class", "chatMessage"); let chatKuang = document.createElement("div"); chatKuang.setAttribute("class", "chatKuang"); chatKuang.appendChild(chatName); chatKuang.appendChild(chatMessage); let content = document.querySelector("#chatContent"); content.appendChild(chatKuang); content.scrollTop = 9999999; } // 通知新人加入房间 if (json.event === "joinHint") { let hintKuang = document.createElement("div"); hintKuang.setAttribute("class", "hintKuang"); hintKuang.innerHTML = json.name + "加入直播房间!" let content = document.querySelector("#chatContent"); content.appendChild(hintKuang); content.scrollTop = 9999999; } // 有人退出当前房间 if (json.event === "quitHint") { let hintKuang = document.createElement("div"); hintKuang.setAttribute("class", "hintKuang"); hintKuang.innerHTML = json.name + "退出房间.." let content = document.querySelector("#chatContent"); content.appendChild(hintKuang); content.scrollTop = 9999999; } // 接收房间人数 if (json.event === "count") { let count = document.querySelector("#count"); count.innerHTML = "\t在场众神数量 : " + json.count; } // 所有房间的信息 if (json.event === "roomsInfo") { if (tag != 0) return; let content = document.querySelector("#roomContent"); content.innerHTML = ""; for (let i = 0; i < json.info.length; i++) { let div = document.createElement("div"); div.innerHTML = json.info[i]; //点击进入房间事件捆绑 div.setAttribute("onclick", "joinRoom('" + json.info[i] + "')"); div.setAttribute("class", "room"); content.appendChild(div); } } // 创建房间失败 if (json.event === "createRoomFailed") { alert("创建房间失败,名称已存在 或 名称格式有误"); localStream.getTracks()[0].stop(); } // 创建房间成功 if (json.event === "createRoomOk") { document.querySelector("input").disabled = true; document.querySelector("button").innerHTML = "关闭直播间"; document.querySelector("button").setAttribute("onclick", "closeRoom()"); //将捕捉的本地媒体流赋值到直播窗口 document.querySelector("video").srcObject = localStream; document.querySelector("video").volume = 0; isMe = true; changeTag(1); let hintKuang = document.createElement("div"); hintKuang.setAttribute("class", "hintKuang"); hintKuang.innerHTML = "创建直播房间成功!" let content = document.querySelector("#chatContent"); content.appendChild(hintKuang); content.scrollTop = 9999999; } // 加入房间失败 if (json.event === "joinRoomFailed") { alert("加入房间失败,名称已存在 或 名称格式有误"); } // 主播下播,退出房间 if (json.event === "roomClosed") { document.querySelector("video").srcObject = null; pcs = {}; let content = document.querySelector("#chatContent"); content.innerHTML = ""; changeTag(0); document.querySelector("input").disabled = false; let count = document.querySelector("#count"); count.innerHTML = ""; } // 有人欲加入我的直播间 if (json.event === "joinRoom") { // 为对方创建一个pc实例,并发送offer给他 var pc = createRTCPeerConnection(json.name); // rtc建立连接:创建一个offer给对方 pc.createOffer(function(desc) { pc.setLocalDescription(desc); socket.send(JSON.stringify({ event: "_offer", data: { sdp: desc, nameB: json.name } })); }, function(error) { console.log("CreateOffer Failure callback: " + error); }); } // rtc建立连接:接收到offer if (json.event === "_offer") { // 为对方创建一个pc实例,并发送offer给他 var pc = createRTCPeerConnection(json.data.nameA); pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp)); // rtc建立连接:创建一个answer给对方 pc.createAnswer(function(desc) { pc.setLocalDescription(desc); socket.send(JSON.stringify({ event: "_answer", data: { sdp: desc, nameA: json.data.nameA } })); }, function(error) { console.log("CreateAnswer Failure callback: " + error); }); } // rtc建立连接:接收到answer --A收到B的answer // 将来自对方的 Answer SDP 设置为本地 WebRTC 连接的远程描述,以便双方能够正确地理解和处理对方的媒体数据,从而建立成功的实时通信连接。 if (json.event === "_answer") { pcs[json.data.nameB].setRemoteDescription( new RTCSessionDescription(json.data.sdp)); } // rtc建立连接:接收到_ice_candidate if (json.event === "_ice_candidate") { pcs[json.data.from].addIceCandidate( new RTCIceCandidate(json.data.candidate)); } } // Q:一对多直播情况下 直播用户A的远程描述需要怎么变化? // A:连接都是独立的 分别设置对应的远端描述符 A的描述符不变 // 存储pc实例 var pcs = {}; // stun和turn服务器URL及配置 var iceServer = { iceServers: [ { urls: "stun:parva.cool:3478" }, { urls: "turn:parva.cool:3478", username: "parva", credential: "Parva089" } ] }; // 创建RTCPeerConnection实例 function createRTCPeerConnection(name) { let pc = new RTCPeerConnection(iceServer); if (!isMe) { for (let n in pcs) pcs[n].close(); pcs = {}; } // 以{对方的名字:PC实例}键值对形式把PC实例存储起来 pcs[name] = pc; if (localStream) pc.addStream(localStream); else pc.addStream(new MediaStream()); pc.onicecandidate = function(event) { if (event.candidate !== null) socket.send(JSON.stringify({ event: "_ice_candidate", data: { candidate: event.candidate, to: Object.keys(pcs).find(k => pcs[k] == pc) } })); } pc.ontrack = function(event) { if (isMe) return; changeTag(1); document.querySelector("video").srcObject = event.streams[0]; document.querySelector("input").disabled = true; let hintKuang = document.createElement("div"); hintKuang.setAttribute("class", "hintKuang"); hintKuang.innerHTML = "成功进入直播间!" let content = document.querySelector("#chatContent"); content.innerHTML = ""; content.appendChild(hintKuang); content.scrollTop = 9999999; } return pc; } </script> </body> </html>
前提条件:
A 是直播主播,已经在直播间创建了媒体流并开始直播。
B 和 C 是观众,希望加入直播间并观看 A 的直播。
建立 WebRTC 连接的过程:
1.B 或 C 加入直播间:
B 或 C 在前端页面选择要加入的直播间并点击加入按钮。
前端通过 WebSocket 向服务器发送加入房间的请求,包括用户信息和房间名称。
2.服务器收到加入房间请求:
后端服务器接收到 B 或 C 的加入房间请求,将其添加到对应房间的用户列表中。
3.A 发送 WebRTC offer 给 B 或 C:
当 B 或 C成功加入房间后,后端服务器会通知 A(主播)有新用户加入了直播间。
A 在前端收到加入房间的通知后,使用 WebRTC 创建一个 PeerConnection 对象(pc),并生成一个 SDP offer。
A 将这个 SDP offer 通过 WebSocket 发送给服务器。
4.服务器转发 WebRTC offer 给 B 或 C:
后端服务器收到 A 发送的 SDP offer。
后端服务器将 SDP offer 转发给房间内除 A 之外的其他用户(即 B 或 C)。
5.B 或 C 接收 WebRTC offer:
B 或 C 前端收到 A 发送的 SDP offer。
B 或 C 使用 WebRTC 创建一个 PeerConnection 对象(pc),并设置 A 的 SDP offer 作为远端描述(setRemoteDescription)。
6.B 或 C 创建 WebRTC answer 给 A:
B 或 C 使用自己的本地媒体流(视频和音频)创建一个 SDP answer。
B 或 C 将这个 SDP answer 发送给服务器。
7.服务器转发 WebRTC answer 给 A:
后端服务器收到 B 或 C 发送的 SDP answer。
后端服务器将 SDP answer 转发给 A。
8.A 接收 WebRTC answer:
A 前端收到 B 或 C 发送的 SDP answer。
A 设置 B 或 C 的 SDP answer 作为远端描述(setRemoteDescription)。
9.ICE 候选者交换:
PeerConnection 开始收集和交换 ICE 候选者信息(网络地址、端口等)。
10.B、C 和 A 通过服务器交换 ICE 候选者信息,以便彼此建立直接的通信路径。
建立直播观看连接:
完成以上步骤后,B 或 C 和 A 之间的 WebRTC 连接就建立起来了。
B 或 C 的本地媒体流通过 ICE 候选者的协商直接传输到 A,A 的直播内容会被 B 或 C 观看。
通过以上步骤,B 或 C 可以加入 A 创建的直播间,并与 A 建立起 WebRTC 连接,实现实时的音视频传输和观看直播内容。整个过程涉及前端的用户交互和媒体流处理,以及后端的 WebRTC 信令传递和 ICE 候选者交换,共同实现了观众与主播之间的实时通信和直播功能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。