WebRTC实现双端音视频聊天(Vue3 + SpringBoot)

news2024/11/20 11:18:44

目录

概述

相关概念

双端连接整体实现步骤概述

文章代码实现注意点

STUN和TURN服务器的搭建

开发过程描述

后端开发流程

前端开发流程

效果演示

Gitee源码地址


 

概述

  • 文章描述使用WebRTC技术实现一对一音视频通话

  • 由于设备摄像头限制(一台电脑作测试无法在开启的双端同时获取摄像头数据流),导致一台电脑无法同时测试双端,因此文章使用mp4音视频文件模拟摄像头音视频数据流输入

  • 使用技术

    • 前端:Vue3,WebRTC相关API,axios

    • 后端信令服务器实现:SpringBoot,WebSocket

相关概念

  • Peer-to-Peer (P2P) 连接:WebRTC主要是基于 P2P 连接的,这意味着通信是直接在两端的浏览器之间进行的,而不需要经过中介服务器(尽管可能会使用服务器来初始化和协调连接)。这种方式降低了延迟并节省了带宽。

  • SDPSession Description Protocol描述媒体信息(如音频、视频编码格式、传输协议等)的协议。例如我们在双方构建连接时,我们需要知道对方使用的音视频编解码格式,以确保双方使用相同编解码格式。编解码格式就是定义在SDP信息中的其中之一的信息。

  • ICE Candidate:ICE 候选是 WebRTC 在 P2P 连接过程中为寻找最佳传输路径(如 STUN 或 TURN 服务器)提供的一系列地址和端口。在双方构建连接时需要知道对方的公网IP地址和端口,以实现P2P连接,Candidate信息中就包含自身的公网IP和端口。

  • STUN(Session Traversal Utilities for NAT)服务器:是 NAT 穿透的协议,用来获取客户端的公网 IP 地址和端口。我们身处各种局域网中,对方如果想要和我们构建P2P连接,就必然要知道我们的公网IP和端口才能和我们连接上,我们可以通过STUN服务器获取我们的公网IP和端口

  • TURN(Traversal Using Relays around NAT)服务器当 STUN 连接不可用时,TURN 服务器作为中继服务器转发数据。当STUN服务器无法帮助我们获取公网IP和端口时,我们就可以使用TURN服务器作为中转站传递音视频流数据。

  • 信令服务器:上面介绍了媒体信息SDP和网络信息Candidate,这些实际上可以称为"信令",我们如果想要与对端连接,那么我们就需要知道对端的媒体信息和网络信息来构建连接,信令服务器就是帮助我们实现两端的信息交换的。本文中信令服务器就是我们自己编写的SpringBoot后端,来帮助两端互传连接信息。

双端连接整体实现步骤概述

在大致知道了上面介绍的WebRTC基本概念之后,我们以双端音视频互联的整体过程。

假设存在A端(发起端)B端(接收端)

1. 创建RTC连接对象(new RTCPeerConnection),此对象存在构建连接时所需的API。

2. A端和B端分别连接后端WebSocket(信令服务器),以为接下来信息互传奠定基础。

3. A端创建媒体信息SDP(createOffer)保存到本地(setLocalDescription),将A端SDP信息通过WebSocket发送给B端。

4. B端接收到A端的SDP信息,设置为远端媒体信息(setRemoteDescription),然后B端创建应答媒体信息(实际上就是B端的媒体信息)SDP(createAnswer)保存到本地(setLocalDescription),并将B端创建的应答媒体信息SDP通过WebSocket发送给A端。

5. A端收到B端发送的应答媒体信息SDP后,保存为远端媒体信息(setRemoteDescription)。

6. 至此,A端和B端媒体信息SDP交换完毕。

7. 开始交换网络信息Candidate,我们在创建RTC连接对象时(步骤1)监听网络信息的获取(onicecandidate),当我们调用setRemoteDescription函数设置了远端媒体信息之后,会触发onicecandidate并给予condidate网络信息。

