无锡找厂网站,acm网站免费做,wordpress模板旅游,怎样办网站前言
最近一些时间我有研究#xff0c;如何实现一个视频会议功能#xff0c;但是找了好多资料都不太理想#xff0c;最终参考了一个文章 WebRTC实现双端音视频聊天#xff08;Vue3 SpringBoot#xff09; 只不过#xff0c;它的实现效果里面只会播放本地的mp4视频文件如何实现一个视频会议功能但是找了好多资料都不太理想最终参考了一个文章 WebRTC实现双端音视频聊天Vue3 SpringBoot 只不过它的实现效果里面只会播放本地的mp4视频文件但是按照它的原理是可以正常的实现音视频通话的 它的最终效果是这样的 然后我的实现逻辑在它的基础上进行了优化 实现了如下效果如下是我部署项目到服务器之后和朋友验证之后的截图 针对它的逻辑我优化了如下几点 第一个人可以输入房间号创建房间需要注意的是当前第二个人还没加入进来的时候视频两边都不展示第二个人根据第一个人的房间号输入进行加入房间等待视频流的加载就可以互相看到两边的视频和听到音频添加了关闭/开启麦克风和摄像头功能 ps: 需要注意的是我接下来分享的代码逻辑如果某个人突然加入别的房间原房间它视频分享还是在的我没有额外进行处理关闭原房间的音视频流大家可根据这个进行调整 题外话根据如上的原理你可以进一步优化将其开发一个视频会议功能,当前我有开发一个类似的但是本次只分享双人音视频通话会议项目 VUE逻辑 如下为前端部分逻辑,需要注意的是本次项目还是沿用参考文章的VUE3项目 前端项目结构如下: 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 axios1.7.7vite.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: {// 如果需要部署服务器需要申请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.vuecreateApp(App).mount(#app)
App.vue
templatediv classvideo-chatdiv v-ifisRoomEmptyp{{ roomStatusText }}/p/div!-- 视频双端显示 --div classvideo_boxdiv classself_videodiv classtext_tip我span classuserId{{ userId }}/span/divvideo reflocalVideo autoplay playsinline/video/divdiv classremote_videodiv classtext_tip对方span classuserId{{ oppositeUserId }}/span/divvideo refremoteVideo autoplay playsinline/video/div/div!-- 加入房间按钮 --div classroom-controlsdiv classroom-inputinput v-modelroomId placeholder请输入房间号 /button clickcreateRoom创建房间/buttonbutton clickjoinRoomWithId加入房间/button/divdiv classmedia-controlsbutton clicktoggleAudio{{ isAudioEnabled ? 关闭麦克风 : 打开麦克风 }}/buttonbutton clicktoggleVideo{{ isVideoEnabled ? 关闭摄像头 : 打开摄像头 }}/button/div/div!-- 日志打印 --div classlog_boxprediv v-for(item, index) of logData :keyindex{{ item }}/div/pre/div/div
/templatescript setup
import { ref, onMounted, nextTick } from vue;
import axios from axios;// WebRTC 相关变量
const localVideo ref(null);
const remoteVideo ref(null);
const isRoomEmpty ref(true); // 判断房间是否为空let localStream; // 本地流数据
let peerConnection; // RTC连接对象
let signalingSocket; // 信令服务器socket对象
let userId; // 当前用户ID
let oppositeUserId; // 对方用户IDlet logData ref([日志初始化...]);// 请求根路径如果需要部署服务器把对应ip改成自己服务器ip
let BaseUrl https://localhost:8095/meetingV1slet wsUrl wss://localhost:8095/meetingV1s;// candidate信息
let candidateInfo ;// 发起端标识
let offerFlag false;// 房间状态文本
let roomStatusText ref(点击加入房间开始音视频聊天);// STUN 服务器
// const iceServers [
// {
// urls: stun:stun.l.google.com:19302 // Google 的 STUN 服务器
// },
// {
// urls: stun:自己的公网IP:3478 // 自己的Stun服务器
// },
// {
// urls: turn:自己的公网IP:3478, // 自己的 TURN 服务器
// username: maohe,
// credential: maohe
// }
// ];
// 看这
// 没有搭建STUN和TURN服务器的使用如下ice配置即可
const iceServers [{urls: stun:stun.l.google.com:19302 // Google 的 STUN 服务器}
];// 在 script setup 中添加新的变量声明
const roomId ref(); // 房间号
const isAudioEnabled ref(true); // 音频状态
const isVideoEnabled ref(true); // 视频状态onMounted(() {generateRandomId();
})// 加入房间开启本地摄像头获取音视频流数据。
function joinRoomHandle() {roomStatusText.value 等待对方加入房间...getVideoStream();
}// 获取本地视频 模拟从本地摄像头获取音视频流数据
async function getVideoStream() {try {localStream await navigator.mediaDevices.getUserMedia({video: true,audio: true});localVideo.value.srcObject localStream;wlog(获取本地流成功~)createPeerConnection(); // 创建RTC对象监听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(解析结果${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;// 取出对方IDfor (let id of userIds) {if (id ! userId) {oppositeUserId id;}}wlog(对端ID: ${oppositeUserId})// 开始交换SDP和CandidateswapVideoInfo()}
}// 交换SDP和candidate
async function swapVideoInfo() {wlog(开始交换Sdp和Candidate...);// 检查是否为发起端如果是创建offer设置到本地并发送给远端if (offerFlag) {wlog(发起端创建offer)let offer await peerConnection.createOffer()await peerConnection.setLocalDescription(offer); // 将媒体信息设置到本地wlog(发启端设置SDP-offer到本地);// 构造消息ws发送给远端let paramObj {userId: oppositeUserId,type: offer,data: JSON.stringify(offer)};wlog(构造offer信息发送给远端${paramObj})// 执行发送const res await axios.post(${BaseUrl}/rtcs/sendMessage, paramObj);}
}// 将candidate信息发送给远端
async function sendCandidate(candidate) {// 构造消息ws发送给远端let paramObj {userId: oppositeUserId,type: candidate,data: JSON.stringify(candidate)};wlog(构造candidate信息发送给远端${paramObj});// 执行发送const res await axios.post(${BaseUrl}/rtcs/sendMessage, paramObj);}// 创建RTC连接对象并监听和获取condidate信息
function createPeerConnection() {wlog(开始创建PC对象...)peerConnection new RTCPeerConnection(iceServers);wlog(创建PC对象成功)// 创建RTC连接对象后连接websocketinitWebSocket();// 监听网络信息ICE CandidatepeerConnection.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(); // 强制播放}});};// 监听ice连接状态peerConnection.oniceconnectionstatechange () {wlog(RTC连接状态改变${peerConnection.iceConnectionState});};// 添加本地音视频流到 PeerConnectionlocalStream.getTracks().forEach(track {peerConnection.addTrack(track, localStream);});
}// 日志编写
function wlog(text) {logData.value.unshift(text);
}// 给用户生成随机ID.
function generateRandomId() {userId Math.random().toString(36).substring(2, 12); // 生成10位的随机IDwlog(分配到ID:${userId})
}// 创建房间
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(创建房间成功${roomId.value});joinRoomHandle();}} catch (error) {wlog(创建房间失败${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(麦克风已${audioTrack.enabled ? 打开 : 关闭});}}
}// 切换视频
function toggleVideo() {if (localStream) {const videoTrack localStream.getVideoTracks()[0];if (videoTrack) {videoTrack.enabled !videoTrack.enabled;isVideoEnabled.value videoTrack.enabled;wlog(摄像头已${videoTrack.enabled ? 打开 : 关闭});}}
}
/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;
}
/styleSpringBoot逻辑 如下为后端逻辑,项目结构如下: pom.xml
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.7.9/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.mh/groupIdartifactIdwebrtc-backend/artifactIdversion0.0.1-SNAPSHOT/versionnamewebrtc-backend/namedescriptionwebrtc-backend/descriptionpropertiesjava.version1.8/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-websocket/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.34/version/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdversion2.6.2/versionconfigurationmainClasscom.mh.WebrtcBackendApplication/mainClasslayoutZIP/layout/configurationexecutionsexecutiongoalsgoalrepackage/goal/goals/execution/executions/plugin/plugins/build/project
application.yml
server:port: 8095servlet:context-path: /meetingV1sssl: #ssl配置enabled: true # 默认为true#key-alias: alias-key # 别名(可以不进行配置)# 保存SSL证书的秘钥库的路径如果部署到服务器必须要开启ssl才能获取到摄像头和麦克风key-store: classpath:www.springsso.top.jks# ssl证书密码key-password: gf71v8lfkey-store-password: gf71v8lfkey-store-type: JKStomcat: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();// 用户连接成功Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {// 获取用户ID和房间IDString userId getParameterByName(session.getUri(), userId);String roomId getParameterByName(session.getUri(), roomId);if (userId ! null roomId ! null) {// 保存用户会话userManager.addUser(userId, session);log.info(用户 {} 连接成功房间{}, userId, roomId);// 获取房间中的所有用户SetString roomUsers userManager.getRoomUsers(roomId);// 通知房间内所有用户包括新加入的用户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(向用户 {} 发送房间更新消息, uid);}}}}// 接收到客户端消息解析消息内容进行分发Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 转换并分发消息log.info(收到消息);}// 处理断开的连接Overridepublic 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);// 获取更新后的房间用户列表SetString remainingUsers userManager.getRoomUsers(roomId);// 通知房间内的其他用户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(向用户 {} 发送用户离开更新消息, uid);}}log.info(用户 {} 断开连接房间{}, userId, roomId);}}// 辅助方法从URI中获取参数值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;Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(rtcWebSocketHandler, /rtc).setAllowedOrigins(*);}
}
webRtc相关接口
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 相关接口**/RestController
Slf4j
CrossOrigin
RequiredArgsConstructor
RequestMapping(/rtcs)
public class RtcController {private final UserManager userManager;/*** 给指定用户发送执行类型消息* param messageReceive 消息参数接收Vo* return ·*/PostMapping(/sendMessage)public Boolean sendMessage(RequestBody MessageReceive messageReceive){userManager.sendMessage(messageReceive);return true;}PostMapping(/createRoom)public ResponseEntity? createRoom(RequestBody MapString, String params) {String roomId params.get(roomId);String userId params.get(userId);// 在 UserManager 中实现房间创建逻辑boolean success userManager.createRoom(roomId, userId);MapString, Object response new HashMap();response.put(success, success);return ResponseEntity.ok(response);}PostMapping(/joinRoom)public ResponseEntity? joinRoom(RequestBody MapString, String params) {String roomId params.get(roomId);String userId params.get(userId);// 在 UserManager 中实现加入房间逻辑boolean success userManager.joinRoom(roomId, userId);MapString, 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 HashMapString, WebSocketSession userMap new HashMap();// 添加房间管理的Mapprivate final MapString, SetString 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构造成list返回public ListString getAllUserId() {return userMap.keySet().stream().collect(Collectors.toList());}// 通知用户加入-广播消息public void sendMessageAllUser() throws IOException {// 获取所有连接用户ID列表ListString 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));}}/*** 创建房间* param roomId 房间ID* param userId 用户ID* return 创建结果*/public boolean createRoom(String roomId, String userId) {if (roomUsers.containsKey(roomId)) {log.warn(房间 {} 已存在, roomId);return false;}SetString users new HashSet();users.add(userId);roomUsers.put(roomId, users);log.info(用户 {} 创建了房间 {}, userId, roomId);return true;}/*** 加入房间* param roomId 房间ID* param userId 用户ID* return 加入结果*/public boolean joinRoom(String roomId, String userId) {SetString 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) {SetString 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 SetString getRoomUsers(String roomId) {return roomUsers.getOrDefault(roomId, new HashSet());}// 修改现有的 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(消息发送成功: type{}, to{}, type, userId);} catch (Exception e) {log.error(消息发送失败, 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;/*** 消息内容 前端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;/*** 消息内容 前端stringFiy序列化后字符串*/private String data;
}
结语
如上为vuespringbootwebtrcwebsocket实现双人音视频通话会议的全部逻辑如有遗漏后续会进行补充