写在前面
最近在写一个web项目,需要实现web客户端之间的语音通话,期望能够借助webSocket全双工通信的方式来实现,但是网上没有发现可以正确使用的代码。网上能找到的一个代码使用之后只能听到“嘀嘀嘀”的杂音
技术栈:VUE3、SpingBoot、WebSocket
Java后端代码
pom.xml
配置Maven所需的jar包
org.springframework.boot spring-boot-starter-websocket
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是语音聊天室的id,userId是发送语音的用户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 sessionPool = new ConcurrentHashMap(); private static CopyOnWriteArraySet 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 getOnlineUser(String roomId) { List userList = new ArrayList(); 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数组数据,实现了语音通话功能。
开始对讲 结束对讲 // 语音聊天的变量 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 { 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; } }
关于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即可