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

前端 0

写在前面

最近在写一个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; @Configurationpublic 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;// 语音socketconst 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即可
在这里插入图片描述

也许您对下面的内容还感兴趣: