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 ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();
	private static CopyOnWriteArraySet<WebSocketAudioServer> webSocketSet = new CopyOnWriteArraySet<>();
	private Session webSocketsession;
	private String roomId;
	private String userId;

	@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();
		}
	}


	@OnError
	public void onError(Session session, Throwable error) {
		error.printStackTrace();
	}

	/**
	 * 为指定用户发送消息
	 *
	 */
	public void sendMessage(String message) throws IOException {
		// 加同步锁,解决多线程下发送消息异常关闭
		synchronized (this.webSocketsession) {
			this.webSocketsession.getBasicRemote().sendText(message);
		}
	}

	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 = () => {
  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即可

相关推荐
!!!5254 小时前
日志技术-LogBack入门程序&Log配置文件&日志级别
spring boot
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
浪浪山小白兔6 小时前
HTML5 新表单属性详解
前端·html·html5
feilieren7 小时前
SpringBoot 搭建 SSE
java·spring boot·spring
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me7 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架