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

打印 上一主题 下一主题

主题 549|帖子 549|积分 1647

写在前面

迩来在写一个web项目,必要实现web客户端之间的语音通话,期望能够借助webSocket全双工通讯的方式来实现,但是网上没有发现可以正确利用的代码。网上能找到的一个代码利用之后只能听到“嘀嘀嘀”的杂音
办理方案:利用Json来通报数据代替原有的二进制输入输出流
技能栈:VUE3、SpingBoot、WebSocket
Java后端代码

pom.xml
配置Maven所需的jar包
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-websocket</artifactId>
  4. </dependency>
复制代码
WebSocketConfig.java
webSocket配置类
  1. package com.shu.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
  5. @Configuration
  6. public class WebSocketConfig {
  7.     /**
  8.      *         注入ServerEndpointExporter,
  9.      *         这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
  10.      */
  11.     @Bean
  12.     public ServerEndpointExporter serverEndpointExporter() {
  13.         return new ServerEndpointExporter();
  14.     }
  15.    
  16. }
复制代码
WebSocketAudioServer.java
webSocket实现类,其中roomId是语音谈天室的id,userId是发送语音的用户id
以是前端哀求加入webSocket时间的哀求样例应该是:ws://localhost:8080/audio/1/123这个哀求中1是roomId,123是userId,这里建议利用ws,一般来说ws对于http,wss对应https
  1. package com.shu.socket;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.stereotype.Component;
  4. import jakarta.websocket.OnClose;
  5. import jakarta.websocket.OnError;
  6. import jakarta.websocket.OnMessage;
  7. import jakarta.websocket.OnOpen;
  8. import jakarta.websocket.Session;
  9. import jakarta.websocket.server.PathParam;
  10. import jakarta.websocket.server.ServerEndpoint;
  11. import java.io.BufferedInputStream;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.nio.ByteBuffer;
  15. import java.util.ArrayList;
  16. import java.util.HashMap;
  17. import java.util.List;
  18. import java.util.Map;
  19. import java.util.concurrent.ConcurrentHashMap;
  20. import java.util.concurrent.CopyOnWriteArraySet;
  21. /**
  22. * @Author:Long
  23. **/
  24. @Component
  25. @Slf4j
  26. @ServerEndpoint(value = "/audio/{roomId}/{userId}")
  27. public class WebSocketAudioServer {
  28.         private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();
  29.         private static CopyOnWriteArraySet<WebSocketAudioServer> webSocketSet = new CopyOnWriteArraySet<>();
  30.         private Session webSocketsession;
  31.         private String roomId;
  32.         private String userId;
  33.         @OnOpen
  34.         public void onOpen(@PathParam(value = "roomId") String roomId, @PathParam(value = "userId") String userId,
  35.                         Session webSocketsession) {
  36.                 // 接收到发送消息的人员编号
  37.                 this.roomId = roomId;
  38.                 this.userId = userId;
  39.                 // 加入map中,绑定当前用户和socket
  40.                 sessionPool.put(userId, webSocketsession);
  41.                 webSocketSet.add(this);
  42.                 this.webSocketsession = webSocketsession;
  43.                 // 在线数加1
  44.                 addOnlineCount();
  45.                 System.out.println("user编号:" + userId + ":加入Room:" + roomId + "语音聊天  " + "总数为:" + webSocketSet.size());
  46.         }
  47.         @OnClose
  48.         public void onClose() {
  49.                 try {
  50.                         sessionPool.remove(this.userId);
  51.                 } catch (Exception e) {
  52.                 }
  53.         }
  54.        
  55.         @OnMessage(maxMessageSize = 5242880)
  56.         public void onMessage(@PathParam(value = "roomId") String roomId, @PathParam(value = "userId") String userId,
  57.                         String inputStream) {
  58.                 try {
  59.                         for (WebSocketAudioServer webSocket : webSocketSet) {
  60.                                 try {
  61.                                         if (webSocket.webSocketsession.isOpen() && webSocket.roomId.equals(roomId)
  62.                                                         && !webSocket.userId.equals(userId)) {
  63.                                                 webSocket.webSocketsession.getBasicRemote().sendText(inputStream);
  64.                                         }
  65.                                 } catch (Exception e) {
  66.                                         e.printStackTrace();
  67.                                 }
  68.                         }
  69.                 } catch (Exception e) {
  70.                         e.printStackTrace();
  71.                 }
  72.         }
  73.         @OnError
  74.         public void onError(Session session, Throwable error) {
  75.                 error.printStackTrace();
  76.         }
  77.         /**
  78.          * 为指定用户发送消息
  79.          *
  80.          */
  81.         public void sendMessage(String message) throws IOException {
  82.                 // 加同步锁,解决多线程下发送消息异常关闭
  83.                 synchronized (this.webSocketsession) {
  84.                         this.webSocketsession.getBasicRemote().sendText(message);
  85.                 }
  86.         }
  87.         public List<String> getOnlineUser(String roomId) {
  88.                 List<String> userList = new ArrayList<String>();
  89.                 for (WebSocketAudioServer webSocketAudioServer : webSocketSet) {
  90.                         try {
  91.                                 if (webSocketAudioServer.webSocketsession.isOpen() && webSocketAudioServer.roomId.equals(roomId)) {
  92.                                         if (!userList.contains(webSocketAudioServer.userId)) {
  93.                                                 userList.add(webSocketAudioServer.userId);
  94.                                         }
  95.                                 }
  96.                         } catch (Exception e) {
  97.                                 e.printStackTrace();
  98.                         }
  99.                 }
  100.                 return userList;
  101.         }
  102. }
