java版本使用springboot vue websocket webrtc实现视频通话

打印 上一主题 下一主题

主题 345|帖子 345|积分 1035

原理简单表明

​ 浏览器提供获取屏幕、音频等媒体数据的接口,
​ 双方的媒体流数据通过Turn服务器传输
websocket传递信令服务
使用技能


  • java jdk17
  • springboot 3.2.2
  • websocket
  • 前端使用 vue
搭建websocket环境依赖

  1.         <dependencies>
  2.         <dependency>
  3.             <groupId>org.springframework.boot</groupId>
  4.             <artifactId>spring-boot-starter-web</artifactId>
  5.         </dependency>
  6.         <dependency>
  7.             <groupId>org.springframework.boot</groupId>
  8.             <artifactId>spring-boot-starter-websocket</artifactId>
  9.         </dependency>
  10.         <dependency>
  11.             <groupId>org.springframework.boot</groupId>
  12.             <artifactId>spring-boot-starter-test</artifactId>
  13.             <scope>test</scope>
  14.         </dependency>
  15.     </dependencies>
复制代码
websocket的配置类
  1. package com.example.webrtc.config;
  2. import com.example.webrtc.Interceptor.AuthHandshakeInterceptor;
  3. import com.example.webrtc.Interceptor.MyChannelInterceptor;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.messaging.converter.MessageConverter;
  10. import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
  11. import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
  12. import org.springframework.messaging.simp.config.ChannelRegistration;
  13. import org.springframework.messaging.simp.config.MessageBrokerRegistry;
  14. import org.springframework.web.socket.config.annotation.*;
  15. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
  16. import java.util.List;
  17. @Configuration
  18. @EnableWebSocketMessageBroker
  19. public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport implements WebSocketMessageBrokerConfigurer {
  20.     private static final Logger log = LoggerFactory.getLogger(WebSocketConfig.class);
  21.     @Autowired
  22.     private AuthHandshakeInterceptor authHandshakeInterceptor;
  23.     @Autowired
  24.     private MyChannelInterceptor myChannelInterceptor;
  25.     @Bean
  26.     public ServerEndpointExporter serverEndpointExporter(){
  27.         return new ServerEndpointExporter();
  28.     }
  29.     @Override
  30.     public void registerStompEndpoints(StompEndpointRegistry registry) {
  31.         registry.addEndpoint("/chat-websocket")
  32.                 .setAllowedOriginPatterns("*")
  33.                 .addInterceptors(authHandshakeInterceptor)
  34.                 .setAllowedOriginPatterns("*")
  35.              //   .setHandshakeHandler(myHandshakeHandler)
  36.                 .withSockJS();
  37.     }
  38.     @Override
  39.     public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
  40.             registry.setMessageSizeLimit(Integer.MAX_VALUE);
  41.             registry.setSendBufferSizeLimit(Integer.MAX_VALUE);
  42.             super.configureWebSocketTransport(registry);
  43.     }
  44.     @Override
  45.     public void configureMessageBroker(MessageBrokerRegistry registry) {
  46.         //客户端需要把消息发送到/message/xxx地址
  47.         registry.setApplicationDestinationPrefixes("/webSocket");
  48.         //服务端广播消息的路径前缀,客户端需要相应订阅/topic/yyy这个地址的消息
  49.         registry.enableSimpleBroker("/topic", "/user");
  50.         //给指定用户发送消息的路径前缀,默认值是/user/
  51.         registry.setUserDestinationPrefix("/user/");
  52.     }
  53.     @Override
  54.     public void configureClientInboundChannel(ChannelRegistration registration) {
  55.         registration.interceptors(myChannelInterceptor);
  56.     }
  57.     @Override
  58.     public void configureClientOutboundChannel(ChannelRegistration registration) {
  59.         WebSocketMessageBrokerConfigurer.super.configureClientOutboundChannel(registration);
  60.     }
  61.     @Override
  62.     public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
  63.         WebSocketMessageBrokerConfigurer.super.addArgumentResolvers(argumentResolvers);
  64.     }
  65.     @Override
  66.     public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
  67.         WebSocketMessageBrokerConfigurer.super.addReturnValueHandlers(returnValueHandlers);
  68.     }
  69.     @Override
  70.     public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
  71.         return WebSocketMessageBrokerConfigurer.super.configureMessageConverters(messageConverters);
  72.     }
  73. }
