WebRTC直播间搭建记录

devtools/2024/9/23 3:59:07/

考虑到后续增加平台直播的可能性,笔记记录一下WebRTC相关.

在这里插入图片描述

让我们分别分析两种情况下的WebRTC连接建立过程:

情况一: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服务器进行中转,从而实现对等通信。

什么情况下AB不能直接建立点对点连接?

双方位于不同的私有网络:如果设备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建立连接:接收到offerif (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_candidateif (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>

建立 BC 和 A 之间的 WebRTC 连接涉及以下步骤和流程:

前提条件

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 候选者交换,共同实现了观众与主播之间的实时通信和直播功能。


http://www.ppmy.cn/devtools/9435.html

相关文章

mysql基础18——权限管理

权限管理 根据不同的用户进行横向和纵向的分组 横向的分组 用户可以接触到的数据的范围 纵向的分组 用户对接触到的数据能访问到什么程度 把具有相同数据访问范围和程度的用户分为不同的类别 这种类别叫做角色 通过角色对相同权限的用户进行分组管理 可以使权限管理更加简单…

Hudi 表支持多种查询引擎对比

Hudi 表支持多种查询引擎对比 Apache Hudi有两种主要的表类型&#xff0c;分别是Copy on Write&#xff08;COW&#xff09;表和Merge on Read&#xff08;MOR&#xff09;表。 Copy on Write&#xff08;COW&#xff09;表&#xff1a; 特点&#xff1a;COW表在写入新数据时会…

Python爬虫数据可视化分析

Python爬虫用于从网络上获取数据&#xff0c;数据可视化分析则是将获取的数据进行可视化展示和分析&#xff0c;帮助我们更好地理解数据、发现规律、做出决策。下面是一个基本的Python爬虫数据可视化分析的流程&#xff1a; 步骤一&#xff1a;数据爬取 1.选择合适的爬虫工具&a…

大话设计模式-装饰器模式

大话设计模式书中&#xff0c;作者举了一个穿衣服的例子来为我们引入装饰器模式。 概念 定义 装饰模式在书中的定义是&#xff1a;动态地给一个对象添加一些额外的职责&#xff0c;就增加功能来说&#xff0c;装饰模式比生成子类更灵活。 这句话直接去理解可能会有点抽象&#…

python面向对象的使用(2)

题目 面向对象模拟电影院自动售票系统实现自动选择电影、场次、座位。 思路 通过类定义电影的相关信息&#xff0c;输出输入相关电影信息&#xff0c;对座位进行顺序取 代码解释 class Movie:def __init__(self, title, duration):self.title titleself.duration durati…

将组件 赋值给变量li 想拿到 组件的html页面结构 但是 打印出来的是 文件路径 该如何实现呢?

需求 import { ref } from vue; import refrigerationRight from /views/bim3D/components/right/refrigerationRight.vue const liref({lis:refrigerationRight }) const refrigerationclick()>{console.log(li) }##方法 import { createApp } from vue; import refriger…

计算机服务器中了locked勒索病毒怎么办,locked勒索病毒解密工具流程步骤

随着网络技术的不断应用与发展&#xff0c;越来越多的企业离不开网络&#xff0c;网络大大提升了企业的办公效率水平&#xff0c;也为企业的带来快速发展&#xff0c;对于企业来说&#xff0c;网络数据安全成为了大家关心的主要话题。近日&#xff0c;云天数据恢复中心接到多家…

在群晖上安装GPT4Free

什么是 GPT4Free &#xff1f; GPT4Free 简称 G4F&#xff0c;是一个强大的大型语言模型命令行界面&#xff08;LLM-CLI&#xff09;&#xff0c;旨在去中心化并提供免费访问先进人工智能技术的能力。G4F 的目标是通过提供用户友好和高效的工具&#xff0c;使人工智能民主化&am…