web网页端使用webSocket实现语音通话功能(SpringBoot+VUE)

写在前面

最近在写一个web项目,需要实现web客户端之间的语音通话,期望能够借助webSocket全双工通信的方式来实现,但是网上没有发现可以正确使用的代码。网上能找到的一个代码使用之后只能听到"嘀嘀嘀"的杂音

解决方案:使用Json来传递数据代替原有的二进制输入输出流

技术栈:VUE3、SpingBoot、WebSocket

Java后端代码

pom.xml

配置Maven所需的jar包

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocketConfig.java

webSocket配置类

java 复制代码
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

java 复制代码
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数组数据,实现了语音通话功能

vue 复制代码
<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即可

相关推荐
伍哥的传说2 分钟前
鸿蒙系统(HarmonyOS)应用开发之手势锁屏密码锁(PatternLock)
前端·华为·前端框架·harmonyos·鸿蒙
yugi9878384 分钟前
前端跨域问题解决Access to XMLHttpRequest at xxx from has been blocked by CORS policy
前端
幽络源小助理8 分钟前
SpringBoot基于JavaWeb的城乡居民基本医疗信息管理系统
java·spring boot·学习
浪裡遊15 分钟前
Sass详解:功能特性、常用方法与最佳实践
开发语言·前端·javascript·css·vue.js·rust·sass
旧曲重听11 小时前
最快实现的前端灰度方案
前端·程序人生·状态模式
默默coding的程序猿1 小时前
3.前端和后端参数不一致,后端接不到数据的解决方案
java·前端·spring·ssm·springboot·idea·springcloud
夏梦春蝉1 小时前
ES6从入门到精通:常用知识点
前端·javascript·es6
归于尽1 小时前
useEffect玩转React Hooks生命周期
前端·react.js
G等你下课1 小时前
React useEffect 详解与运用
前端·react.js
我想说一句1 小时前
当饼干遇上代码:一场HTTP与Cookie的奇幻漂流 🍪🌊
前端·javascript