8. 我们将监听到的网络信息candidate通过WebSocket发送给对端,对端收到后将对方的网络信息配置上(addIceCandidate)以实现连接。

9. 当媒体信息SDP和网络信息Candidate互相交换并设置上之后,就可以开始音视频流数据互传显示了。

10. 通过addTrack发送本地流数据,通过ontrack监听对端音视频流数据的发送,监听到就显示对端音视频。


 媒体协商和网络协商时序图: 

3ca34de8be9642c8b9151a74b97c3bee.png

 总结:在视频互传之前重要的就是交换媒体SDP信息和网络Candidate信息(媒体和网络协商),当双方都获取到对方的媒体和网络信息之后。就能够成功构建连接并传递音视频数据了。

文章代码实现注意点

在最开始的概述中有提到,本文提供的1对1音视频聊天代码示例中没有真实调用用户摄像头获取音视频流数据,因为作者只有一台电脑,为了可以更方便的在一台电脑上开启两端并测试,因此使用了MP4音视频作为音视频流数据输入作为测试。

这实际上并不会和真实开启摄像头获取音视频数据流有很大的区别。仅仅是获取流数据的方式不同罢了。

在真实的场景下,可以使用API:getUserMedia去获取摄像头音视频流数据即可。

const stream = await navigator.mediaDevices.getUserMedia({
	video: true,
	audio: true
});

STUN和TURN服务器的搭建

为了能够获取到我们本地的公网IP和端口去和对端创建连接,我们可以尝试去搭建STUN服务器和TURN中继服务器。

注:此步骤不是一定需要做,因为Google给我们提供了一个免费公用的STUN服务器地址:stun:stun.l.google.com:19302,如果你发现用不了,或需要搭建复杂的音视频通话应用,还是推荐自己搭建一下STUN/TURN服务器。

我们直接搭建开源的Coturn服务器即可,因为Coturn 同时支持 TURN 和 STUN 协议。

下面会介绍在CentOS8中搭建Coturn服务器步骤:

1. 安装所需依赖包

yum install -y make gcc cc gcc-c++ wget openssl-devel libevent libevent-devel openssl 

2. yum直接一键下载安装

sudo yum install coturn

# (验证安装)安装程序结束后执行如下命令查看是否正确输出turnserver路径
which turnserver

3. 配置Coturn相关属性,找到配置文件路径:

find / -name turnserver.conf

 4. 获取服务器内网IP和公网IP

# 输入命令查看Ip
ifconfig

找到自己启用的网络下的内网IP,公网IP就是你连接服务器的IP地址。 

1fcc4a0de8b34d60aef3b3471bbb9efc.png

5. 使用openSSL生成cert和pkey配置的自签名证书 

openssl req -x509 -newkey rsa:2048 -keyout /turn_server_pkey.pem -out /turn_server_cert.pem -days 999 -nodes 

输入上面命令后,填写一下证书的一些信息(城市,地区等),随便填一下回车回车!就行。

上面的/turn_server_pkey.pem和 /turn_server_cert.pem 请自己设置好保存证书的路径,上面默认放到了根路径下。

6. 编辑刚才找到的配置文件

将下面的配置部分修改后替换掉原配置文件的所有内容。

# 网卡名
relay-device=eth0
#内网IP
listening-ip=172.24.52.189 
listening-port=3478
#内网IP,加密访问配置
relay-ip=172.24.52.189
tls-listening-port=5349
# 外网IP
external-ip=自己的外网IP
relay-threads=500
#打开密码验证
lt-cred-mech
cert=/turn_server_cert.pem
pkey=/turn_server_pkey.pem
min-port=40000
max-port=65535
#设置用户名和密码,创建IceServer时使用
user=user:123456
# 外网IP绑定的域名
realm=你自己IP绑定的域名
# 服务器名称,用于OAuth认证,默认和realm相同,部分浏览器本段不设可能会引发cors错误。
server-name=你自己IP绑定的域名
# 认证密码,和前面设置的密码保持一致
cli-password=123456

