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

打印 上一主题 下一主题

主题 1027|帖子 1027|积分 3081

目录
概述
相关概念
双端连接团体实现步调概述
文章代码实现注意点
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监听对端音视频流数据的发送,监听到就表现对端音视频。

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

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

在最开始的概述中有提到,本文提供的1对1音视频聊天代码示例中没有真实调用用户摄像头获取音视频流数据,因为作者只有一台电脑,为了可以更方便的在一台电脑上开启两头并测试,因此利用了MP4音视频作为音视频流数据输入作为测试。
这现实上并不会和真实开启摄像头获取音视频数据流有很大的区别。仅仅是获取流数据的方式差别罢了。
在真实的场景下,可以利用API:getUserMedia去获取摄像头音视频流数据即可。
  1. const stream = await navigator.mediaDevices.getUserMedia({
  2.         video: true,
  3.         audio: true
  4. });
复制代码
STUN和TURN服务器的搭建

为了能够获取到我们本地的公网IP和端口去和对端创建连接,我们可以尝试去搭建STUN服务器和TURN中继服务器。
注:此步调不是肯定需要做,因为Google给我们提供了一个免费公用的STUN服务器地址:stun:stun.l.google.com:19302,假如你发现用不了,或需要搭建复杂的音视频通话应用,照旧推荐自己搭建一下STUN/TURN服务器。
   我们直接搭建开源的Coturn服务器即可,因为Coturn 同时支持 TURN 和 STUN 协议。
  下面会介绍在CentOS8中搭建Coturn服务器步调:
1. 安装所需依赖包
  1. yum install -y make gcc cc gcc-c++ wget openssl-devel libevent libevent-devel openssl
复制代码
2. yum直接一键下载安装
  1. sudo yum install coturn
  2. # (验证安装)安装程序结束后执行如下命令查看是否正确输出turnserver路径
  3. which turnserver
复制代码
3. 配置Coturn相关属性,找到配置文件路径:
  1. find / -name turnserver.conf
复制代码
 4. 获取服务器内网IP和公网IP
  1. # 输入命令查看Ip
  2. ifconfig
复制代码
找到自己启用的网络下的内网IP,公网IP就是你连接服务器的IP地址。 

5. 利用openSSL生成cert和pkey配置的自签名证书 
  1. 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. 编辑刚才找到的配置文件
   将下面的配置部分修改后替换掉原配置文件的所有内容。
  1. # 网卡名
  2. relay-device=eth0
  3. #内网IP
  4. listening-ip=172.24.52.189
  5. listening-port=3478
  6. #内网IP,加密访问配置
  7. relay-ip=172.24.52.189
  8. tls-listening-port=5349
  9. # 外网IP
  10. external-ip=自己的外网IP
  11. relay-threads=500
  12. #打开密码验证
  13. lt-cred-mech
  14. cert=/turn_server_cert.pem
  15. pkey=/turn_server_pkey.pem
  16. min-port=40000
  17. max-port=65535
  18. #设置用户名和密码,创建IceServer时使用
  19. user=user:123456
  20. # 外网IP绑定的域名
  21. realm=你自己IP绑定的域名
  22. # 服务器名称,用于OAuth认证,默认和realm相同,部分浏览器本段不设可能会引发cors错误。
  23. server-name=你自己IP绑定的域名
  24. # 认证密码,和前面设置的密码保持一致
  25. cli-password=123456
复制代码
7. 开启端口访问
7.1 开启云服务器安全组端口

   开启4000-65535端口的缘故原由:外部客户端与 TURN 服务器的通讯利用动态端口。通常,操作系统会为每个连接分配一个临时端口(通常是大于 1024 的端口),而 40000 到 65535 端口 作为 高端端口,是常用的临时端口范围。因此,为了确保 TURN 服务器能够处理大量的并发连接,并为每个连接分配一个端口,需要确保 TURN 服务器的端口范围足够大。
  7.2 开启本地防火墙端口 
  1. #开放端口
  2. firewall-cmd --zone=public --add-port=3478/udp --permanent
  3. firewall-cmd --zone=public --add-port=3478/tcp --permanent
  4. #重启防火墙
  5. firewall-cmd --reload
复制代码
 8. 启动Coturn服务器
  1. turnserver -o -a -f
复制代码
9. 测试启动状态
 访问测试网站:Trickle ICE


开发过程描述

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



  • websocket连接成功后维护用户连接信息并广播join消息。数据携带用户ID列表。
  1. // 后端维护Session连接的数据结构
  2. private final HashMap<String, WebSocketSession> userMap = new HashMap<>();
复制代码


  • 编写接收信息通用接口,dto对象包含userID,type,data(JSON序列化字符串),接口根据传入userId取出session,给session发送消息对象。
