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是语音聊天室的iduserId是发送语音的用户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即可
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/239848.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

中级软件设计师-note-3

又一个逆向思维的例子是“有两个年轻人&#xff0c;追同一个女孩子。第一个年轻人用了4000块&#xff1a;花3500块给女孩子买了一个手机&#xff0c;剩下的500块准备用作吃饭和玩的&#xff0c;然后他骑着共享单车去找女孩子&#xff0c;女孩子直接就给拒绝了&#xff0c;说&am…

算法练习Day19 (Leetcode/Python-二叉树)

108. Convert Sorted Array to Binary Search Tree Given an integer array nums where the elements are sorted in ascending order, convert it to a height-balanced binary search tree. 思路&#xff1a; 一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的…

RSA 加密方案

RSA 算法 RSA 加密和签名&#xff1a;因大整数因子分解难算&#xff0c;合数可成公钥。 d - 私钥&#xff0c;e - 公钥&#xff0c;n - 可公开的合数&#xff0c;(e,n) 作为公钥可以公开&#xff0c;(d,n) 作为私钥。 详细理论证明参考&#xff1a;RSA算法原理&#xff08;二…

3.[BUU]warmup_csaw_20161

1.checksec 检查文件类型 ELF-64-little &#xff0c;无其他限权&#xff0c;直接用ida检查代码。 2.IDA进行反编译&#xff0c;进行代码审计 查看各个名称的内容&#xff1a; 了解基本攻击思路&#xff1a; 攻击思路&#xff1a;gets输入垃圾数据覆盖v5内容&#xff0c;再将s…

51单片机的羽毛球计分器系统【含proteus仿真+程序+报告+原理图】

1、主要功能 该系统由AT89C51单片机LCD1602显示模块按键等模块构成。适用于羽毛球计分、乒乓球计分、篮球计分等相似项目。 可实现基本功能: 1、LCD1602液晶屏实时显示比赛信息 2、按键控制比赛的开始、暂停和结束&#xff0c;以及两位选手分数的加减。 本项目同时包含器件清…

Ubuntu 常用命令之 fdisk 命令用法介绍

&#x1f4d1;Linux/Ubuntu 常用命令归类整理 fdisk 是一个用于处理磁盘分区的命令行工具&#xff0c;它在 Linux 系统中广泛使用。fdisk 命令可以创建、删除、更改、复制和显示硬盘分区&#xff0c;以及更改硬盘的分区 ID。 fdisk 命令的常用参数如下 -l&#xff1a;列出所…

【基于激光雷达的路沿检测用于自动驾驶的真值标注】

文章目录 概要主要贡献内容概述实验小结 概要 论文地址&#xff1a;https://arxiv.org/pdf/2312.00534.pdf 路沿检测在自动驾驶中扮演着重要的角色&#xff0c;因为它能够帮助车辆感知道可行驶区域和不可行驶区域。为了开发和验证自动驾驶功能&#xff0c;标注的数据是必不可…

Python电能质量扰动信号分类(二)基于CNN模型的一维信号分类

目录 前言 1 电能质量数据集制作与加载 1.1 导入数据 1.2 制作数据集 2 CNN-2D分类模型和训练、评估 2.1 定义CNN-2d分类模型 2.2 定义模型参数 2.3 模型结构 2.4 模型训练 2.5 模型评估 3 CNN-1D分类模型和训练、评估 3.1 定义CNN-1d分类模型 3.2 定义模型参数 …

【ONE·MySQL || 基础介绍】

总言 主要内容&#xff1a;MySQL在Centos 7下的安装&#xff08;主要学习相关指令语句&#xff0c;理解安装操作是在做什么&#xff09;、对MySQL数据库有一个基础认识。 文章目录 总言0、MySQL的安装与卸载&#xff08;Centos 7&#xff09;0.1、MySQL的卸载0.1.1、卸载不必要…

HarmonyOS4.0系统性深入开发01应用模型的构成要素

应用模型的构成要素 应用模型是HarmonyOS为开发者提供的应用程序所需能力的抽象提炼&#xff0c;它提供了应用程序必备的组件和运行机制。有了应用模型&#xff0c;开发者可以基于一套统一的模型进行应用开发&#xff0c;使应用开发更简单、高效。 HarmonyOS应用模型的构成要…