7. 开启端口访问

7.1 开启云服务器安全组端口

b038c23a5e6f4acc9ecce100ee0d99e5.png

开启4000-65535端口的原因:外部客户端与 TURN 服务器的通信使用动态端口。通常,操作系统会为每个连接分配一个临时端口(通常是大于 1024 的端口),而 40000 到 65535 端口 作为 高端端口,是常用的临时端口范围。因此,为了确保 TURN 服务器能够处理大量的并发连接,并为每个连接分配一个端口,需要确保 TURN 服务器的端口范围足够大。

7.2 开启本地防火墙端口 

#开放端口
firewall-cmd --zone=public --add-port=3478/udp --permanent
firewall-cmd --zone=public --add-port=3478/tcp --permanent
#重启防火墙
firewall-cmd --reload

 8. 启动Coturn服务器

turnserver -o -a -f

9. 测试启动状态

 访问测试网站:Trickle ICE

36f8c9dcf5e244edacb1331d89a432a8.png

a1862e778e53400a9585c1850bffe2b7.png

开发过程描述

如下仅展示关键性代码解释说明,具体代码请到文章最后获取Gitee源码地址。

后端开发流程

  • websocket连接成功后维护用户连接信息并广播join消息。数据携带用户ID列表。

// 后端维护Session连接的数据结构
private final HashMap<String, WebSocketSession> userMap = new HashMap<>();
  • 编写接收信息通用接口,dto对象包含userID,type,data(JSON序列化字符串),接口根据传入userId取出session,给session发送消息对象。

前端开发流程

  • 日志系统,监听ice状态及日志打印。

  • 创建随机ID,连接ws。

  • 协商函数:协商前创建peerConnection对象并监听candidate,当双方都连接成功后调用,判断本地offerFlag状态,如果为true,创建offer设置本地并发送消息给对端。

// STUN 服务器
const iceServers = [
  {
    urls: "stun:stun.l.google.com:19302"  // Google公开的STUN 服务器
  },
  {
    urls: "stun:自己的STUN服务器IP:3478" // 自己的Stun服务器
  },
  {
    urls: "turn:自己的TRUN服务器IP:3478",   // 自己的TURN服务器
    username: "userName",
    credential: "Password"
  }
];

// 创建RTC连接对象并监听和获取condidate信息
function createPeerConnection() {
  wlog("开始创建PC对象...")
  peerConnection = new RTCPeerConnection(iceServers);
  wlog("创建PC对象成功")
  // 创建RTC连接对象后连接websocket
  initWebSocket();

  // 监听网络信息(ICE Candidate)
  peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
      candidateInfo = event.candidate;
      wlog("candidate信息变化...");
      // 将candidate信息发送给远端
      setTimeout(()=>{
        sendCandidate(event.candidate);
      }, 150)
    }
  };

  // 监听远端音视频流
  peerConnection.ontrack = (event) => {
    nextTick(() => {
      wlog("====> 收到远端数据流 <=====")
      if (!remoteVideo.value.srcObject) {
        remoteVideo.value.srcObject = event.streams[0];
        remoteVideo.value.play();  // 强制播放
      }
    });
    // remoteVideo.value.srcObject = event.streams[0];
  };

  // 监听ice连接状态
  peerConnection.oniceconnectionstatechange = () => {
    wlog(`RTC连接状态改变:${peerConnection.iceConnectionState}`);
  };


  // 添加本地音视频流到 PeerConnection
  localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
  });
}
  • candidate监听:当监听到candidate后判断双方是否已连接,如果已连接,构造并发送candidate给对端。

  • 解析消息处理器

    • 解析join:type为join取出userId列表,如果为一个代表仅自己在线,标识为创建offer端,日志打印相关信息,如果有两个者取出对方ID保存,代表双方都上线成功,日志打印,调用协商函数,开始媒体协商和网络协商。

    • 解析offer:type为offer,说明收到发起端offer,将offer设置为远端信息,然后创建answer设置到本地,构建answer消息发送给对端。

    • 解析answer:type为answer,说明收到接收端应答,取出answer设置为远端消息。

    • 解析candidate:type为candidate,说明收到对端的网络信息,取出设置到本地。


