vue+springboot+webtrc+websocket實(shí)現(xiàn)雙人音視頻通話會(huì)議(最新推薦)
前言
最近一些時(shí)間我有研究,如何實(shí)現(xiàn)一個(gè)視頻會(huì)議功能,但是找了好多資料都不太理想,最終參考了一個(gè)文章
WebRTC實(shí)現(xiàn)雙端音視頻聊天(Vue3 + SpringBoot)
只不過(guò),它的實(shí)現(xiàn)效果里面只會(huì)播放本地的mp4視頻文件,但是按照它的原理是可以正常的實(shí)現(xiàn)音視頻通話的
它的最終效果是這樣的
然后我的實(shí)現(xiàn)邏輯在它的基礎(chǔ)上進(jìn)行了優(yōu)化
實(shí)現(xiàn)了如下效果,如下是我部署項(xiàng)目到服務(wù)器
之后,和朋友驗(yàn)證之后的截圖
針對(duì)它的邏輯,我優(yōu)化了如下幾點(diǎn)
第一個(gè)人可以輸入房間號(hào)創(chuàng)建房間,需要注意的是,當(dāng)前第二個(gè)人還沒(méi)加入進(jìn)來(lái)的時(shí)候,視頻兩邊都不展示
第二個(gè)人根據(jù)第一個(gè)人的房間號(hào)輸入進(jìn)行加入房間,等待視頻流的加載就可以互相看到兩邊的視頻和聽(tīng)到音頻
添加了關(guān)閉/開(kāi)啟麥克風(fēng)和攝像頭功能
ps: 需要注意的是,我接下來(lái)分享的代碼邏輯,如果某個(gè)人突然加入別的房間,原房間它視頻分享還是在的,我沒(méi)有額外進(jìn)行處理關(guān)閉原房間的音視頻流,大家可根據(jù)這個(gè)進(jìn)行調(diào)整
題外話,根據(jù)如上的原理,你可以進(jìn)一步優(yōu)化,將其開(kāi)發(fā)一個(gè)視頻會(huì)議功能,當(dāng)前我有開(kāi)發(fā)一個(gè)類似的,但是本次只分享雙人音視頻通話會(huì)議項(xiàng)目
VUE邏輯
如下為前端部分邏輯,需要注意的是,本次項(xiàng)目還是沿用參考文章的
VUE3
項(xiàng)目
前端項(xiàng)目結(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ù)器,需要申請(qǐng)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">對(duì)方:<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="請(qǐng)輸入房間號(hào)" /> <button @click="createRoom">創(chuàng)建房間</button> <button @click="joinRoomWithId">加入房間</button> </div> <div class="media-controls"> <button @click="toggleAudio"> {{ isAudioEnabled ? '關(guān)閉麥克風(fēng)' : '打開(kāi)麥克風(fēng)' }} </button> <button @click="toggleVideo"> {{ isVideoEnabled ? '關(guān)閉攝像頭' : '打開(kāi)攝像頭' }} </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連接對(duì)象 let signalingSocket; // 信令服務(wù)器socket對(duì)象 let userId; // 當(dāng)前用戶ID let oppositeUserId; // 對(duì)方用戶ID let logData = ref(["日志初始化..."]); // 請(qǐng)求根路徑,如果需要部署服務(wù)器,把對(duì)應(yīng)ip改成自己服務(wù)器ip let BaseUrl = "https://localhost:8095/meetingV1s" let wsUrl = "wss://localhost:8095/meetingV1s"; // candidate信息 let candidateInfo = ""; // 發(fā)起端標(biāo)識(shí) let offerFlag = false; // 房間狀態(tài)文本 let roomStatusText = ref("點(diǎn)擊'加入房間'開(kāi)始音視頻聊天"); // 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" // } // ]; // ============< 看這 >================ // 沒(méi)有搭建STUN和TURN服務(wù)器的使用如下ice配置即可 const iceServers = [ { urls: "stun:stun.l.google.com:19302" // Google 的 STUN 服務(wù)器 } ]; // 在 script setup 中添加新的變量聲明 const roomId = ref(''); // 房間號(hào) const isAudioEnabled = ref(true); // 音頻狀態(tài) const isVideoEnabled = ref(true); // 視頻狀態(tài) onMounted(() => { generateRandomId(); }) // 加入房間,開(kāi)啟本地?cái)z像頭獲取音視頻流數(shù)據(jù)。 function joinRoomHandle() { roomStatusText.value = "等待對(duì)方加入房間..." getVideoStream(); } // 獲取本地視頻 模擬從本地?cái)z像頭獲取音視頻流數(shù)據(jù) async function getVideoStream() { try { localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.value.srcObject = localStream; wlog(`獲取本地流成功~`) createPeerConnection(); // 創(chuàng)建RTC對(duì)象,監(jiān)聽(tīng)candidate } catch (err) { console.error('獲取本地媒體流失敗:', err); } } // 初始化 WebSocket 連接 function initWebSocket() { wlog("開(kāi)始連接websocket") // 連接ws時(shí)攜帶用戶ID和房間號(hào) 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消息,開(kāi)始解析...") 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,開(kāi)始解析..."); offerHandle(parseMsg.data); } else if (parseMsg.type == "answer") { wlog("收到接收端的answer,開(kāi)始解析..."); answerHandle(parseMsg.data); }else if(parseMsg.type == "candidate"){ wlog("收到遠(yuǎn)端candidate,開(kāi)始解析..."); 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ā)送給對(duì)端 let paramObj = { userId: oppositeUserId, type: "answer", data: JSON.stringify(answer) } // 執(zhí)行發(fā)送 const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj); } // 加入處理器 function joinHandle(userIds) { // 判斷連接的用戶個(gè)數(shù) if (userIds.length == 1 && userIds[0] == userId) { wlog("標(biāo)識(shí)為發(fā)起端,等待對(duì)方加入房間...") isRoomEmpty.value = true; // 存在一個(gè)連接并且是自身,標(biāo)識(shí)我們是發(fā)起端 offerFlag = true; } else if (userIds.length > 1) { // 對(duì)方加入了 wlog("對(duì)方已連接...") isRoomEmpty.value = false; // 取出對(duì)方ID for (let id of userIds) { if (id != userId) { oppositeUserId = id; } } wlog(`對(duì)端ID: ${oppositeUserId}`) // 開(kāi)始交換SDP和Candidate swapVideoInfo() } } // 交換SDP和candidate async function swapVideoInfo() { wlog("開(kāi)始交換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連接對(duì)象并監(jiān)聽(tīng)和獲取condidate信息 function createPeerConnection() { wlog("開(kāi)始創(chuàng)建PC對(duì)象...") peerConnection = new RTCPeerConnection(iceServers); wlog("創(chuàng)建PC對(duì)象成功") // 創(chuàng)建RTC連接對(duì)象后連接websocket initWebSocket(); // 監(jiān)聽(tīng)網(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)聽(tīng)遠(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)聽(tīng)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('請(qǐng)輸入房間號(hào)'); 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('請(qǐng)輸入房間號(hào)'); 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 ? '打開(kāi)' : '關(guān)閉'}`); } } } // 切換視頻 function toggleVideo() { if (localStream) { const videoTrack = localStream.getVideoTracks()[0]; if (videoTrack) { videoTrack.enabled = !videoTrack.enabled; isVideoEnabled.value = videoTrack.enabled; wlog(`攝像頭已${videoTrack.enabled ? '打開(kāi)' : '關(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邏輯
如下為后端邏輯,項(xiàng)目結(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證書的秘鑰庫(kù)的路徑,如果部署到服務(wù)器,必須要開(kāi)啟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
入口文件
// 這個(gè)是自己實(shí)際項(xiàng)目位置 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) { // 保存用戶會(huì)話 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("收到消息"); } // 處理斷開(kāi)的連接 @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) { // 從房間和會(huì)話管理中移除用戶 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ā)送用戶離開(kāi)更新消息", uid); } } log.info("用戶 {} 斷開(kāi)連接,房間:{}", 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 中實(shí)現(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 中實(shí)現(xiàn)加入房間邏輯 boolean success = userManager.joinRoom(roomId, userId); Map<String, Object> response = new HashMap<>(); response.put("success", success); return ResponseEntity.ok(response); } }
用戶管理器單例對(duì)象
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: 用戶管理器單例對(duì)象 **/ @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; } /** * 離開(kāi)房間 * @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("用戶 {} 離開(kāi)了房間 {}", 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對(duì)象
package com.mh.dto.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * Date:2024/11/15 * author:zmh * description: 消息輸出前端Vo對(duì)象 **/ @Data @AllArgsConstructor @NoArgsConstructor public class MessageOut { /** * 消息類型【join, offer, answer, candidate, leave】 */ private String type; /** * 消息內(nèi)容 前端stringFiy序列化后字符串 */ private Object data; }
消息接收Vo對(duì)象
package com.mh.dto.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * Date:2024/11/15 * author:zmh * description: 消息接收Vo對(duì)象 **/ @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é)語(yǔ)
如上為vue+springboot+webtrc+websocket實(shí)現(xiàn)雙人音視頻通話會(huì)議的全部邏輯,如有遺漏后續(xù)會(huì)進(jìn)行補(bǔ)充
到此這篇關(guān)于vue+springboot+webtrc+websocket實(shí)現(xiàn)雙人音視頻通話會(huì)議的文章就介紹到這了,更多相關(guān)vue springboot音視頻通話會(huì)議內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中前臺(tái)往后臺(tái)傳遞多個(gè)id參數(shù)的實(shí)例
下面小編就為大家?guī)?lái)一篇Java中前臺(tái)往后臺(tái)傳遞多個(gè)id參數(shù)的實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07SpringBoot+jsp項(xiàng)目啟動(dòng)出現(xiàn)404的解決方法
這篇文章主要介紹了SpringBoot+jsp項(xiàng)目啟動(dòng)出現(xiàn)404的解決方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03SpringBoot如何接收前端傳來(lái)的json數(shù)據(jù)
這篇文章主要介紹了SpringBoot如何接收前端傳來(lái)的json數(shù)據(jù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04Java 高并發(fā)十: JDK8對(duì)并發(fā)的新支持詳解
本文主要介紹Java 高并發(fā)JDK8的支持,這里整理了詳細(xì)的資料及1. LongAdder 2. CompletableFuture 3. StampedLock的介紹,有興趣的小伙伴可以參考下2016-09-09java圖片滑動(dòng)驗(yàn)證(登錄驗(yàn)證)原理與實(shí)現(xiàn)方法詳解
這篇文章主要介紹了java圖片滑動(dòng)驗(yàn)證(登錄驗(yàn)證)原理與實(shí)現(xiàn)方法,結(jié)合實(shí)例形式詳細(xì)分析了java圖片滑動(dòng)登錄驗(yàn)證的相關(guān)原理、實(shí)現(xiàn)方法與操作技巧,需要的朋友可以參考下2019-09-09