前端开发流程



  • 日志系统,监听ice状态及日志打印。
  • 创建随机ID,连接ws。
  • 协商函数:协商前创建peerConnection对象并监听candidate,当双方都连接成功后调用,判定本地offerFlag状态,假如为true,创建offer设置本地并发送消息给对端。
  1. // STUN 服务器
  2. const iceServers = [
  3.   {
  4.     urls: "stun:stun.l.google.com:19302"  // Google公开的STUN 服务器
  5.   },
  6.   {
  7.     urls: "stun:自己的STUN服务器IP:3478" // 自己的Stun服务器
  8.   },
  9.   {
  10.     urls: "turn:自己的TRUN服务器IP:3478",   // 自己的TURN服务器
  11.     username: "userName",
  12.     credential: "Password"
  13.   }
  14. ];
  15. // 创建RTC连接对象并监听和获取condidate信息
  16. function createPeerConnection() {
  17.   wlog("开始创建PC对象...")
  18.   peerConnection = new RTCPeerConnection(iceServers);
  19.   wlog("创建PC对象成功")
  20.   // 创建RTC连接对象后连接websocket
  21.   initWebSocket();
  22.   // 监听网络信息(ICE Candidate)
  23.   peerConnection.onicecandidate = (event) => {
  24.     if (event.candidate) {
  25.       candidateInfo = event.candidate;
  26.       wlog("candidate信息变化...");
  27.       // 将candidate信息发送给远端
  28.       setTimeout(()=>{
  29.         sendCandidate(event.candidate);
  30.       }, 150)
  31.     }
  32.   };
  33.   // 监听远端音视频流
  34.   peerConnection.ontrack = (event) => {
  35.     nextTick(() => {
  36.       wlog("====> 收到远端数据流 <=====")
  37.       if (!remoteVideo.value.srcObject) {
  38.         remoteVideo.value.srcObject = event.streams[0];
  39.         remoteVideo.value.play();  // 强制播放
  40.       }
  41.     });
  42.     // remoteVideo.value.srcObject = event.streams[0];
  43.   };
  44.   // 监听ice连接状态
  45.   peerConnection.oniceconnectionstatechange = () => {
  46.     wlog(`RTC连接状态改变:${peerConnection.iceConnectionState}`);
  47.   };
  48.   // 添加本地音视频流到 PeerConnection
  49.   localStream.getTracks().forEach(track => {
  50.     peerConnection.addTrack(track, localStream);
  51.   });
  52. }
复制代码


  • candidate监听:当监听到candidate后判定双方是否已连接,假如已连接,构造并发送candidate给对端。


  • 解析消息处理器

    • 解析join:type为join取出userId列表,假如为一个代表仅自己在线,标识为创建offer端,日志打印相关信息,假如有两个者取出对方ID保存,代表双方都上线成功,日志打印,调用协商函数,开始媒体协商和网络协商。
    • 解析offer:type为offer,阐明收到发起端offer,将offer设置为远端信息,然后创建answer设置到本地,构建answer消息发送给对端。
    • 解析answer:type为answer,阐明收到接收端应答,取出answer设置为远端消息。
    • 解析candidate:type为candidate,阐明收到对端的网络信息,取出设置到本地。

  1. // 消息处理器 - 解析器
  2. function handleSignalingMessage(message) {
  3.   wlog("收到ws消息,开始解析...")
  4.   wlog(message)
  5.   let parseMsg = JSON.parse(message);
  6.   wlog(`解析结果:${parseMsg}`);
  7.   if (parseMsg.type == "join") {
  8.     joinHandle(parseMsg.data);
  9.   } else if (parseMsg.type == "offer") {
  10.     wlog("收到发起端offer,开始解析...");
  11.     offerHandle(parseMsg.data);
  12.   } else if (parseMsg.type == "answer") {
  13.     wlog("收到接收端的answer,开始解析...");
  14.     answerHandle(parseMsg.data);
  15.   }else if(parseMsg.type == "candidate"){
  16.     wlog("收到远端candidate,开始解析...");
  17.     candidateHandle(parseMsg.data);
  18.   }
  19. }
  20. // 远端Candidate处理器
  21. async function candidateHandle(candidate){
  22.   peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate)));
  23.   wlog("+++++++ 本端candidate设置完毕 ++++++++");
  24. }
  25. // 接收端的answer处理
  26. async function answerHandle(answer) {
  27.   wlog("将answer设置为远端信息");
  28.   peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(answer))); // 设置远端SDP
  29. }
  30. // 发起端offer处理器
  31. async function offerHandle(offer) {
  32.   wlog("将发起端的offer设置为远端媒体信息");
  33.   await peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(offer)));
  34.   wlog("创建Answer 并设置到本地");
  35.   let answer = await peerConnection.createAnswer()
  36.   await peerConnection.setLocalDescription(answer);
  37.   wlog("发送answer给发起端");
  38.   // 构造answer消息发送给对端
  39.   let paramObj = {
  40.     userId: oppositeUserId,
  41.     type: "answer",
  42.     data: JSON.stringify(answer)
  43.   }
  44.   // 执行发送
  45.   const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj);
  46. }
  47. // 加入处理器
  48. function joinHandle(userIds) {
  49.   // 判断连接的用户个数
  50.   if (userIds.length == 1 && userIds[0] == userId) {
  51.     wlog("标识为发起端,等待对方加入房间...")
  52.     isRoomEmpty.value = true;
  53.     // 存在一个连接并且是自身,标识我们是发起端
  54.     offerFlag = true;
  55.   } else if (userIds.length > 1) {
  56.     // 对方加入了
  57.     wlog("对方已连接...")
  58.     isRoomEmpty.value = false;
  59.     // 取出对方ID
  60.     for (let id of userIds) {
  61.       if (id != userId) {
  62.         oppositeUserId = id;
  63.       }
  64.     }
  65.     wlog(`对端ID: ${oppositeUserId}`)
  66.     // 开始交换SDP和Candidate
  67.     swapVideoInfo()
  68.   }
  69. }
复制代码
效果演示

初始状态 

 发起端参加房间

接收端参加房间

Gitee源码地址

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

吴旭华

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表