【Amazon 实验①】使用 Amazon CloudFront加速Web内容分发

文章目录 实验架构图1. 准备实验环境2. 创建CloudFront分配、配置动、静态资源分发2.1 创建CloudFront分配&#xff0c;添加S3作为静态资源源站2.2 为CloudFront分配添加动态源站 在本实验——使用CloudFront进行全站加速中&#xff0c;将了解与学习Amazon CloudFront服务&…

CSS(五) -- 动效实现(立体盒子旋转-四方体+正六边)

一. 四面立体旋转 正方形旋转 小程序中 wxss中 <!-- 背景 --><view class"dragon"><!--旋转物体位置--><view class"dragon-position"><!--旋转 加透视 有立体的感觉--><view class"d-parent"><view …

linux分辨率添加

手动添加分辨率 注&#xff1a;添加分辨率需要显卡驱动支持&#xff0c;若显卡驱动有问题&#xff0c;则不能添加 可通过 xrandr 结果判断 # xrandr 若图中第二行” eDP“ 显示为 ” default “ &#xff0c;则显卡驱动加载失败&#xff0c;不能添加分辨率 1. 添加分辨率 # …

hiveserver负载均衡配置

一.安装nginx 参数我的另一篇文章&#xff1a;https://mp.csdn.net/mp_blog/creation/editor/135152478 二.配置nginx服务参数 worker_processes 1; events { worker_connections 1024; } stream { upstream hiveserver2 { # least_conn; # 使用最少连接路由…

助力智能人群检测计数,基于DETR(DEtectionTRansformer)开发构建通用场景下人群检测计数识别系统

在一些人流量比较大的场合&#xff0c;或者是一些特殊时刻、时段、节假日等特殊时期下&#xff0c;密切关注当前系统所承载的人流量是十分必要的&#xff0c;对于超出系统负荷容量的情况做到及时预警对于管理团队来说是保障人员安全的重要手段&#xff0c;本文的主要目的是想要…

SolidKits.BOMs工具—BOM及焊件切割清单输出

SolidKits.BOMs工具—BOM及焊件切割清单输出包含自动出BOM&#xff0c;自定义模板&#xff0c;焊件切割清单的输出&#xff0c;虚拟件的输出等功能&#xff0c;使用该功能&#xff0c;无需打开SOLIDWORKS软件&#xff0c;可大大提高工作效率。为回馈新老客户&#xff0c;此工具…

Ubuntu 常用命令之 exit 命令用法介绍

&#x1f4d1;Linux/Ubuntu 常用命令归类整理 exit命令在Ubuntu系统下用于结束一个终端会话。它可以用于退出当前的shell&#xff0c;结束当前的脚本执行&#xff0c;或者结束一个ssh会话。 exit命令的参数是一个可选的整数&#xff0c;用于指定退出状态。如果没有指定&#…

基于ip地址通过openssl生成自签名证书

最近在配置geo的时候&#xff0c;客户说自己使用的是自签证书&#xff0c;然后是通过ip地址和端口的方式访问gitlab&#xff0c;比较好奇这块&#xff0c;因此对证书的生成和使用做了一些整理&#xff0c;对此网上关于这部分资料也很多&#xff0c;不过作为记录&#xff0c;也算…

美国的目的暴露了,夺下6台EUV光刻机推进2纳米,反超台积电

日前消息指光刻机巨头ASML预计明年量产10台第二代EUV光刻机&#xff0c;其中6台已确定被美国芯片企业Intel夺下&#xff0c;剩下的两台将由三星和台积电竞争&#xff0c;如此一来拿到更多第二代EUV光刻机的美国可望率先量产2纳米&#xff0c;反超台积电。 一、第二代EUV光刻机的…

Pytorch项目,肺癌检测项目之二

diameter_dict{} with open(/xunlian/annotations.csv &#xff0c;‘r’) as f: for row in list(csv.reader(f)[1:]): series_uid row[0] annotationCenter_xyz tuple([float(x) for x in row[1:4]]) annotationDiameter_mm float(row[4]) diameter_dict.setdefault(seri…