复制代码
控制层 WebSocketController
  1. package com.example.webrtc.controller;
  2. import com.example.webrtc.config.Message;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.messaging.handler.annotation.MessageMapping;
  5. import org.springframework.messaging.simp.SimpMessagingTemplate;
  6. import org.springframework.web.bind.annotation.RequestMapping;
  7. import org.springframework.web.bind.annotation.RestController;
  8. import java.security.Principal;
  9. import java.util.HashMap;
  10. import java.util.Map;
  11. import java.util.concurrent.atomic.AtomicInteger;
  12. // 私信聊天的控制器
  13. @RestController
  14. public class WebSocketController {
  15.     @Autowired
  16.     private SimpMessagingTemplate messagingTemplate;
  17.     private AtomicInteger i=new AtomicInteger(1);
  18.     @RequestMapping("/user")
  19.     public String findUser(){
  20.         return "00"+i.decrementAndGet();
  21.     }
  22.     @MessageMapping("/api/chat")
  23.     //在springmvc 中可以直接获得principal,principal 中包含当前用户的信息
  24.     public void handleChat(Principal principal, Message messagePara) {
  25.         String currentUserName = principal.getName();
  26.         System.out.println(currentUserName);
  27.         try {
  28.             messagePara.setFrom(principal.getName());
  29.             System.out.println("from" + messagePara.getFrom());
  30.             messagingTemplate.convertAndSendToUser(messagePara.getTo(),
  31.                     "/queue/notifications",
  32.                     messagePara);
  33.         } catch (Exception e) {
  34.             // 打印异常
  35.             e.printStackTrace();
  36.         }
  37.     }
  38. }
