欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

vue+springboot+webtrc+websocket實(shí)現(xiàn)雙人音視頻通話會(huì)議(最新推薦)

 更新時(shí)間:2025年05月15日 10:56:02   作者:相與還  
這篇文章主要介紹了vue+springboot+webtrc+websocket實(shí)現(xiàn)雙人音視頻通話會(huì)議,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧

前言

最近一些時(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)文章

  • Java8 Comparator: 列表排序的深入講解

    Java8 Comparator: 列表排序的深入講解

    這篇文章主要給大家介紹了關(guān)于Java 8 Comparator: 列表排序的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Java8具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05
  • Java雙向鏈表的操作

    Java雙向鏈表的操作

    這篇文章主要介紹了Java雙向鏈表的操作,雙向鏈表,對(duì)于該鏈表中的任意節(jié)點(diǎn),既可以通過(guò)該節(jié)點(diǎn)向前遍歷,也可以通過(guò)該節(jié)點(diǎn)向后遍歷,雙向鏈表在實(shí)際工程中應(yīng)用非常廣泛,是使用鏈表這個(gè)結(jié)構(gòu)的首選
    2022-06-06
  • 徹底搞定堆排序:二叉堆

    徹底搞定堆排序:二叉堆

    二叉堆有兩種:最大堆和最小堆。最大堆:父結(jié)點(diǎn)的鍵值總是大于或等于任何一個(gè)子節(jié)點(diǎn)的鍵值;最小堆:父結(jié)點(diǎn)的鍵值總是小于或等于任何一個(gè)子節(jié)點(diǎn)的鍵值
    2021-07-07
  • Java中前臺(tái)往后臺(tái)傳遞多個(gè)id參數(shù)的實(shí)例

    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-07
  • SpringBoot+jsp項(xiàng)目啟動(dòng)出現(xiàn)404的解決方法

    SpringBoot+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-03
  • Java中的自旋鎖與適應(yīng)性自旋鎖的區(qū)別

    Java中的自旋鎖與適應(yīng)性自旋鎖的區(qū)別

    這篇文章主要介紹了Java中的自旋鎖與適應(yīng)性自旋鎖的區(qū)別,當(dāng)一個(gè)線程嘗試去獲取某一把鎖的時(shí)候,如果這個(gè)鎖此時(shí)已經(jīng)被別人獲取(占用),那么此線程就無(wú)法獲取到這把鎖,該線程將會(huì)等待,間隔一段時(shí)間后會(huì)再次嘗試獲取,需要的朋友可以參考下
    2023-10-10
  • SpringBoot如何接收前端傳來(lái)的json數(shù)據(jù)

    SpringBoot如何接收前端傳來(lái)的json數(shù)據(jù)

    這篇文章主要介紹了SpringBoot如何接收前端傳來(lái)的json數(shù)據(jù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • 淺談java中定義泛型類和定義泛型方法的寫法

    淺談java中定義泛型類和定義泛型方法的寫法

    下面小編就為大家?guī)?lái)一篇淺談java中定義泛型類和定義泛型方法的寫法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-02-02
  • Java 高并發(fā)十: JDK8對(duì)并發(fā)的新支持詳解

    Java 高并發(fā)十: JDK8對(duì)并發(fā)的新支持詳解

    本文主要介紹Java 高并發(fā)JDK8的支持,這里整理了詳細(xì)的資料及1. LongAdder 2. CompletableFuture 3. StampedLock的介紹,有興趣的小伙伴可以參考下
    2016-09-09
  • java圖片滑動(dòng)驗(yàn)證(登錄驗(yàn)證)原理與實(shí)現(xiàn)方法詳解

    java圖片滑動(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

最新評(píng)論