web网页端使用webSocket实现语音通话功能(SpringBoot+VUE)
写在前面
最近在写一个web项目,需要实现web客户端之间的语音通话,期望能够借助webSocket全双工通信的方式来实现,但是网上没有发现可以正确使用的代码。网上能找到的一个代码使用之后只能听到“嘀嘀嘀”的杂音
解决方案:使用Json来传递数据代替原有的二进制输入输出流
技术栈:VUE3、SpingBoot、WebSocket
Java后端代码
pom.xml
配置Maven所需的jar包
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocketConfig.java
webSocket配置类
package com.shu.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration
public class WebSocketConfig {/*** 注入ServerEndpointExporter,* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}
WebSocketAudioServer.java
webSocket实现类,其中roomId是语音聊天室的id
,userId是发送语音的用户id
所以前端请求加入webSocket时候的请求样例应该是:ws://localhost:8080/audio/1/123
这个请求中1是roomId,123是userId,这里建议使用ws,一般来说ws对于http,wss对应https
package com.shu.socket;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;/*** @Author:Long**/
@Component
@Slf4j
@ServerEndpoint(value = "/audio/{roomId}/{userId}")
public class WebSocketAudioServer {/** 当前在线连接数。应该把它设计成线程安全的 */private static int onlineCount = 0;/** 存放每个客户端对应的MyWebSocket对象。实现服务端与单一客户端通信的话,其中Key可以为用户标识 */private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();private static CopyOnWriteArraySet<WebSocketAudioServer> webSocketSet = new CopyOnWriteArraySet<>();/** 与某个客户端的连接会话,需要通过它来给客户端发送数据 */private Session webSocketsession;/** 当前发消息的人员编号 */private String roomId;private String userId;/*** 连接建立成功调用的方法* * @param param 发送者ID,是由谁发送的* @param WebSocketsession 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据*/@OnOpenpublic void onOpen(@PathParam(value = "roomId") String roomId, @PathParam(value = "userId") String userId,Session webSocketsession) {// 接收到发送消息的人员编号this.roomId = roomId;this.userId = userId;// 加入map中,绑定当前用户和socketsessionPool.put(userId, webSocketsession);webSocketSet.add(this);this.webSocketsession = webSocketsession;// 在线数加1addOnlineCount();System.out.println("user编号:" + userId + ":加入Room:" + roomId + "语音聊天 " + "总数为:" + webSocketSet.size());}/*** 连接关闭调用的方法*/@OnClosepublic void onClose() {try {sessionPool.remove(this.userId);} catch (Exception e) {}}/*** 收到客户端语音消息后调用的方法**/@OnMessage(maxMessageSize = 5242880)public void onMessage(@PathParam(value = "roomId") String roomId, @PathParam(value = "userId") String userId,String inputStream) {try {for (WebSocketAudioServer webSocket : webSocketSet) {try {if (webSocket.webSocketsession.isOpen() && webSocket.roomId.equals(roomId)&& !webSocket.userId.equals(userId)) {webSocket.webSocketsession.getBasicRemote().sendText(inputStream);}} catch (Exception e) {e.printStackTrace();}}} catch (Exception e) {e.printStackTrace();}}/*** 发生错误时调用** @param session* @param error*/@OnErrorpublic void onError(Session session, Throwable error) {error.printStackTrace();}/*** 为指定用户发送消息** @param message 消息内容* @throws IOException*/public void sendMessage(String message) throws IOException {// 加同步锁,解决多线程下发送消息异常关闭synchronized (this.webSocketsession) {this.webSocketsession.getBasicRemote().sendText(message);}}/*** 获取当前在线人数* * @return 返回当前在线人数*/public static synchronized int getOnlineCount() {return onlineCount;}/*** 增加当前在线人数*/public static synchronized void addOnlineCount() {WebSocketAudioServer.onlineCount++;}/*** 减少当前在线人数*/public static synchronized void subOnlineCount() {WebSocketAudioServer.onlineCount--;}public List<String> getOnlineUser(String roomId) {List<String> userList = new ArrayList<String>();for (WebSocketAudioServer webSocketAudioServer : webSocketSet) {try {if (webSocketAudioServer.webSocketsession.isOpen() && webSocketAudioServer.roomId.equals(roomId)) {if (!userList.contains(webSocketAudioServer.userId)) {userList.add(webSocketAudioServer.userId);}}} catch (Exception e) {e.printStackTrace();}}return userList;}
}
VUE前端代码
audioChat.vue
这段代码是博主从自己的vue代码中截取出来的(原本的代码太多了),可能有些部分代码有函数没写上(如果有错的话麻烦大家在评论区指出,博主会及时修改)
注意事项
之前有博客使用二进制数据输入输出流来向后端传输数据,但是功能无法实现,后来发现那位博主的数据并没有发成功,我直接在Java中使用Json来传输float数组数据,实现了语音通话功能
。
<template><div class="play-audio"><button @click="startCall" ref="start">开始对讲</el-button><button @click="stopCall" ref="stop">结束对讲</el-button></div>
</template><script setup>
// 语音聊天的变量
const audioSocket = ref(null);
let mediaStack;
let audioCtx;
let scriptNode;
let source;
let play;
// 语音socket
const connectAudioWebSocket = () => {//获取tokenconst token = window.sessionStorage.getItem("token");if (!token) {return;}let url = "ws://localhost:8080/audio/1/123"; //roomId:1 ,userId123audioSocket.value = new WebSocket(url); // 替换为实际的 WebSocket 地址audioSocket.value.onopen = () => {console.log("audioSocket connected");};audioSocket.value.onmessage = (event) => {// 将接收的数据转换成与传输过来的数据相同的Float32Arrayconst jsonAudio = JSON.parse(event.data);// let buffer = new Float32Array(event.data);let buffer = new Float32Array(4096);for (let i = 0; i < 4096; i++) {// buffer.push(parseFloat(jsonAudio[i]));buffer[i] = parseFloat(jsonAudio[i]);}// 创建一个空白的AudioBuffer对象,这里的4096跟发送方保持一致,48000是采样率const myArrayBuffer = audioCtx.createBuffer(1, 4096, 16000);// 也是由于只创建了一个音轨,可以直接取到0const nowBuffering = myArrayBuffer.getChannelData(0);// 通过循环,将接收过来的数据赋值给简单音频对象for (let i = 0; i < 4096; i++) {nowBuffering[i] = buffer[i];}// 使用AudioBufferSourceNode播放音频const source = audioCtx.createBufferSource();source.buffer = myArrayBuffer;const gainNode = audioCtx.createGain();source.connect(gainNode);gainNode.connect(audioCtx.destination);var muteValue = 1;if (!play) {// 是否静音muteValue = 0;}gainNode.gain.setValueAtTime(muteValue, audioCtx.currentTime);source.start();};audioSocket.value.onclose = () => {console.log("audioSocket closed");};audioSocket.value.onerror = (error) => {console.error("audioSocket error:", error);};
};
// 开始对讲
function startCall() {isInChannel.value = true;play = true;audioCtx = new AudioContext();connectAudioWebSocket();// 该变量存储当前MediaStreamAudioSourceNode的引用// 可以通过它关闭麦克风停止音频传输// 创建一个ScriptProcessorNode 用于接收当前麦克风的音频scriptNode = audioCtx.createScriptProcessor(4096, 1, 1);navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((stream) => {mediaStack = stream;source = audioCtx.createMediaStreamSource(stream);source.connect(scriptNode);scriptNode.connect(audioCtx.destination);}).catch(function (err) {/* 处理error */isInChannel.value = false;console.log("err", err);});// 当麦克风有声音输入时,会调用此事件// 实际上麦克风始终处于打开状态时,即使不说话,此事件也在一直调用scriptNode.onaudioprocess = (audioProcessingEvent) => {const inputBuffer = audioProcessingEvent.inputBuffer;// console.log("inputBuffer",inputBuffer);// 由于只创建了一个音轨,这里只取第一个频道的数据const inputData = inputBuffer.getChannelData(0);// 通过socket传输数据,实际上传输的是Float32Arrayif (audioSocket.value.readyState === 1) {// console.log("发送的数据",inputData);// audioSocket.value.send(inputData);let jsonData = JSON.stringify(inputData);audioSocket.value.send(jsonData);// stopCall();}};
}
// 关闭麦克风
function stopCall() {isInChannel.value = false;play = false;mediaStack.getTracks()[0].stop();scriptNode.disconnect();if (audioSocket.value) {audioSocket.value.close();audioSocket.value = null;}
}
</script>
关于Chrome或Edge浏览器报错
关于谷歌浏览器提示TypeError: Cannot read property ‘getUserMedia’ of undefined
解决方案:
1.网页使用https访问,服务端升级为https访问,配置ssl证书
2.使用localhost或127.0.0.1 进行访问
3.修改浏览器安全配置(最直接、简单)
在chrome浏览器中输入如下指令
chrome://flags/#unsafely-treat-insecure-origin-as-secure
开启 Insecure origins treated as secure
在下方输入栏内输入你访问的地址url,然后将右侧Disabled 改成 Enabled即可
浏览器会提示重启, 点击Relaunch即可
相关文章:

web网页端使用webSocket实现语音通话功能(SpringBoot+VUE)
写在前面 最近在写一个web项目,需要实现web客户端之间的语音通话,期望能够借助webSocket全双工通信的方式来实现,但是网上没有发现可以正确使用的代码。网上能找到的一个代码使用之后只能听到“嘀嘀嘀”的杂音 解决方案:使用Jso…...

读取spring boot项目resource目录下的文件
背景 项目开发过程中,有一些情况下将配置文件放在resource下能简化代码实现和部署时的打包步骤。例如: 项目中使用的数据库升级脚本、初始化脚本。将文件放到resource下,打包在jar包中,不能直接通过File路径读取。下面介绍两种读…...
R语言生物群落(生态)数据统计分析与绘图实践技术
R 语言作的开源、自由、免费等特点使其广泛应用于生物群落数据统计分析。生物群落数据多样而复杂,涉及众多统计分析方法。本内容以生物群落数据分析中的最常用的统计方法回归和混合效应模型、多元统计分析技术及结构方程等数量分析方法为主线,通过多个来…...

c# OpenCV 检测(斑点检测、边缘检测、轮廓检测)(五)
在C#中使用OpenCV进行图像处理时,可以使用不同的算法和函数来实现斑点检测、边缘检测和轮廓检测。 斑点检测边缘检测轮廓检测 一、斑点检测(Blob) 斑点检测是指在图像中找到明亮或暗的小区域(通常表示为斑点)&#…...

PHP下载安装以及基本配置
目录 引言 官网 下载 配置 1. 鼠标右键“此电脑”>“属性” 2. 打开高级系统设置 3. 打开环境变量 4. 双击系统变量中的path 5. 新建新的path 6. 将刚刚安装的位置加入环境变量 7. 检查是否安装成功 引言 PHP("PHP: Hypertext Preprocessor"…...
黑苹果安装经验总结2023-12
最近2个月安装了3台黑苹果 B85,I3-4330,HD7750,最容易安装,MacOS12一次成功山寨X99,E5-2650V4,RX470,难度高惠普Z840,X99平台,2颗E5-2630,128G内存ÿ…...

基于深度学习的森林火焰烟雾检测系统(含UI界面,yolov8、Python代码,数据集)
项目介绍 项目中所用到的算法模型和数据集等信息如下: 算法模型: yolov8 yolov8主要包含以下几种创新: 1. 添加注意力机制(SE、CBAM等) 2. 修改可变形卷积(DySnake-主干c…...

测试开发体系介绍——测试体系介绍-L1
目录: 软件测试基础概念 软件测试:软件测试作用:软件缺陷:软件测试原则:软件测试对象:测试用例软件开发流程 软件:软件生命周期:软件开发流程:瀑布模型:瀑布模型优缺点敏捷开发模型: XP - 极限编程:SCRUM:DevOps:DevOps 生命周期:DevOps 对发…...
Linux中的链接运算符详解 - 提高编程效率与性能
Linux 命令的链接意味着,组合多个命令并根据它们之间使用的操作符的行为使它们执行。 Linux 中的命令链就像您在 shell 本身编写简短的 shell 脚本,然后直接从终端执行它们。链接使得流程自动化成为可能。 此外,无人值守的机器可以在链接操作…...

JS模块化规范之ES6及UMD
JS模块化规范之ES6及总结 前言ES6模块化概念基本使用ES6实现 UMD(Universal Module Definition)总结 前言 ESM在模块之间的依赖关系是高度确定的,与运行状态无关,编译工具只需要对ESM模块做静态分析,就可以从代码字面中推断出哪些模块值未曾被…...

XM平台官网开户注册流程图解
注册前准备 在进行XM外汇官网注册之前,首先需要准备必要的信息,包括个人身份信息、联系方式以及相关财务信息。确保这些信息的准确性是保证注册流程顺利进行的关键。 一、要访问XM外汇官方网站,首先打开您的浏览器。在浏览器的地址栏中输入…...

【Linux进阶之路】线程
文章目录 一、初始线程1.概念2.执行3.调度4.切换 二、线程控制1.创建2.等待3.分离4.退出5.取消 三、线程安全1.互斥1.1初始1.2理解1.3锁1.3.1概念1.3.2原理1.3.4死锁 2.同步2.1概念2.2原理 3.生产消费者模型 总结尾序 一、初始线程 1.概念 简单的概念: 线程就是一…...

个性化TikTok外贸工具定制!突破营销新境界!
随着全球化的加速发展,外贸行业正面临着前所未有的机遇和挑战,在这个竞争激烈的市场环境中,如何脱颖而出,吸引更多的潜在客户,成为每个外贸企业亟待解决的问题,而个性化TikTok外贸工具的定制,正…...

设计模式-门面模式
设计模式专栏 模式介绍模式特点应用场景门面模式和代理模式的区别代码示例Java实现门面模式Python实现门面模式 门面模式在spring中的应用 模式介绍 门面模式是一种常用的软件设计模式,也称为外观模式。它提供了一个高层次的接口,将一个子系统的外部与内…...

搭建接口自动化测试框架python+requests+pytest
安装python(最好是比较新比较稳定的版本),然后是python的解释器或者叫编译器pycharm安装后新建一个项目,以此项目为基础,安装依赖搭建框架。打开pycharm,点击左上角的File->New project->弹出如下界面…...
一套rk3588 rtsp服务器推流的 github 方案及记录 -02
整体方案参考上一篇博文 https://blog.csdn.net/qq_31764341/article/details/134810566 本篇博文主要介绍基于RK3588进行硬解码 还是之前的套路,我不生产代码,我只是代码的搬运工,今天我们搬运瑞芯微的官方代码,并记录下来整个调…...
docker运行java程序的Dockerfile
1,docker运行java程序的Dockerfile # 使用基础镜像 FROM alpine:latest # 暴露容器的端口 不会自动将容器的端口映射到宿主机上 docker run -d -p <宿主机端口>:7080 <镜像名称> EXPOSE 9202 EXPOSE 19202 #下载jdk8 RUN apk update && apk a…...

docker数据卷数据卷容器
前言 今天调休在家,随便玩玩,简单做下学习记录 1. 数据卷特点 数据卷在容器启动时初始化,如果容器使用的镜像在挂载点包含了数据,这些数据会被拷贝到新初始化的数据卷中数据卷可以在容器之间共享和重用可以对数据卷里的内容直接…...

使用HTTP协议有哪些风险?HTTP与HTTPS的区别是什么
作为两种常见的网络协议,HTTP和HTTPS都是用于在浏览器和服务器之间传输数据的。然而在保障数据安全性方面,HTTPS远远优于HTTP。在网络安全愈发重要的当下,HTTP协议的不安全性使得其逐渐被淘汰弃用。那么使用HTTP协议有哪些风险呢?…...

【jvm从入门到实战】(十) 实战篇-内存调优
内存溢出和内存泄漏:在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。内存泄漏绝大多数情况都是由堆内存泄漏引起的。少量的内存泄漏可以容忍&#x…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

盘古信息PCB行业解决方案:以全域场景重构,激活智造新未来
一、破局:PCB行业的时代之问 在数字经济蓬勃发展的浪潮中,PCB(印制电路板)作为 “电子产品之母”,其重要性愈发凸显。随着 5G、人工智能等新兴技术的加速渗透,PCB行业面临着前所未有的挑战与机遇。产品迭代…...

学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...

佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析 一、第一轮提问(基础概念问题) 1. 请解释Spring框架的核心容器是什么?它在Spring中起到什么作用? Spring框架的核心容器是IoC容器&#…...
命令行关闭Windows防火墙
命令行关闭Windows防火墙 引言一、防火墙:被低估的"智能安检员"二、优先尝试!90%问题无需关闭防火墙方案1:程序白名单(解决软件误拦截)方案2:开放特定端口(解决网游/开发端口不通)三、命令行极速关闭方案方法一:PowerShell(推荐Win10/11)方法二:CMD命令…...

小智AI+MCP
什么是小智AI和MCP 如果还不清楚的先看往期文章 手搓小智AI聊天机器人 MCP 深度解析:AI 的USB接口 如何使用小智MCP 1.刷支持mcp的小智固件 2.下载官方MCP的示例代码 Github:https://github.com/78/mcp-calculator 安这个步骤执行 其中MCP_ENDPOI…...
FOPLP vs CoWoS
以下是 FOPLP(Fan-out panel-level packaging 扇出型面板级封装)与 CoWoS(Chip on Wafer on Substrate)两种先进封装技术的详细对比分析,涵盖技术原理、性能、成本、应用场景及市场趋势等维度: 一、技术原…...
学习 Hooks【Plan - June - Week 2】
一、React API React 提供了丰富的核心 API,用于创建组件、管理状态、处理副作用、优化性能等。本文档总结 React 常用的 API 方法和组件。 1. React 核心 API React.createElement(type, props, …children) 用于创建 React 元素,JSX 会被编译成该函数…...