复制代码
前端交互拨号index.vue
  1. <template>
  2.   <div class="play-audio">
  3.     <h2 style="text-align: center;">播放页面</h2>
  4.     <div class="main-box">
  5.       <video ref="localVideo" class="video" autoplay="autoplay"></video>
  6.       <video ref="remoteVideo" class="video" height="500px" autoplay="autoplay"></video>
  7.     </div>
  8.     <div style="text-align: center;">
  9.       <el-button @click="requestConnect()" ref="callBtn">开始对讲</el-button>
  10.       <el-button @click="hangupHandle()" ref="hangupBtn">结束对讲</el-button>
  11.     </div>
  12.     <div style="text-align: center;">
  13.       <label for="name">发送人:</label>
  14.       <input type="text" id="name" readonly v-model="userId" class="form-control"/>
  15.     </div>
  16.     <div style="text-align: center;">
  17.       <label for="name">接收人:</label>
  18.       <input type="text" id="name" v-model="toUserId" class="form-control"/>
  19.     </div>
  20.   </div>
  21. </template>
  22. <el-dialog :title="'提示'" :visible.sync="dialogVisible" width="30%">
  23. <span>{{ toUserId + '请求连接!' }}</span>
  24. <span slot="footer" class="dialog-footer">
  25.     <el-button @click="handleClose">取 消</el-button>
  26.     <el-button type="primary" @click="dialogVisibleYes">确 定</el-button>
  27.   </span>
  28. </el-dialog>
  29. <script>
  30. import request from '@/utils/reeques'
  31. import Websocket from '@/utils/websocket'
  32. import Stomp from "stompjs";
  33. import SockJS from "sockjs-client";
  34. import adapter from "webrtc-adapter";
  35. import axios from 'axios'
  36. export default {
  37.   data() {
  38.     return {
  39.       stompClient: null,
  40.       userId: '001',
  41.       socket: null,
  42.       toUserId: '',
  43.       localStream: null,
  44.       remoteStream: null,
  45.       localVideo: null,
  46.       remoteVideo: null,
  47.       callBtn: null,
  48.       hangupBtn: null,
  49.       peerConnection: null,
  50.       dialogVisible: false,
  51.       msg: '',
  52.       config: {
  53.         iceServers: [
  54.           {urls: 'stun:global.stun.twilio.com:3478?transport=udp'}
  55.         ],
  56.       }
  57.     };
  58.   },
  59.   computed: {},
  60.   methods: {
  61.     handleClose() {
  62.       this.dialogVisible = false
  63.     },
  64.     dialogVisibleYes() {
  65.       var _self = this;
  66.       this.dialogVisible = false
  67.       _self.startHandle().then(() => {
  68.         _self.stompClient.send("/api/chat", _self.toUserId, {'type': 'start'})
  69.       })
  70.     },
  71.     requestConnect() {
  72.       let that = this;
  73.       if (!that.toUserId) {
  74.         alert('请输入对方id')
  75.         return false
  76.       } else if (!that.stompClient) {
  77.         alert('请先打开websocket')
  78.         return false
  79.       } else if (that.toUserId == that.userId) {
  80.         alert('自己不能和自己连接')
  81.         return false
  82.       }
  83.       //准备连接
  84.       that.startHandle().then(() => {
  85.         that.stompClient.send("/api/chat", that.toUserId, {'type': 'connect'})
  86.       })
  87.     },
  88.     startWebsocket(user) {
  89.       let that = this;
  90.       that.stompClient = new Websocket(user);
  91.       that.stompClient.connect(() => {
  92.         that.stompClient.subscribe("/user/" + that.userId + "/queue/notifications", function (result) {
  93.           that.onmessage(result)
  94.         })
  95.       })
  96.     }
  97.     ,
  98.     gotLocalMediaStream(mediaStream) {
  99.       var _self = this;
  100.       _self.localVideo.srcObject = mediaStream;
  101.       _self.localStream = mediaStream;
  102.       // _self.callBtn.disabled = false;
  103.     }
  104.     ,
  105.     createConnection() {
  106.       var _self = this;
  107.       _self.peerConnection = new RTCPeerConnection()
  108.       if (_self.localStream) {
  109.         // 视频轨道
  110.         const videoTracks = _self.localStream.getVideoTracks();
  111.         // 音频轨道
  112.         const audioTracks = _self.localStream.getAudioTracks();
  113.         // 判断视频轨道是否有值
  114.         if (videoTracks.length > 0) {
  115.           console.log(`使用的设备为: ${videoTracks[0].label}.`);
  116.         }
  117.         // 判断音频轨道是否有值
  118.         if (audioTracks.length > 0) {
  119.           console.log(`使用的设备为: ${audioTracks[0].label}.`);
  120.         }
  121.         _self.localStream.getTracks().forEach((track) => {
  122.           _self.peerConnection.addTrack(track, _self.localStream)
  123.         })
  124.       }
  125.       // 监听返回的 Candidate
  126.       _self.peerConnection.addEventListener('icecandidate', _self.handleConnection);
  127.       // 监听 ICE 状态变化
  128.       _self.peerConnection.addEventListener('iceconnectionstatechange', _self.handleConnectionChange)
  129.       //拿到流的时候调用
  130.       _self.peerConnection.addEventListener('track', _self.gotRemoteMediaStream);
  131.     }
  132.     ,
  133.     startConnection() {
  134.       var _self = this;
  135.       // _self.callBtn.disabled  = true;
  136.       // _self.hangupBtn.disabled = false;
  137.       // 发送offer
  138.       _self.peerConnection.createOffer().then(description => {
  139.         console.log(`本地创建offer返回的sdp:\n${description.sdp}`)
  140.         // 将 offer 保存到本地
  141.         _self.peerConnection.setLocalDescription(description).then(() => {
  142.           console.log('local 设置本地描述信息成功');
  143.           // 本地设置描述并将它发送给远端
  144.           // _self.socket.send(JSON.stringify({
  145.           //   'userId': _self.userId,
  146.           //   'toUserId': _self.toUserId,
  147.           //   'message': description
  148.           // }));
  149.           _self.stompClient.send("/api/chat", _self.toUserId, description)
  150.         }).catch((err) => {
  151.           console.log('local 设置本地描述信息错误', err)
  152.         });
  153.       })
  154.         .catch((err) => {
  155.           console.log('createdOffer 错误', err);
  156.         });
  157.     }
  158.     ,
  159.     async startHandle() {
  160.       this.callBtn = this.$refs.callBtn
  161.       this.hangupBtn = this.$refs.hangupBtn
  162.       this.remoteVideo = this.$refs.remoteVideo
  163.       this.localVideo = this.$refs.localVideo
  164.       var _self = this;
  165.       // 1.获取本地音视频流
  166.       // 调用 getUserMedia API 获取音视频流
  167.       let constraints = {
  168.         video: true,
  169.         audio: {
  170.           // 设置回音消除
  171.           noiseSuppression: true,
  172.           // 设置降噪
  173.           echoCancellation: true,
  174.         }
  175.       }
  176.       navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia
  177.       await navigator.mediaDevices.getUserMedia(constraints)
  178.         .then(_self.gotLocalMediaStream)
  179.         .catch((err) => {
  180.           console.log('getUserMedia 错误', err);
  181.           //创建点对点连接对象
  182.         });
  183.       _self.createConnection();
  184.     },
  185.     onmessage(e) {
  186.       var _self = this;
  187.       const description = e.message
  188.       _self.toUserId = e.from
  189.       switch (description.type) {
  190.         case 'connect':
  191.           _self.dialogVisible = true
  192.           this.$confirm(_self.toUserId + '请求连接!', '提示', {}).then(() => {
  193.             _self.startHandle().then(() => {
  194.               _self.stompClient.send("/api/chat", _self.toUserId, {'type': 'start'})
  195.             })
  196.           }).catch(() => {
  197.           });
  198.           break;
  199.         case 'start':
  200.           //同意连接之后开始连接
  201.           _self.startConnection()
  202.           break;
  203.         case 'offer':
  204.           _self.peerConnection.setRemoteDescription(new RTCSessionDescription(description)).then(() => {
  205.           }).catch((err) => {
  206.             console.log('local 设置远端描述信息错误', err);
  207.           });
  208.           _self.peerConnection.createAnswer().then(function (answer) {
  209.             _self.peerConnection.setLocalDescription(answer).then(() => {
  210.               console.log('设置本地answer成功!');
  211.             }).catch((err) => {
  212.               console.error('设置本地answer失败', err);
  213.             });
  214.             _self.stompClient.send("/api/chat", _self.toUserId, answer)
  215.           }).catch(e => {
  216.             console.error(e)
  217.           });
  218.           break;
  219.         case 'icecandidate':
  220.           // 创建 RTCIceCandidate 对象
  221.           let newIceCandidate = new RTCIceCandidate(description.icecandidate);
  222.           // 将本地获得的 Candidate 添加到远端的 RTCPeerConnection 对象中
  223.           _self.peerConnection.addIceCandidate(newIceCandidate).then(() => {
  224.             console.log(`addIceCandidate 成功`);
  225.           }).catch((error) => {
  226.             console.log(`addIceCandidate 错误:\n` + `${error.toString()}.`);
  227.           });
  228.           break;
  229.         case 'answer':
  230.           _self.peerConnection.setRemoteDescription(new RTCSessionDescription(description)).then(() => {
  231.             console.log('设置remote answer成功!');
  232.           }).catch((err) => {
  233.             console.log('设置remote answer错误', err);
  234.           });
  235.           break;
  236.         default:
  237.           break;
  238.       }
  239.     },
  240.     hangupHandle() {
  241.       var _self = this;
  242.       // 关闭连接并设置为空
  243.       _self.peerConnection.close();
  244.       _self.peerConnection = null;
  245.       // _self.hangupBtn.disabled = true;
  246.       // _self.callBtn.disabled = false;
  247.       _self.localStream.getTracks().forEach((track) => {
  248.         track.stop()
  249.       })
  250.     },
  251.     handleConnection(event) {
  252.       var _self = this;
  253.       // 获取到触发 icecandidate 事件的 RTCPeerConnection 对象
  254.       // 获取到具体的Candidate
  255.       console.log("handleConnection")
  256.       const peerConnection = event.target;
  257.       const icecandidate = event.candidate;
  258.       if (icecandidate) {
  259.         _self.stompClient.send("/api/chat", _self.toUserId, {
  260.           type: 'icecandidate',
  261.           icecandidate: icecandidate
  262.         })
  263.       }
  264.     },
  265.     gotRemoteMediaStream(event) {
  266.       var _self = this;
  267.       console.log('remote 开始接受远端流')
  268.       if (event.streams[0]) {
  269.         console.log(' remoteVideo')
  270.         _self.remoteVideo.srcObject = event.streams[0];
  271.         _self.remoteStream = event.streams[0];
  272.       }
  273.     },
  274.     handleConnectionChange(event) {
  275.       const peerConnection = event.target;
  276.       console.log('ICE state change event: ', event);
  277.       console.log(`ICE state: ` + `${peerConnection.iceConnectionState}.`);
  278.     },
  279.     log(v) {
  280.       console.log(v)
  281.     },
  282.   },
  283.   created() {
  284.     let that = this;
  285.     request({
  286.       url: '/user',
  287.       method: 'get',
  288.       params: {}
  289.     }).then(response => {
  290.       console.log(response.data)
  291.       that.userId = response.data;
  292.       this.startWebsocket(response.data)
  293.       debugger
  294.     })
  295.     debugger
  296.   }
  297. }
  298. </script>
  299. <style lang="scss">
  300. .spreadsheet {
  301.   padding: 0 10px;
  302.   margin: 20px 0;
  303. }
  304. .main-box {
  305.   display: flex;
  306.   flex-direction: row;
  307.   align-items: center;
  308.   justify-content: center;
  309. }
  310. </style>
复制代码
最终演示效果


具体代码查看

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

本帖子中包含更多资源

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

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

李优秀

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

标签云

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