写在前面
最近在写一个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是语音聊天室的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<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即可