vue+springboot+webtrc+websocket實現(xiàn)雙人音視頻通話會議(最新推薦)
前言
最近一些時間我有研究,如何實現(xiàn)一個視頻會議功能,但是找了好多資料都不太理想,最終參考了一個文章
WebRTC實現(xiàn)雙端音視頻聊天(Vue3 + SpringBoot)
只不過,它的實現(xiàn)效果里面只會播放本地的mp4視頻文件,但是按照它的原理是可以正常的實現(xiàn)音視頻通話的
它的最終效果是這樣的

然后我的實現(xiàn)邏輯在它的基礎(chǔ)上進(jìn)行了優(yōu)化
實現(xiàn)了如下效果,如下是我部署項目到服務(wù)器之后,和朋友驗證之后的截圖

針對它的邏輯,我優(yōu)化了如下幾點(diǎn)
第一個人可以輸入房間號創(chuàng)建房間,需要注意的是,當(dāng)前第二個人還沒加入進(jìn)來的時候,視頻兩邊都不展示
第二個人根據(jù)第一個人的房間號輸入進(jìn)行加入房間,等待視頻流的加載就可以互相看到兩邊的視頻和聽到音頻
添加了關(guān)閉/開啟麥克風(fēng)和攝像頭功能
ps: 需要注意的是,我接下來分享的代碼邏輯,如果某個人突然加入別的房間,原房間它視頻分享還是在的,我沒有額外進(jìn)行處理關(guān)閉原房間的音視頻流,大家可根據(jù)這個進(jìn)行調(diào)整
題外話,根據(jù)如上的原理,你可以進(jìn)一步優(yōu)化,將其開發(fā)一個視頻會議功能,當(dāng)前我有開發(fā)一個類似的,但是本次只分享雙人音視頻通話會議項目


VUE邏輯
如下為前端部分邏輯,需要注意的是,本次項目還是沿用參考文章的
VUE3項目
前端項目結(jié)構(gòu)如下:

package.json
{
"name": "webrtc_test",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.7",
"vue": "^3.5.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.10"
}
}換言之,你需要使用npm安裝如上依賴
npm i axios@1.7.7
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import fs from 'fs';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
// 如果需要部署服務(wù)器,需要申請SSL證書,然后下載證書到指定文件夾
https: {
key: fs.readFileSync('src/certs/www.springsso.top.key'),
cert: fs.readFileSync('src/certs/www.springsso.top.pem'),
}
},
})main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')App.vue
<template>
<div class="video-chat">
<div v-if="isRoomEmpty">
<p>{{ roomStatusText }}</p>
</div>
<!-- 視頻雙端顯示 -->
<div class="video_box">
<div class="self_video">
<div class="text_tip">我:<span class="userId">{{ userId }}</span></div>
<video ref="localVideo" autoplay playsinline></video>
</div>
<div class="remote_video">
<div class="text_tip">對方:<span class="userId">{{ oppositeUserId }}</span></div>
<video ref="remoteVideo" autoplay playsinline></video>
</div>
</div>
<!-- 加入房間按鈕 -->
<div class="room-controls">
<div class="room-input">
<input v-model="roomId" placeholder="請輸入房間號" />
<button @click="createRoom">創(chuàng)建房間</button>
<button @click="joinRoomWithId">加入房間</button>
</div>
<div class="media-controls">
<button @click="toggleAudio">
{{ isAudioEnabled ? '關(guān)閉麥克風(fēng)' : '打開麥克風(fēng)' }}
</button>
<button @click="toggleVideo">
{{ isVideoEnabled ? '關(guān)閉攝像頭' : '打開攝像頭' }}
</button>
</div>
</div>
<!-- 日志打印 -->
<div class="log_box">
<pre>
<div v-for="(item, index) of logData" :key="index">{{ item }}</div>
</pre>
</div>
</div>
</template><script setup>
import { ref, onMounted, nextTick } from "vue";
import axios from "axios";
// WebRTC 相關(guān)變量
const localVideo = ref(null);
const remoteVideo = ref(null);
const isRoomEmpty = ref(true); // 判斷房間是否為空
let localStream; // 本地流數(shù)據(jù)
let peerConnection; // RTC連接對象
let signalingSocket; // 信令服務(wù)器socket對象
let userId; // 當(dāng)前用戶ID
let oppositeUserId; // 對方用戶ID
let logData = ref(["日志初始化..."]);
// 請求根路徑,如果需要部署服務(wù)器,把對應(yīng)ip改成自己服務(wù)器ip
let BaseUrl = "https://localhost:8095/meetingV1s"
let wsUrl = "wss://localhost:8095/meetingV1s";
// candidate信息
let candidateInfo = "";
// 發(fā)起端標(biāo)識
let offerFlag = false;
// 房間狀態(tài)文本
let roomStatusText = ref("點(diǎn)擊'加入房間'開始音視頻聊天");
// STUN 服務(wù)器,
// const iceServers = [
// {
// urls: "stun:stun.l.google.com:19302" // Google 的 STUN 服務(wù)器
// },
// {
// urls: "stun:自己的公網(wǎng)IP:3478" // 自己的Stun服務(wù)器
// },
// {
// urls: "turn:自己的公網(wǎng)IP:3478", // 自己的 TURN 服務(wù)器
// username: "maohe",
// credential: "maohe"
// }
// ];
// ============< 看這 >================
// 沒有搭建STUN和TURN服務(wù)器的使用如下ice配置即可
const iceServers = [
{
urls: "stun:stun.l.google.com:19302" // Google 的 STUN 服務(wù)器
}
];
// 在 script setup 中添加新的變量聲明
const roomId = ref(''); // 房間號
const isAudioEnabled = ref(true); // 音頻狀態(tài)
const isVideoEnabled = ref(true); // 視頻狀態(tài)
onMounted(() => {
generateRandomId();
})
// 加入房間,開啟本地攝像頭獲取音視頻流數(shù)據(jù)。
function joinRoomHandle() {
roomStatusText.value = "等待對方加入房間..."
getVideoStream();
}
// 獲取本地視頻 模擬從本地攝像頭獲取音視頻流數(shù)據(jù)
async function getVideoStream() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.value.srcObject = localStream;
wlog(`獲取本地流成功~`)
createPeerConnection(); // 創(chuàng)建RTC對象,監(jiān)聽candidate
} catch (err) {
console.error('獲取本地媒體流失敗:', err);
}
}
// 初始化 WebSocket 連接
function initWebSocket() {
wlog("開始連接websocket")
// 連接ws時攜帶用戶ID和房間號
signalingSocket = new WebSocket(`${wsUrl}/rtc?userId=${userId}&roomId=${roomId.value}`);
signalingSocket.onopen = () => {
wlog('WebSocket 已連接');
};
// 消息處理
signalingSocket.onmessage = (event) => {
handleSignalingMessage(event.data);
};
};
// 消息處理器 - 解析器
function handleSignalingMessage(message) {
wlog("收到ws消息,開始解析...")
wlog(message)
let parseMsg = JSON.parse(message);
wlog(`解析結(jié)果:${parseMsg}`);
if (parseMsg.type == "join") {
joinHandle(parseMsg.data);
} else if (parseMsg.type == "offer") {
wlog("收到發(fā)起端offer,開始解析...");
offerHandle(parseMsg.data);
} else if (parseMsg.type == "answer") {
wlog("收到接收端的answer,開始解析...");
answerHandle(parseMsg.data);
}else if(parseMsg.type == "candidate"){
wlog("收到遠(yuǎn)端candidate,開始解析...");
candidateHandle(parseMsg.data);
}
}
// 遠(yuǎn)端Candidate處理器
async function candidateHandle(candidate){
peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate)));
wlog("+++++++ 本端candidate設(shè)置完畢 ++++++++");
}
// 接收端的answer處理
async function answerHandle(answer) {
wlog("將answer設(shè)置為遠(yuǎn)端信息");
peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(answer))); // 設(shè)置遠(yuǎn)端SDP
}
// 發(fā)起端offer處理器
async function offerHandle(offer) {
wlog("將發(fā)起端的offer設(shè)置為遠(yuǎn)端媒體信息");
await peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(offer)));
wlog("創(chuàng)建Answer 并設(shè)置到本地");
let answer = await peerConnection.createAnswer()
await peerConnection.setLocalDescription(answer);
wlog("發(fā)送answer給發(fā)起端");
// 構(gòu)造answer消息發(fā)送給對端
let paramObj = {
userId: oppositeUserId,
type: "answer",
data: JSON.stringify(answer)
}
// 執(zhí)行發(fā)送
const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj);
}
// 加入處理器
function joinHandle(userIds) {
// 判斷連接的用戶個數(shù)
if (userIds.length == 1 && userIds[0] == userId) {
wlog("標(biāo)識為發(fā)起端,等待對方加入房間...")
isRoomEmpty.value = true;
// 存在一個連接并且是自身,標(biāo)識我們是發(fā)起端
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()
}
}
// 交換SDP和candidate
async function swapVideoInfo() {
wlog("開始交換Sdp和Candidate...");
// 檢查是否為發(fā)起端,如果是創(chuàng)建offer設(shè)置到本地,并發(fā)送給遠(yuǎn)端
if (offerFlag) {
wlog(`發(fā)起端創(chuàng)建offer`)
let offer = await peerConnection.createOffer()
await peerConnection.setLocalDescription(offer); // 將媒體信息設(shè)置到本地
wlog("發(fā)啟端設(shè)置SDP-offer到本地");
// 構(gòu)造消息ws發(fā)送給遠(yuǎn)端
let paramObj = {
userId: oppositeUserId,
type: "offer",
data: JSON.stringify(offer)
};
wlog(`構(gòu)造offer信息發(fā)送給遠(yuǎn)端:${paramObj}`)
// 執(zhí)行發(fā)送
const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj);
}
}
// 將candidate信息發(fā)送給遠(yuǎn)端
async function sendCandidate(candidate) {
// 構(gòu)造消息ws發(fā)送給遠(yuǎn)端
let paramObj = {
userId: oppositeUserId,
type: "candidate",
data: JSON.stringify(candidate)
};
wlog(`構(gòu)造candidate信息發(fā)送給遠(yuǎn)端:${paramObj}`);
// 執(zhí)行發(fā)送
const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj);
}
// 創(chuàng)建RTC連接對象并監(jiān)聽和獲取condidate信息
function createPeerConnection() {
wlog("開始創(chuàng)建PC對象...")
peerConnection = new RTCPeerConnection(iceServers);
wlog("創(chuàng)建PC對象成功")
// 創(chuàng)建RTC連接對象后連接websocket
initWebSocket();
// 監(jiān)聽網(wǎng)絡(luò)信息(ICE Candidate)
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
candidateInfo = event.candidate;
wlog("candidate信息變化...");
// 將candidate信息發(fā)送給遠(yuǎn)端
setTimeout(()=>{
sendCandidate(event.candidate);
}, 150)
}
};
// 監(jiān)聽遠(yuǎn)端音視頻流
peerConnection.ontrack = (event) => {
nextTick(() => {
wlog("====> 收到遠(yuǎn)端數(shù)據(jù)流 <=====")
if (!remoteVideo.value.srcObject) {
remoteVideo.value.srcObject = event.streams[0];
remoteVideo.value.play(); // 強(qiáng)制播放
}
});
};
// 監(jiān)聽ice連接狀態(tài)
peerConnection.oniceconnectionstatechange = () => {
wlog(`RTC連接狀態(tài)改變:${peerConnection.iceConnectionState}`);
};
// 添加本地音視頻流到 PeerConnection
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
}
// 日志編寫
function wlog(text) {
logData.value.unshift(text);
}
// 給用戶生成隨機(jī)ID.
function generateRandomId() {
userId = Math.random().toString(36).substring(2, 12); // 生成10位的隨機(jī)ID
wlog(`分配到ID:${userId}`)
}
// 創(chuàng)建房間
async function createRoom() {
if (!roomId.value) {
alert('請輸入房間號');
return;
}
try {
const res = await axios.post(`${BaseUrl}/rtcs/createRoom`, {
roomId: roomId.value,
userId: userId
});
if (res.data.success) {
wlog(`創(chuàng)建房間成功:${roomId.value}`);
joinRoomHandle();
}
} catch (error) {
wlog(`創(chuàng)建房間失?。?{error}`);
}
}
// 加入指定房間
async function joinRoomWithId() {
if (!roomId.value) {
alert('請輸入房間號');
return;
}
try {
const res = await axios.post(`${BaseUrl}/rtcs/joinRoom`, {
roomId: roomId.value,
userId: userId
});
if (res.data.success) {
wlog(`加入房間成功:${roomId.value}`);
joinRoomHandle();
}
} catch (error) {
wlog(`加入房間失敗:${error}`);
}
}
// 切換音頻
function toggleAudio() {
if (localStream) {
const audioTrack = localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
isAudioEnabled.value = audioTrack.enabled;
wlog(`麥克風(fēng)已${audioTrack.enabled ? '打開' : '關(guān)閉'}`);
}
}
}
// 切換視頻
function toggleVideo() {
if (localStream) {
const videoTrack = localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
isVideoEnabled.value = videoTrack.enabled;
wlog(`攝像頭已${videoTrack.enabled ? '打開' : '關(guān)閉'}`);
}
}
}
</script><style scoped>
.video-chat {
display: flex;
flex-direction: column;
align-items: center;
}
video {
width: 300px;
height: 200px;
margin: 10px;
}
.remote_video {
border: solid rgb(30, 40, 226) 1px;
margin-left: 20px;
}
.self_video {
border: solid red 1px;
}
.video_box {
display: flex;
}
.video_box div {
border-radius: 10px;
}
.join_room_btn button {
border: none;
background-color: rgb(119 178 63);
height: 30px;
width: 80px;
border-radius: 10px;
color: white;
margin-top: 10px;
cursor: pointer;
font-size: 13px;
}
.text_tip {
font-size: 13px;
color: #484848;
padding: 6px;
}
pre {
width: 600px;
height: 300px;
background-color: #d4d4d4;
border-radius: 10px;
padding: 10px;
overflow-y: auto;
}
pre div {
padding: 4px 0px;
font-size: 15px;
}
.userId{
color: #3669ad;
}
.video-chat p{
font-weight: 600;
color: #b24242;
}
.room-controls {
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.room-input {
display: flex;
gap: 10px;
align-items: center;
}
.room-input input {
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.media-controls {
display: flex;
gap: 10px;
}
.room-controls button {
border: none;
background-color: rgb(119 178 63);
height: 30px;
padding: 0 15px;
border-radius: 5px;
color: white;
cursor: pointer;
font-size: 13px;
}
.media-controls button {
background-color: #3669ad;
}
</style>SpringBoot邏輯
如下為后端邏輯,項目結(jié)構(gòu)如下:

pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.9</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.mh</groupId> <artifactId>webrtc-backend</artifactId> <version>0.0.1-SNAPSHOT</version> <name>webrtc-backend</name> <description>webrtc-backend</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.34</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.6.2</version> <configuration> <mainClass>com.mh.WebrtcBackendApplication</mainClass> <layout>ZIP</layout> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
application.yml
server:
port: 8095
servlet:
context-path: /meetingV1s
ssl: #ssl配置
enabled: true # 默認(rèn)為true
#key-alias: alias-key # 別名(可以不進(jìn)行配置)
# 保存SSL證書的秘鑰庫的路徑,如果部署到服務(wù)器,必須要開啟ssl才能獲取到攝像頭和麥克風(fēng)
key-store: classpath:www.springsso.top.jks
# ssl證書密碼
key-password: gf71v8lf
key-store-password: gf71v8lf
key-store-type: JKS
tomcat:
uri-encoding: UTF-8入口文件
// 這個是自己實際項目位置
package com.mh;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebrtcBackendApplication {
public static void main(String[] args) {
SpringApplication.run(WebrtcBackendApplication.class, args);
}
}WebSocket處理器
package com.mh.common;
import com.mh.dto.bo.UserManager;
import com.mh.dto.vo.MessageOut;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.util.ArrayList;
import java.util.Set;
/**
* Date:2024/11/14
* author:zmh
* description: WebSocket處理器
**/
@Component
@RequiredArgsConstructor
@Slf4j
public class RtcWebSocketHandler extends TextWebSocketHandler {
// 管理用戶的加入和退出...
private final UserManager userManager;
private final ObjectMapper objectMapper = new ObjectMapper();
// 用戶連接成功
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 獲取用戶ID和房間ID
String userId = getParameterByName(session.getUri(), "userId");
String roomId = getParameterByName(session.getUri(), "roomId");
if (userId != null && roomId != null) {
// 保存用戶會話
userManager.addUser(userId, session);
log.info("用戶 {} 連接成功,房間:{}", userId, roomId);
// 獲取房間中的所有用戶
Set<String> roomUsers = userManager.getRoomUsers(roomId);
// 通知房間內(nèi)所有用戶(包括新加入的用戶)
for (String uid : roomUsers) {
WebSocketSession userSession = userManager.getUser(uid);
if (userSession != null && userSession.isOpen()) {
MessageOut messageOut = new MessageOut();
messageOut.setType("join");
messageOut.setData(new ArrayList<>(roomUsers));
String message = objectMapper.writeValueAsString(messageOut);
userSession.sendMessage(new TextMessage(message));
log.info("向用戶 {} 發(fā)送房間更新消息", uid);
}
}
}
}
// 接收到客戶端消息,解析消息內(nèi)容進(jìn)行分發(fā)
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 轉(zhuǎn)換并分發(fā)消息
log.info("收到消息");
}
// 處理斷開的連接
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String userId = getParameterByName(session.getUri(), "userId");
String roomId = getParameterByName(session.getUri(), "roomId");
if (userId != null && roomId != null) {
// 從房間和會話管理中移除用戶
userManager.removeUser(userId);
userManager.leaveRoom(roomId, userId);
// 獲取更新后的房間用戶列表
Set<String> remainingUsers = userManager.getRoomUsers(roomId);
// 通知房間內(nèi)的其他用戶
for (String uid : remainingUsers) {
WebSocketSession userSession = userManager.getUser(uid);
if (userSession != null && userSession.isOpen()) {
MessageOut messageOut = new MessageOut();
messageOut.setType("join");
messageOut.setData(new ArrayList<>(remainingUsers));
String message = objectMapper.writeValueAsString(messageOut);
userSession.sendMessage(new TextMessage(message));
log.info("向用戶 {} 發(fā)送用戶離開更新消息", uid);
}
}
log.info("用戶 {} 斷開連接,房間:{}", userId, roomId);
}
}
// 輔助方法:從URI中獲取參數(shù)值
private String getParameterByName(URI uri, String paramName) {
String query = uri.getQuery();
if (query != null) {
String[] pairs = query.split("&");
for (String pair : pairs) {
String[] keyValue = pair.split("=");
if (keyValue.length == 2 && keyValue[0].equals(paramName)) {
return keyValue[1];
}
}
}
return null;
}
}WebSocket配置類
package com.mh.config;
import com.mh.common.RtcWebSocketHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* Date:2024/11/14
* author:zmh
* description: WebSocket配置類
**/
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final RtcWebSocketHandler rtcWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(rtcWebSocketHandler, "/rtc")
.setAllowedOrigins("*");
}
}webRtc相關(guān)接口
package com.mh.controller;
import com.mh.dto.bo.UserManager;
import com.mh.dto.vo.MessageReceive;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* Date:2024/11/15
* author:zmh
* description: rtc 相關(guān)接口
**/
@RestController
@Slf4j
@CrossOrigin
@RequiredArgsConstructor
@RequestMapping("/rtcs")
public class RtcController {
private final UserManager userManager;
/**
* 給指定用戶發(fā)送執(zhí)行類型消息
* @param messageReceive 消息參數(shù)接收Vo
* @return ·
*/
@PostMapping("/sendMessage")
public Boolean sendMessage(@RequestBody MessageReceive messageReceive){
userManager.sendMessage(messageReceive);
return true;
}
@PostMapping("/createRoom")
public ResponseEntity<?> createRoom(@RequestBody Map<String, String> params) {
String roomId = params.get("roomId");
String userId = params.get("userId");
// 在 UserManager 中實現(xiàn)房間創(chuàng)建邏輯
boolean success = userManager.createRoom(roomId, userId);
Map<String, Object> response = new HashMap<>();
response.put("success", success);
return ResponseEntity.ok(response);
}
@PostMapping("/joinRoom")
public ResponseEntity<?> joinRoom(@RequestBody Map<String, String> params) {
String roomId = params.get("roomId");
String userId = params.get("userId");
// 在 UserManager 中實現(xiàn)加入房間邏輯
boolean success = userManager.joinRoom(roomId, userId);
Map<String, Object> response = new HashMap<>();
response.put("success", success);
return ResponseEntity.ok(response);
}
}用戶管理器單例對象
package com.mh.dto.bo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mh.dto.vo.MessageOut;
import com.mh.dto.vo.MessageReceive;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
/**
* Date:2024/11/14
* author:zmh
* description: 用戶管理器單例對象
**/
@Data
@Component
@Slf4j
public class UserManager {
// 管理連接用戶信息
private final HashMap<String, WebSocketSession> userMap = new HashMap<>();
// 添加房間管理的Map
private final Map<String, Set<String>> roomUsers = new ConcurrentHashMap<>();
// 加入用戶
public void addUser(String userId, WebSocketSession session) {
userMap.put(userId, session);
log.info("用戶 {} 加入", userId);
}
// 移除用戶
public void removeUser(String userId) {
userMap.remove(userId);
log.info("用戶 {} 退出", userId);
}
// 獲取用戶
public WebSocketSession getUser(String userId) {
return userMap.get(userId);
}
// 獲取所有用戶ID構(gòu)造成list返回
public List<String> getAllUserId() {
return userMap.keySet().stream().collect(Collectors.toList());
}
// 通知用戶加入-廣播消息
public void sendMessageAllUser() throws IOException {
// 獲取所有連接用戶ID列表
List<String> allUserId = getAllUserId();
for (String userId : userMap.keySet()) {
WebSocketSession session = userMap.get(userId);
MessageOut messageOut = new MessageOut("join", allUserId);
String messageText = new ObjectMapper().writeValueAsString(messageOut);
// 廣播消息
session.sendMessage(new TextMessage(messageText));
}
}
/**
* 創(chuàng)建房間
* @param roomId 房間ID
* @param userId 用戶ID
* @return 創(chuàng)建結(jié)果
*/
public boolean createRoom(String roomId, String userId) {
if (roomUsers.containsKey(roomId)) {
log.warn("房間 {} 已存在", roomId);
return false;
}
Set<String> users = new HashSet<>();
users.add(userId);
roomUsers.put(roomId, users);
log.info("用戶 {} 創(chuàng)建了房間 {}", userId, roomId);
return true;
}
/**
* 加入房間
* @param roomId 房間ID
* @param userId 用戶ID
* @return 加入結(jié)果
*/
public boolean joinRoom(String roomId, String userId) {
Set<String> users = roomUsers.computeIfAbsent(roomId, k -> new HashSet<>());
if (users.size() >= 2) {
log.warn("房間 {} 已滿", roomId);
return false;
}
users.add(userId);
log.info("用戶 {} 加入房間 {}", userId, roomId);
return true;
}
/**
* 離開房間
* @param roomId 房間ID
* @param userId 用戶ID
*/
public void leaveRoom(String roomId, String userId) {
Set<String> users = roomUsers.get(roomId);
if (users != null) {
users.remove(userId);
if (users.isEmpty()) {
roomUsers.remove(roomId);
log.info("房間 {} 已清空并刪除", roomId);
}
log.info("用戶 {} 離開了房間 {}", userId, roomId);
}
}
/**
* 獲取房間用戶
* @param roomId 房間ID
* @return 用戶集合
*/
public Set<String> getRoomUsers(String roomId) {
return roomUsers.getOrDefault(roomId, new HashSet<>());
}
// 修改現(xiàn)有的 sendMessage 方法,考慮房間信息
public void sendMessage(MessageReceive messageReceive) {
String userId = messageReceive.getUserId();
String type = messageReceive.getType();
String data = messageReceive.getData();
WebSocketSession session = userMap.get(userId);
if (session != null && session.isOpen()) {
try {
MessageOut messageOut = new MessageOut();
messageOut.setType(type);
messageOut.setData(data);
String message = new ObjectMapper().writeValueAsString(messageOut);
session.sendMessage(new TextMessage(message));
log.info("消息發(fā)送成功: type={}, to={}", type, userId);
} catch (Exception e) {
log.error("消息發(fā)送失敗", e);
}
}
}
}消息輸出前端Vo對象
package com.mh.dto.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Date:2024/11/15
* author:zmh
* description: 消息輸出前端Vo對象
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MessageOut {
/**
* 消息類型【join, offer, answer, candidate, leave】
*/
private String type;
/**
* 消息內(nèi)容 前端stringFiy序列化后字符串
*/
private Object data;
}消息接收Vo對象
package com.mh.dto.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Date:2024/11/15
* author:zmh
* description: 消息接收Vo對象
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MessageReceive {
/**
* 用戶ID,用于獲取用戶Session
*/
private String userId;
/**
* 消息類型【join, offer, answer, candidate, leave】
*/
private String type;
/**
* 消息內(nèi)容 前端stringFiy序列化后字符串
*/
private String data;
}結(jié)語
如上為vue+springboot+webtrc+websocket實現(xiàn)雙人音視頻通話會議的全部邏輯,如有遺漏后續(xù)會進(jìn)行補(bǔ)充
到此這篇關(guān)于vue+springboot+webtrc+websocket實現(xiàn)雙人音視頻通話會議的文章就介紹到這了,更多相關(guān)vue springboot音視頻通話會議內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot分布式WebSocket的實現(xiàn)指南
- SpringBoot實現(xiàn)WebSocket通信過程解讀
- 深入淺出SpringBoot WebSocket構(gòu)建實時應(yīng)用全面指南
- 利用SpringBoot與WebSocket實現(xiàn)實時雙向通信功能
- Springboot整合WebSocket 實現(xiàn)聊天室功能
- Springboot使用Websocket的時候調(diào)取IOC管理的Bean報空指針異常問題
- Java?springBoot初步使用websocket的代碼示例
- SpringBoot3整合WebSocket詳細(xì)指南
- SpringBoot實現(xiàn)WebSocket的示例代碼
- Spring Boot集成WebSocket項目實戰(zhàn)的示例代碼
相關(guān)文章
SpringBoot中優(yōu)化Undertow性能的方法總結(jié)
Undertow是一個采用 Java 開發(fā)的靈活的高性能Web服務(wù)器,提供包括阻塞和基于NIO的非堵塞機(jī)制,本文將給大家介紹SpringBoot中優(yōu)化Undertow性能的方法,文中有相關(guān)的代碼示例供大家參考,需要的朋友可以參考下2024-08-08
Java中volatile關(guān)鍵字的作用與用法詳解
volatile關(guān)鍵字雖然從字面上理解起來比較簡單,但是要用好不是一件容易的事情。這篇文章主要介紹了Java中volatile關(guān)鍵字的作用與用法詳解的相關(guān)資料,需要的朋友可以參考下2016-09-09
Java國際化簡介_動力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家簡單介紹了Java國際化的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07
springboot集成druid,多數(shù)據(jù)源可視化,p6spy問題
這篇文章主要介紹了springboot集成druid,多數(shù)據(jù)源可視化,p6spy問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01
老生常談Java中instanceof關(guān)鍵字的理解
java 中的instanceof 運(yùn)算符是用來在運(yùn)行時指出對象是否是特定類的一個實例。這篇文章主要介紹了老生常談Java中instanceof關(guān)鍵字的理解,需要的朋友可以參考下2018-10-10
SpringBoot中Zookeeper分布式鎖的原理和用法詳解
Zookeeper是一個分布式協(xié)調(diào)服務(wù),它提供了高可用、高性能、可擴(kuò)展的分布式鎖機(jī)制,SpringBoot是一個基于Spring框架的開發(fā)框架,它提供了對Zookeeper分布式鎖的集成支持,本文將介紹SpringBoot中的 Zookeeper分布式鎖的原理和使用方法,需要的朋友可以參考下2023-07-07

