WebRTC直播间搭建记录

考虑到后续增加平台直播的可能性,笔记记录一下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.mzph.cn/diannao/1846.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

就业班 第三阶段(nginx) 2401--4.22 day1 nginx1 http+nginx初识+配置+虚拟主机

一、HTTP 介绍 HTTP协议是Hyper Text Transfer Protocol&#xff08;超文本传输协议&#xff09;的缩写,是用于从万维网&#xff08;WWW:World Wide Web &#xff09;服务器传输超文本到本地浏览器的传送协议。 HTTP是一个基于TCP/IP通信协议来传递数据&#xff08;HTML 文件…

PLC_博图系列☞N=:在信号下降沿置位操作数

、 PLC_博图系列☞N&#xff1a;在信号下降沿置位操作数 文章目录 PLC_博图系列☞N&#xff1a;在信号下降沿置位操作数背景介绍N&#xff1a; 在信号下降沿置位操作数说明参数示例 关键字&#xff1a; PLC、 西门子、 博图、 Siemens 、 N 背景介绍 这是一篇关于PLC编程的…

【C语言】strstr函数刨析-----字符串查找

目录 一、strstr 函数介绍 ✨函数头文件&#xff1a; ✨函数原型&#xff1a; ✨函数解读 ✨功能演示 二、函数的原理以及模拟实现 ✨函数原理 ✨函数的模拟实现 三、strstr函数的注意事项 四、共勉 一、strstr 函数介绍 strstr函数是在一个字符串中查找另一个字符…

了解IPS和IDS:这5个差异将改变你的安全观念!

IPS 代表 入侵防御系统&#xff08;Intrusion Prevention System&#xff09;&#xff0c;它是 IDS 的进一步发展&#xff0c;不仅具备检测攻击的能力&#xff0c;还能在检测到攻击后主动采取措施阻止攻击。IPS 通常部署在防火墙和网络设备之间&#xff0c;能够深度感知并检测流…

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

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

程序员缓解工作压力的小窍门

目录 1.概述 2.工作与休息的平衡 3.心理健康与自我关怀 4.社交与网络建设 1.概述 作为程序员&#xff0c;缓解工作压力是非常重要的。压力太大有可能会写很多bug。我们可以采取以下方法来保持高效和创新&#xff0c;同时维护个人健康和工作热情。 定时休息&#xff1a;保持…

Java | Leetcode Java题解之第38题外观数列

题目&#xff1a; 题解&#xff1a; class Solution {public String countAndSay(int n) {String[] arr {"","1","11","21","1211","111221","312211","13112221","1113213211",…

MySql 安装教程+简单的建表

目录 1.安装准备 1.MySQL官方网站下载 2.安装步骤 3.测试安装 4.简单的建表 1.安装准备 1.MySQL官方网站下载 下载安装包或者压缩包都可以 选择相应版本&#xff0c;点击Download开始通过网页下载到本地&#xff08;压缩包下载快一些&#xff09; 2.安装步骤 双击此.exe…

什么是防抖和节流?有什么区别? 如何实现?

防抖&#xff08;Debounce&#xff09;和节流&#xff08;Throttle&#xff09;是两种常用的技术手段&#xff0c;主要用于控制某个函数在一定时间内触发的次数&#xff0c;以减少触发频率&#xff0c;提高性能并避免资源浪费。 防抖&#xff08;Debounce&#xff09;的工作原…

【笔试强训】day8

没啥好说&#xff0c;都是一遍过 1.求最小公倍数 思路&#xff1a; 求lcm。其实就是两数之乘积除以两个数的gcd。gcd就是是求两个数的最大公约数。 代码&#xff1a; #define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> using namespace std;int gcd(int a, int …

轮转数组(Leedcode)的题目

题目&#xff1a;给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步…

C++奇迹之旅:深入学习类和对象的初始化列表

文章目录 &#x1f4dd;再谈构造函数&#x1f320; 构造函数体赋值&#x1f309;初始化列表&#x1f309;初始化列表效率&#x1f320;隐式类型转换&#x1f309;复制初始化 &#x1f320;单多参数构造函数&#x1f309;explicit关键字 &#x1f6a9;总结 &#x1f4dd;再谈构造…

厉害了,Numpy

几乎所有使用Python处理分析数据的人都用过Pandas&#xff0c;因为实在太方便了&#xff0c;就像Excel一样&#xff0c;但你知道Pandas是基于numpy开发出来的吗? Pandas和Numpy的关系类似于国产安卓系统和原生安卓&#xff0c;Numpy提供底层数据结构和算法&#xff0c;搭配数…

C++心决之类和对象详解(中篇)(封装入门二阶)

目录 1.类的6个默认成员函数 2. 构造函数 2.1 概念 2.2 特性 3.析构函数 3.1 概念 3.2 特性 4. 拷贝构造函数 4.1 概念 4.2 特征 5.赋值运算符重载 5.1 运算符重载 5.2 赋值运算符重载 5.3 前置和后置重载 7.const成员 8.取地址及const取地址操作符重载 1.类的…

掌握item_get_app,提升电商推广转化率

一、引言 在数字化时代&#xff0c;电商行业蓬勃发展&#xff0c;竞争也日趋激烈。为了提高销售额和用户满意度&#xff0c;电商企业需要不断探索新的推广策略和技术手段。其中&#xff0c;掌握item_get_app技术&#xff0c;对于提升电商推广转化率具有重要意义。本文将深入探…

软考139-上午题-【软件工程】-软件容错技术

一、软件容错技术 提高软件质量和可靠性的技术大致可分为两类&#xff0c;一类是避开错误&#xff0c;即在开发的过程中不让差错潜入软件的技术&#xff1b;另一类是容错技术&#xff0c;即对某些无法避开的差错&#xff0c;使其影响减至最小的技术。 1-1、容错软件的定义 归…

更换本地yum源的步骤

更换本地yum源的流程与命令&#xff1a;

【Canvas与艺术】绘制圆形biozhazrad蒙版

【关键点】 制作一个半圆形不透明蒙版来造成左右两边相异的明暗效果。 【成果图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><tit…

Vue3、 Vue2 Diff算法比较

Vue2 Diff算法 源码位置:src/core/vdom/patch.ts 源码所在函数:updateChildren() 源码讲解: 有新旧两个节点数组:oldCh和newCh; 有下面几个变量: oldStartIdx 初始值=0 oldStartVnode 初始值=oldCh[0] oldEndIdx 初始值=oldCh.length - 1 oldEndVnode 初始值=oldCh[ol…

Arthas介绍及使用技巧

文章目录 简介能做什么&#xff1f; 使用下载并启动arthas选择应用 java 进程退出 arthas 常用查看命令帮助查看 dashboard通过 thread 命令来获取到线程的栈通过 jad 来反编译 Classwatch 查看方法出入参、sc 搜索类: 查看已加载类所在的包monitor 方法执行监控trace 方法内调…