复制代码
VUE前端代码

audioChat.vue
这段代码是博主从自己的vue代码中截取出来的(原本的代码太多了),可能有些部分代码有函数没写上(如果有错的话贫苦大家在评论区指出,博主会及时修改
注意事项
之前有博客利用二进制数据输入输出流来向后端传输数据,但是功能无法实现,厥后发现那位博主的数据并没有发成功,我直接在Java中利用Json来传输float数组数据,实现了语音通话功能。
  1. <template>
  2.   <div class="play-audio">
  3.     <button @click="startCall" ref="start">开始对讲</el-button>
  4.     <button @click="stopCall" ref="stop">结束对讲</el-button>
  5.   </div>
  6. </template>
  7. <script setup>
  8. // 语音聊天的变量
  9. const audioSocket = ref(null);
  10. let mediaStack;
  11. let audioCtx;
  12. let scriptNode;
  13. let source;
  14. let play;
  15. // 语音socket
  16. const connectAudioWebSocket = () => {
  17.   let url = "ws://localhost:8080/audio/1/123"; //roomId:1 ,userId123
  18.   audioSocket.value = new WebSocket(url); // 替换为实际的 WebSocket 地址
  19.   audioSocket.value.onopen = () => {
  20.     console.log("audioSocket connected");
  21.   };
  22.   audioSocket.value.onmessage = (event) => {
  23.     // 将接收的数据转换成与传输过来的数据相同的Float32Array
  24.     const jsonAudio = JSON.parse(event.data);
  25.     // let buffer = new Float32Array(event.data);
  26.     let buffer = new Float32Array(4096);
  27.     for (let i = 0; i < 4096; i++) {
  28.       // buffer.push(parseFloat(jsonAudio[i]));
  29.       buffer[i] = parseFloat(jsonAudio[i]);
  30.     }
  31.     // 创建一个空白的AudioBuffer对象,这里的4096跟发送方保持一致,48000是采样率
  32.     const myArrayBuffer = audioCtx.createBuffer(1, 4096, 16000);
  33.     // 也是由于只创建了一个音轨,可以直接取到0
  34.     const nowBuffering = myArrayBuffer.getChannelData(0);
  35.     // 通过循环,将接收过来的数据赋值给简单音频对象
  36.     for (let i = 0; i < 4096; i++) {
  37.       nowBuffering[i] = buffer[i];
  38.     }
  39.     // 使用AudioBufferSourceNode播放音频
  40.     const source = audioCtx.createBufferSource();
  41.     source.buffer = myArrayBuffer;
  42.     const gainNode = audioCtx.createGain();
  43.     source.connect(gainNode);
  44.     gainNode.connect(audioCtx.destination);
  45.     var muteValue = 1;
  46.     if (!play) {
  47.       // 是否静音
  48.       muteValue = 0;
  49.     }
  50.     gainNode.gain.setValueAtTime(muteValue, audioCtx.currentTime);
  51.     source.start();
  52.   };
  53.   audioSocket.value.onclose = () => {
  54.     console.log("audioSocket closed");
  55.   };
  56.   audioSocket.value.onerror = (error) => {
  57.     console.error("audioSocket error:", error);
  58.   };
  59. };
  60. // 开始对讲
  61. function startCall() {
  62.     isInChannel.value = true;
  63.     play = true;
  64.     audioCtx = new AudioContext();
  65.     connectAudioWebSocket();
  66.     // 该变量存储当前MediaStreamAudioSourceNode的引用
  67.     // 可以通过它关闭麦克风停止音频传输
  68.     // 创建一个ScriptProcessorNode 用于接收当前麦克风的音频
  69.     scriptNode = audioCtx.createScriptProcessor(4096, 1, 1);
  70.     navigator.mediaDevices
  71.       .getUserMedia({ audio: true, video: false })
  72.       .then((stream) => {
  73.         mediaStack = stream;
  74.         source = audioCtx.createMediaStreamSource(stream);
  75.         source.connect(scriptNode);
  76.         scriptNode.connect(audioCtx.destination);
  77.       })
  78.       .catch(function (err) {
  79.         /* 处理error */
  80.         isInChannel.value = false;
  81.         console.log("err", err);
  82.       });
  83.     // 当麦克风有声音输入时,会调用此事件
  84.     // 实际上麦克风始终处于打开状态时,即使不说话,此事件也在一直调用
  85.     scriptNode.onaudioprocess = (audioProcessingEvent) => {
  86.       const inputBuffer = audioProcessingEvent.inputBuffer;
  87.       // console.log("inputBuffer",inputBuffer);
  88.       // 由于只创建了一个音轨,这里只取第一个频道的数据
  89.       const inputData = inputBuffer.getChannelData(0);
  90.       // 通过socket传输数据,实际上传输的是Float32Array
  91.       if (audioSocket.value.readyState === 1) {
  92.         // console.log("发送的数据",inputData);
  93.         // audioSocket.value.send(inputData);
  94.         let jsonData = JSON.stringify(inputData);
  95.         audioSocket.value.send(jsonData);
  96.         // stopCall();
  97.       }
  98.     };
  99. }
  100. // 关闭麦克风
  101. function stopCall() {
  102.   isInChannel.value = false;
  103.   play = false;
  104.   mediaStack.getTracks()[0].stop();
  105.   scriptNode.disconnect();
  106.   if (audioSocket.value) {
  107.     audioSocket.value.close();
  108.     audioSocket.value = null;
  109.   }
  110. }
  111. </script>
复制代码
关于Chrome或Edge浏览器报错

关于谷歌浏览器提示TypeError: Cannot read property ‘getUserMedia’ of undefined
办理方案:
1.网页利用https访问,服务端升级为https访问,配置ssl证书
2.利用localhost或127.0.0.1 进行访问
3.修改浏览器安全配置
在chrome浏览器中输入如下指令
  1. chrome://flags/#unsafely-treat-insecure-origin-as-secure
复制代码
开启 Insecure origins treated as secure
在下方输入栏内输入你访问的地点url,然后将右侧Disabled 改成 Enabled即可

浏览器会提示重启, 点击Relaunch即可


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

美食家大橙子

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表