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
     */
    @Bean
    public 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为与某个客户端的连接会话,需要通过它来给客户端发送数据
	 */
	@OnOpen
	public void onOpen(@PathParam(value = "roomId") String roomId, @PathParam(value = "userId") String userId,
			Session webSocketsession) {
		// 接收到发送消息的人员编号
		this.roomId = roomId;
		this.userId = userId;
		// 加入map中,绑定当前用户和socket
		sessionPool.put(userId, webSocketsession);
		webSocketSet.add(this);
		this.webSocketsession = webSocketsession;
		// 在线数加1
		addOnlineCount();
		System.out.println("user编号:" + userId + ":加入Room:" + roomId + "语音聊天  " + "总数为:" + webSocketSet.size());
	}

	/**
	 * 连接关闭调用的方法
	 */
	@OnClose
	public 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
	 */
	@OnError
	public 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 = () => {
  //获取token
  const token = window.sessionStorage.getItem("token");
  if (!token) {
    return;
  }
  let url = "ws://localhost:8080/audio/1/123"; //roomId:1 ,userId123
  audioSocket.value = new WebSocket(url); // 替换为实际的 WebSocket 地址

  audioSocket.value.onopen = () => {
    console.log("audioSocket connected");
  };

  audioSocket.value.onmessage = (event) => {
    // 将接收的数据转换成与传输过来的数据相同的Float32Array
    const 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);
    // 也是由于只创建了一个音轨,可以直接取到0
    const 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传输数据,实际上传输的是Float32Array
      if (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即可
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue之router-link页面跳转及传参

文章目录 vue之router-link页面跳转及传参根据路由的路径跳转及传参(query)根据路由的名称跳转及传参(params)根据store传参 vue之router-link页面跳转及传参 <router-link> 是用于在 Vue.js 应用程序中进行路由导航的组件。它可以用来生成具有正确链接的<a> 标签…

我的应用我做主:扩展线程池

自定义线程创建&#xff1a;ThreadFactory 线程池中的线程是从哪里来的呢&#xff1f; ThreadPoolExecutor(int corePoolSize,//指定了线程池种的线程数量 int maximumPoolSize,//指定了线程池中的最大线程数量。 long keepAliveTime,// 当线程池数量超过了corePoolSize&#x…

【电路笔记】-串联电容器

串联电容器 文章目录 串联电容器1、概述2、示例13、示例34、总结 当电容器以菊花链方式连接在一条线上时&#xff0c;它们就串联在一起。 1、概述 对于串联电容器&#xff0c;流过电容器的充电电流 ( i C i_C iC​ ) 对于所有电容器来说都是相同的&#xff0c;因为它只有一条…

在x64上构建智能家居(home assistant)(二)(新版Debain12)连接Postgresql数据库

新版数据库安装基本和旧版相同,大部分可以参考旧版本在x64上构建智能家居(home assistant)&#xff08;二&#xff09;连接Postgresql数据库_homeassist 数据库-CSDN博客 新版本的home assistant系统安装,我在原来写的手顺上直接修改了,需要的可以查看在x64上构建智能家居(home…

心有暖阳,笃定前行,2024考研加油

2024考研学子&#xff0c;所有的付出终有收获&#xff0c;阳光终将穿透阴霾&#xff0c;终将上岸。 当曙光破晓的时候&#xff0c;你可曾记得那些星月为伴&#xff0c;孤独为友&#xff0c;理想为灯来指引前行之路的日子&#xff0c;那些默默扎根的日子终将化作星星在未来闪闪发…

java.lang.IllegalStateException: Duplicate key

序言 最近监控扫描出我们项目的某些异常信息&#xff0c;报错java.lang.IllegalStateException: Duplicate key xxx&#xff0c;看到异常来自stream流&#xff0c;然后定位看了一下是某位同事的代码使用stream流把List转Map集合出现重复的key异常信息。List集合A对象来源于某个…

01-黑马程序员大数据开发

一. Hadoop概述 1. 什么是大数据 &#xfeff;狭义上&#xff1a;对海量数据进行处理的软件技术体系&#xfeff;广义上&#xff1a;数字化、信息化时代的基础支撑&#xff0c;以数据为生活赋 2. 大数据的核心工作&#xff1a; &#xfeff;存储&#xff1a;妥善保存海量待…

Http---HTTP 请求报文

1. HTTP 请求报文介绍 HTTP最常见的请求报文有两种: GET 方式的请求报文POST 方式的请求报文 说明: GET: 获取web服务器数据POST: 向web服务器提交数据 2. HTTP GET 请求报文分析 HTTP GET 请求报文效果图: GET 请求报文说明: ---- 请求行 ---- GET / HTTP/1.1 # GET请…

Qt WebAssembly开发环境配置

目录 前言1、下载Emscripten SDK2、 安装3、环境变量配置4、QtCreator配置5、运行示例程序总结 前言 本文主要介绍 Qt WebAssembly 开发环境的配置。Qt for Webassembly 可以使Qt应用程序在Web上运行。WebAssembly&#xff08;简称Wasm&#xff09;是一种能够在虚拟机中执行的…

内存管理学习

内存管理 在计算系统中&#xff0c;通常存储空间分为两种&#xff1a;内部存储空间和外部存储空间。 内部存储空间通常访问速度比较快&#xff0c;能够按照变量地址随机访问&#xff0c;也就是我们通常所说的RAM&#xff08;随机存储器&#xff09;&#xff0c;可以把它理解为…

【原理图PCB专题】原理图图纸锁定/解锁与PCB文件加密方式

在工作中我们会遇到需要冻结原理图进行评审和加密图纸防止被他人盗用的需求。那么在OrCAD Capture中如何对图纸进行锁定与解锁,如何在Allegro中对PCB工程进行加密呢? 原理图锁定与解锁 打开原理图,在图纸中单击右键,选择lock/unlock就可以进行锁定与解锁。 锁定时图纸图…

PostGIS学习教程十四:更多的空间连接

PostGIS学习教程十四&#xff1a;更多的空间连接 在上一节中&#xff0c;我们看到了ST_Centroid(geometry)和ST_Union([geometry])函数&#xff0c;以及一些简单的示例。在本节中&#xff0c;我们将用它们做一些更详细的事情。 提示&#xff1a;写完文章后&#xff0c;目录可以…

OCC:第一个程序,对话框中显示一个BOX

1. OCC库的获取 从github上获取 gitgithub.com:tpaviot/oce.git&#xff0c;自己编译官网获取二进制包&#xff08;获取下来的只有release 版本的&#xff0c;而且VS版本不一定适合自己&#xff09;官网源码&#xff0c;然后自己编译&#xff08;稍微折腾点&#xff0c;建议按…

带大家做一个,易上手的家常辣椒炒肉

先拿一块猪肉泡水解冻 然后 拿四个螺丝椒 螺丝椒切片 放入四个干辣椒 猪肉切片 三瓣左右蒜 如下图大小的一块姜 姜蒜切小块 将辣椒单独倒入锅中 翻炒出辣味 闻到辣味后将辣椒捞出 这里千万不要洗锅不然就把辣味洗掉了 直接起锅烧油 下入肉片翻炒 猪肉变色后 下入姜蒜…

UG阵列面、阵列集合特征和阵列特征的区别

阵列面 对面进行阵列&#xff0c;当实体中被切除特征的时候可以使用阵列面&#xff0c;当这个命令去阵列一个实体的时候&#xff0c;阵列的是一个片体&#xff0c;优点是速度快&#xff0c;缺点是功能较简单&#xff1b; 阵列几何特征 对实体进行阵列&#xff0c;可以一次性选…

Linux 一键部署二进制Gitea

gitea 前言 Gitea 是一个轻量级的 DevOps 平台软件。从开发计划到产品成型的整个软件生命周期,他都能够高效而轻松的帮助团队和开发者。包括 Git 托管、代码审查、团队协作、软件包注册和 CI/CD。它与 GitHub、Bitbucket 和 GitLab 等比较类似。 Gitea 最初是从 Gogs 分支而来…

数据结构 | 东北大学厦门大学期末试卷查漏补缺

Prim变型算法&#xff08;不会&#xff09; 有人给出求解最小生成树的另外一种算法&#xff1a;将连通图中的边按其权值从大到小顺序逐个删除直至不可再删&#xff0c;删除要遵循的原则是&#xff1a;保证在删除该边后各个顶点之间应该是连通的。请问该算法是正确的吗&#xf…

ElasticSearch 的基础概念与入门使用

ElasticSearch 的基础概念与入门使用 前言 elasticsearch 是一款非常强大的开源搜索引擎&#xff0c;具备非常多强大的功能&#xff0c;可以帮助我们从海量的数据中快速找到需要的内容。 例如&#xff1a; 在 Github 中搜索代码 在电商网站搜索商品 在 Google 搜索答案 …

过采样技术基本原理

本文介绍过采样技术基本原理。 过采样技术在ADC信号采集过程中使用还是比较多的。某些使用场景下&#xff0c;对采样速度要求并不是那么高&#xff08;或ADC采样速度过剩&#xff09;&#xff0c;但是想要获取较高的分辨率&#xff0c;就会用到这种技术&#xff0c;如针对温度…

设计模式:循序渐进走入工厂模式

文章目录 前言一、引入二、简单工厂模式1.实现2.优缺点3.扩展 三、工厂方法模式1.实现2.优缺点 四、抽象工厂模式1.实现2.优缺点3.使用场景 五、模式扩展六、JDK源码解析总结 前言 软件设计模式之工厂模式。 一、引入 需求&#xff1a;设计一个咖啡店点餐系统。 设计一个咖啡类…