java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > WebRTC双端音视频聊天

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

作者:m0_74823094

这篇文章主要介绍了WebRTC实现双端音视频聊天功能(Vue3 + 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去获取摄像头音视频流数据即可。

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 开启云服务器安全组端口

开启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

开发过程描述

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

后端开发流程

// 后端维护Session连接的数据结构

private final HashMap<String, WebSocketSession> userMap = new HashMap<>();

前端开发流程

// 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);
});
}
// 消息处理器 - 解析器
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

接收端加入房间

Gitee源码地址

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

到此这篇关于WebRTC实现双端音视频聊天(Vue3 + SpringBoot )的文章就介绍到这了,更多相关WebRTC双端音视频聊天内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文