// 消息处理器 - 解析器
function handleSignalingMessage(message) {
  wlog("收到ws消息,开始解析...")
  wlog(message)
  let parseMsg = JSON.parse(message);
  wlog(`解析结果:${parseMsg}`);

  if (parseMsg.type == "join") {
    joinHandle(parseMsg.data);
  } else if (parseMsg.type == "offer") {
    wlog("收到发起端offer,开始解析...");
    offerHandle(parseMsg.data);
  } else if (parseMsg.type == "answer") {
    wlog("收到接收端的answer,开始解析...");
    answerHandle(parseMsg.data);
  }else if(parseMsg.type == "candidate"){
    wlog("收到远端candidate,开始解析...");
    candidateHandle(parseMsg.data);
  }

}

// 远端Candidate处理器
async function candidateHandle(candidate){
  peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate)));
  wlog("+++++++ 本端candidate设置完毕 ++++++++");
}

// 接收端的answer处理
async function answerHandle(answer) {
  wlog("将answer设置为远端信息");
  peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(answer))); // 设置远端SDP
}

// 发起端offer处理器
async function offerHandle(offer) {
  wlog("将发起端的offer设置为远端媒体信息");
  await peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(offer)));
  wlog("创建Answer 并设置到本地");
  let answer = await peerConnection.createAnswer()
  await peerConnection.setLocalDescription(answer);

  wlog("发送answer给发起端");
  // 构造answer消息发送给对端
  let paramObj = {
    userId: oppositeUserId,
    type: "answer",
    data: JSON.stringify(answer)
  }
  // 执行发送
  const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj);
}

// 加入处理器
function joinHandle(userIds) {
  // 判断连接的用户个数
  if (userIds.length == 1 && userIds[0] == userId) {
    wlog("标识为发起端,等待对方加入房间...")
    isRoomEmpty.value = true;
    // 存在一个连接并且是自身,标识我们是发起端
    offerFlag = true;
  } else if (userIds.length > 1) {
    // 对方加入了
    wlog("对方已连接...")
    isRoomEmpty.value = false;

    // 取出对方ID
    for (let id of userIds) {
      if (id != userId) {
        oppositeUserId = id;
      }
    }

    wlog(`对端ID: ${oppositeUserId}`)
    // 开始交换SDP和Candidate
    swapVideoInfo()
  }
}

效果演示

初始状态 

db67a4f2924c4cc59a68d4b3c69a1ffb.png

 发起端加入房间

cab98851d7524f7caf1156c3b78788a4.png

接收端加入房间

335ff2a7b30f44568a264ee61e48a63b.png

Gitee源码地址

 源码地址:点击访问Gitee项目源代码。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2244001.html

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

相关文章

[C++]:C++11(二)

1. 左值与右值 1.1 左值与右值的概念 左值&#xff1a;左值本质上是一个表示数据的表达式&#xff0c;常见的如变量名或者解引用后的指针等形式。它具备以下显著特点&#xff1a; 可被取地址&#xff1a;能够通过取地址操作符 & 获取其内存地址&#xff0c;这意味着它在内…

Acme PHP - Let‘s Encrypt

Lets Encrypt是一个于2015年三季度推出的数字证书认证机构&#xff0c;旨在以自动化流程消除手动创建和安装证书的复杂流程&#xff0c;并推广使万维网服务器的加密连接无所不在&#xff0c;为安全网站提供免费的SSL/TLS证书。 使用PHP来更新证书&#xff1a; Acme PHP | Rob…

探索 HTML 和 CSS 实现的 3D旋转相册

效果演示 这段HTML与CSS代码创建了一个包含10张卡片的3D旋转效果&#xff0c;每张卡片都有自己的边框颜色和图片。通过CSS的3D变换和动画&#xff0c;实现了一个动态的旋转展示效果 HTML <div class"wrapper"><div class"inner" style"-…

Cargo Rust 的包管理器

Cargo->Rust 的包管理器 Cargi简介Cargo 的主要功能1. 创建项目2. 管理依赖3. 构建项目4. 运行项目5. 测试代码6. 检查代码7. 生成文档8. 发布和分享包 Cargo 的核心文件1. Cargo.toml2. Cargo.lock **Cargo 的生态系统** 常用命令总结Hello, Cargo! 示例 Cargi简介 Cargo …

代码随想录算法训练营第三十五天| 01背包问题 二维 、01背包问题 一维、416. 分割等和子集 。c++转java

背包理论基础 视频地址&#xff1a; 带你学透0-1背包问题&#xff01;| 关于背包问题&#xff0c;你不清楚的地方&#xff0c;这里都讲了&#xff01;| 动态规划经典问题 | 数据结构与算法_哔哩哔哩_bilibili 01背包问题 二维 题目我是在Acwing上面做的&#xff0c;思路可以…

亚马逊云科技宣布新推出Elasticahe for Redis的 Valkey缓存

Amazon ElastiCache 宣布支持 Valkey&#xff0c;与其他支持的引擎相比&#xff0c;无服务器的价格低 33%&#xff0c;基于节点的价格低 20%。使用 ElastiCache Serverless for Valkey&#xff0c;客户可以在一分钟内创建缓存&#xff0c;并且起步价低至每月 6 USD。Valkey 是由…

SpringBoot3.x.x整合Ehcache3 实例(入门踩坑教程)

近期尝试了一下 SpringBoot 3.3.5 JDK17 Ehcache3.10.8整合 注意&#xff0c;这个版本的boot&#xff0c;提出了公用的缓存模板&#xff0c;Spring官网有相关介绍&#xff0c;整合ehcache需要使用jcache。 老版本的3.0版本以下整合&#xff0c;我这里就不参与了&#xff0c;可以…

跨平台WPF框架Avalonia教程 十六

SelectableTextBlock 可选文本块 SelectableTextBlock 块是一个用于显示文本的标签&#xff0c;允许选择和复制文本。它可以显示多行&#xff0c;并且可以完全控制所使用的字体。 有用的属性​ 您可能最常使用这些属性&#xff1a; 属性描述SelectionStart当前选择的起始字…

每日一练:【优先算法】双指针之移动零(easy)

双指针概念介绍 常见的双指针有两种形式&#xff0c;一种是对撞指针&#xff0c;一种是左右指针。 对撞指针&#xff1a;一般用于顺序结构中&#xff0c;也称左右指针。 • 对撞指针从两端向中间移动。一个指针从最左端开始&#xff0c;另一个从最右端开始&#xff0c;然后逐渐…

ASP.NET Core Webapi 返回数据的三种方式

ASP.NET Core为Web API控制器方法返回类型提供了如下几个选择&#xff1a; Specific type IActionResult ActionResult<T> 1. 返回指定类型&#xff08;Specific type&#xff09; 最简单的API会返回原生的或者复杂的数据类型&#xff08;比如&#xff0c;string 或者…

【JAVA】使用mybatis plus查询数据库中的geometry字段为null

目录 问题描述&#xff1a; 原码&#xff1a; 实体类&#xff1a; 接口&#xff1a; 解决方法&#xff1a; 1、创建Handle 2、配置handle让mybatis-plus能够识别 相关代码&#xff1a; 问题描述&#xff1a; 在 MyBatis Plus 中&#xff0c;查询的 geometry 字段在数据库…

51c自动驾驶~合集28

我自己的原文哦~ https://blog.51cto.com/whaosoft/12030824 #自动驾驶建图的统一矢量先验地图编码 高德地图&西交 | 先验驾驶 论文链接&#xff1a;https://arxiv.org/pdf/2409.05352 写在前面&笔者的个人理解 最近出现了很多先验地图的论文&#xff0c;高德地图…

【全面解读】Apache SeaTunnel常见问题全攻略

使用SeaTunnel需要安装Spark或者Flink这样的引擎么&#xff1f; 不需要&#xff0c;SeaTunnel 支持 Zeta、Spark 和 Flink 作为同步引擎的选择&#xff0c;您可以选择之一就行&#xff0c;社区尤其推荐使用 Zeta 这种专为同步场景打造的新一代超高性能同步引擎。Zeta 被社区用…

Spring Boot3.x自动配置不生效的排查与解决:IDEA 文件夹命名导致的问题

在使用Spring Boot搭建多模块项目时&#xff0c;需要使用到自动配置功能&#xff0c;把一些通用功能封装成模块后通过 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件注册配置类。然而&#xff0c;最近遇到一个奇怪的问题&#xff1a…

JavaSE(十四)——文件操作和IO

文章目录 文件操作和IO文件相关概念Java操作文件文件系统操作文件内容操作字节流FileOutputStreamFileInputStream代码演示 字符流FileWriterFileReader代码演示 缓冲流转换流 案例练习 文件操作和IO 文件相关概念 文件 通常指的是包含用户数据的文件&#xff0c;如文本文件、…

Qt桌面应用开发 第五天(常用控件)

目录 1.QPushButton和ToolButton 1.1QPushButton 1.2ToolButton 2.RadioButton和CheckBox 2.1RadioButton单选按钮 2.2CheckBox多选按钮 3.ListWidget 4.TreeWidget控件 5.TableWidget控件 6.Containers控件 6.1QScrollArea 6.2QToolBox 6.3QTabWidget 6.4QStacke…

Vue.js 插槽 Slots 实际应用 最近重构项目的时候遇到的...

前端开发中 插槽 Slots 是一个重要的概念 我们可以查看一下vue.js的官方文档 https://cn.vuejs.org/guide/components/slots 类似于连接通道一样 可以把核心代码逻辑搬到另外的地方 做一个引用 而原先的地方可能并不能这样书写 对于这个概念我在vue的官方文档里面找到了…

ubuntu pytorch容器内安装gpu版本的ffmpeg

一、基础镜像和生成容器 pytorch/pytorch &#xff1a;1.13.1-cuda11.6-cudnn8-devel 生成容器&#xff0c;一定要加NVIDIA_DRIVER_CAPABILITIEScompute,utility,video,graphics&#xff0c;否则侯建无法推流&#xff0c;报错缺少编码之类的。 docker run -it --gpus all -e …

VSCode+ESP-IDF开发ESP32-S3-DevKitC-1(2)第一个工程 LED心跳灯

VSCodeESP-IDF开发ESP32-S3-DevKitC-1&#xff08;2&#xff09;第一个工程 LED心跳灯 前言1.新建工程2.编写控制LED代码3.LED控制独立成.c和.h文件 前言 实际开发中很多时候我们需要有一个类似心跳灯或运行指示灯的灯以不同的状态闪烁以表示程序的运行状态&#xff0c;所以第…

在ubunto18.04安装node 14.16.0

这里演示安装node 14.16.0&#xff0c;其他版本也一样的安装步骤&#xff0c;需要注意1 1.检查本机服务器类型&#xff0c;有的是x64&#xff0c;有的是ARM&#xff0c;先查清楚是什么类型再进行下载&#xff0c;否则会存在编译出错的问题 bash: /opt/node-v14.16.0-linux-x6…