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

avatar
作者
猴君
阅读量:0

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

原理简单解释

​ 浏览器提供获取屏幕、音频等媒体数据的接口,

​ 双方的媒体流数据通过Turn服务器传输

websocket传递信令服务

使用技术

  1. java jdk17
  2. springboot 3.2.2
  3. websocket
  4. 前端使用 vue

搭建websocket环境依赖

	<dependencies>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-web</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-websocket</artifactId>         </dependency>          <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-test</artifactId>             <scope>test</scope>         </dependency>     </dependencies> 

websocket的配置类

package com.example.webrtc.config;  import com.example.webrtc.Interceptor.AuthHandshakeInterceptor; import com.example.webrtc.Interceptor.MyChannelInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.*; import org.springframework.web.socket.server.standard.ServerEndpointExporter;  import java.util.List;  @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport implements WebSocketMessageBrokerConfigurer {     private static final Logger log = LoggerFactory.getLogger(WebSocketConfig.class);      @Autowired     private AuthHandshakeInterceptor authHandshakeInterceptor;       @Autowired     private MyChannelInterceptor myChannelInterceptor;      @Bean     public ServerEndpointExporter serverEndpointExporter(){         return new ServerEndpointExporter();     }      @Override     public void registerStompEndpoints(StompEndpointRegistry registry) {         registry.addEndpoint("/chat-websocket")                 .setAllowedOriginPatterns("*")                 .addInterceptors(authHandshakeInterceptor)                 .setAllowedOriginPatterns("*")              //   .setHandshakeHandler(myHandshakeHandler)                 .withSockJS();     }      @Override     public void configureWebSocketTransport(WebSocketTransportRegistration registry) {             registry.setMessageSizeLimit(Integer.MAX_VALUE);             registry.setSendBufferSizeLimit(Integer.MAX_VALUE);             super.configureWebSocketTransport(registry);     }      @Override     public void configureMessageBroker(MessageBrokerRegistry registry) {         //客户端需要把消息发送到/message/xxx地址         registry.setApplicationDestinationPrefixes("/webSocket");         //服务端广播消息的路径前缀,客户端需要相应订阅/topic/yyy这个地址的消息         registry.enableSimpleBroker("/topic", "/user");         //给指定用户发送消息的路径前缀,默认值是/user/         registry.setUserDestinationPrefix("/user/");     }       @Override     public void configureClientInboundChannel(ChannelRegistration registration) {         registration.interceptors(myChannelInterceptor);     }      @Override     public void configureClientOutboundChannel(ChannelRegistration registration) {         WebSocketMessageBrokerConfigurer.super.configureClientOutboundChannel(registration);     }      @Override     public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {         WebSocketMessageBrokerConfigurer.super.addArgumentResolvers(argumentResolvers);     }      @Override     public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {         WebSocketMessageBrokerConfigurer.super.addReturnValueHandlers(returnValueHandlers);     }      @Override     public boolean configureMessageConverters(List<MessageConverter> messageConverters) {         return WebSocketMessageBrokerConfigurer.super.configureMessageConverters(messageConverters);     }  } 

控制层 WebSocketController

package com.example.webrtc.controller;  import com.example.webrtc.config.Message; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;  import java.security.Principal; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger;  // 私信聊天的控制器 @RestController public class WebSocketController {     @Autowired     private SimpMessagingTemplate messagingTemplate;     private AtomicInteger i=new AtomicInteger(1);     @RequestMapping("/user")     public String findUser(){         return "00"+i.decrementAndGet();     }     @MessageMapping("/api/chat")     //在springmvc 中可以直接获得principal,principal 中包含当前用户的信息     public void handleChat(Principal principal, Message messagePara) {          String currentUserName = principal.getName();         System.out.println(currentUserName);          try {             messagePara.setFrom(principal.getName());             System.out.println("from" + messagePara.getFrom());             messagingTemplate.convertAndSendToUser(messagePara.getTo(),                     "/queue/notifications",                     messagePara);         } catch (Exception e) {             // 打印异常             e.printStackTrace();         }     } }  

前端交互拨号index.vue

<template>   <div class="play-audio">     <h2 style="text-align: center;">播放页面</h2>     <div class="main-box">       <video ref="localVideo" class="video" autoplay="autoplay"></video>       <video ref="remoteVideo" class="video" height="500px" autoplay="autoplay"></video>     </div>     <div style="text-align: center;">       <el-button @click="requestConnect()" ref="callBtn">开始对讲</el-button>       <el-button @click="hangupHandle()" ref="hangupBtn">结束对讲</el-button>     </div>     <div style="text-align: center;">       <label for="name">发送人:</label>       <input type="text" id="name" readonly v-model="userId" class="form-control"/>     </div>     <div style="text-align: center;">       <label for="name">接收人:</label>       <input type="text" id="name" v-model="toUserId" class="form-control"/>     </div>    </div>  </template>  <el-dialog :title="'提示'" :visible.sync="dialogVisible" width="30%"> <span>{{ toUserId + '请求连接!' }}</span> <span slot="footer" class="dialog-footer">     <el-button @click="handleClose">取 消</el-button>     <el-button type="primary" @click="dialogVisibleYes">确 定</el-button>   </span> </el-dialog>  <script> import request from '@/utils/reeques' import Websocket from '@/utils/websocket' import Stomp from "stompjs"; import SockJS from "sockjs-client"; import adapter from "webrtc-adapter"; import axios from 'axios'  export default {   data() {     return {       stompClient: null,       userId: '001',       socket: null,       toUserId: '',       localStream: null,       remoteStream: null,       localVideo: null,       remoteVideo: null,       callBtn: null,       hangupBtn: null,       peerConnection: null,       dialogVisible: false,       msg: '',       config: {         iceServers: [           {urls: 'stun:global.stun.twilio.com:3478?transport=udp'}         ],       }      };   },   computed: {},   methods: {     handleClose() {       this.dialogVisible = false     },     dialogVisibleYes() {       var _self = this;       this.dialogVisible = false       _self.startHandle().then(() => {         _self.stompClient.send("/api/chat", _self.toUserId, {'type': 'start'})       })     },     requestConnect() {       let that = this;        if (!that.toUserId) {         alert('请输入对方id')         return false       } else if (!that.stompClient) {         alert('请先打开websocket')         return false       } else if (that.toUserId == that.userId) {         alert('自己不能和自己连接')         return false       }       //准备连接       that.startHandle().then(() => {         that.stompClient.send("/api/chat", that.toUserId, {'type': 'connect'})       })     },      startWebsocket(user) {       let that = this;       that.stompClient = new Websocket(user);       that.stompClient.connect(() => {         that.stompClient.subscribe("/user/" + that.userId + "/queue/notifications", function (result) {           that.onmessage(result)         })       })     }     ,     gotLocalMediaStream(mediaStream) {       var _self = this;       _self.localVideo.srcObject = mediaStream;       _self.localStream = mediaStream;       // _self.callBtn.disabled = false;     }     ,     createConnection() {       var _self = this;       _self.peerConnection = new RTCPeerConnection()        if (_self.localStream) {         // 视频轨道         const videoTracks = _self.localStream.getVideoTracks();         // 音频轨道         const audioTracks = _self.localStream.getAudioTracks();         // 判断视频轨道是否有值         if (videoTracks.length > 0) {           console.log(`使用的设备为: ${videoTracks[0].label}.`);         }         // 判断音频轨道是否有值         if (audioTracks.length > 0) {           console.log(`使用的设备为: ${audioTracks[0].label}.`);         }          _self.localStream.getTracks().forEach((track) => {           _self.peerConnection.addTrack(track, _self.localStream)         })       }        // 监听返回的 Candidate       _self.peerConnection.addEventListener('icecandidate', _self.handleConnection);       // 监听 ICE 状态变化       _self.peerConnection.addEventListener('iceconnectionstatechange', _self.handleConnectionChange)       //拿到流的时候调用       _self.peerConnection.addEventListener('track', _self.gotRemoteMediaStream);     }     ,     startConnection() {       var _self = this;       // _self.callBtn.disabled  = true;       // _self.hangupBtn.disabled = false;       // 发送offer       _self.peerConnection.createOffer().then(description => {         console.log(`本地创建offer返回的sdp:\n${description.sdp}`)          // 将 offer 保存到本地         _self.peerConnection.setLocalDescription(description).then(() => {           console.log('local 设置本地描述信息成功');           // 本地设置描述并将它发送给远端           // _self.socket.send(JSON.stringify({           //   'userId': _self.userId,           //   'toUserId': _self.toUserId,           //   'message': description           // }));           _self.stompClient.send("/api/chat", _self.toUserId, description)          }).catch((err) => {           console.log('local 设置本地描述信息错误', err)         });       })         .catch((err) => {           console.log('createdOffer 错误', err);         });     }     ,     async startHandle() {       this.callBtn = this.$refs.callBtn       this.hangupBtn = this.$refs.hangupBtn       this.remoteVideo = this.$refs.remoteVideo       this.localVideo = this.$refs.localVideo       var _self = this;       // 1.获取本地音视频流       // 调用 getUserMedia API 获取音视频流       let constraints = {         video: true,         audio: {           // 设置回音消除           noiseSuppression: true,           // 设置降噪           echoCancellation: true,         }       }       navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia       await navigator.mediaDevices.getUserMedia(constraints)         .then(_self.gotLocalMediaStream)         .catch((err) => {           console.log('getUserMedia 错误', err);           //创建点对点连接对象         });        _self.createConnection();     },     onmessage(e) {       var _self = this;       const description = e.message       _self.toUserId = e.from       switch (description.type) {         case 'connect':           _self.dialogVisible = true           this.$confirm(_self.toUserId + '请求连接!', '提示', {}).then(() => {             _self.startHandle().then(() => {               _self.stompClient.send("/api/chat", _self.toUserId, {'type': 'start'})             })           }).catch(() => {           });           break;         case 'start':           //同意连接之后开始连接           _self.startConnection()           break;         case 'offer':           _self.peerConnection.setRemoteDescription(new RTCSessionDescription(description)).then(() => {            }).catch((err) => {             console.log('local 设置远端描述信息错误', err);           });            _self.peerConnection.createAnswer().then(function (answer) {              _self.peerConnection.setLocalDescription(answer).then(() => {               console.log('设置本地answer成功!');             }).catch((err) => {               console.error('设置本地answer失败', err);             });             _self.stompClient.send("/api/chat", _self.toUserId, answer)           }).catch(e => {             console.error(e)           });           break;         case 'icecandidate':           // 创建 RTCIceCandidate 对象           let newIceCandidate = new RTCIceCandidate(description.icecandidate);           // 将本地获得的 Candidate 添加到远端的 RTCPeerConnection 对象中           _self.peerConnection.addIceCandidate(newIceCandidate).then(() => {             console.log(`addIceCandidate 成功`);           }).catch((error) => {             console.log(`addIceCandidate 错误:\n` + `${error.toString()}.`);           });           break;         case 'answer':           _self.peerConnection.setRemoteDescription(new RTCSessionDescription(description)).then(() => {             console.log('设置remote answer成功!');           }).catch((err) => {             console.log('设置remote answer错误', err);           });           break;         default:           break;       }     },     hangupHandle() {       var _self = this;       // 关闭连接并设置为空       _self.peerConnection.close();       _self.peerConnection = null;        // _self.hangupBtn.disabled = true;       // _self.callBtn.disabled = false;        _self.localStream.getTracks().forEach((track) => {         track.stop()       })     },     handleConnection(event) {       var _self = this;       // 获取到触发 icecandidate 事件的 RTCPeerConnection 对象       // 获取到具体的Candidate       console.log("handleConnection")       const peerConnection = event.target;       const icecandidate = event.candidate;        if (icecandidate) {         _self.stompClient.send("/api/chat", _self.toUserId, {           type: 'icecandidate',           icecandidate: icecandidate         })       }     },     gotRemoteMediaStream(event) {       var _self = this;       console.log('remote 开始接受远端流')        if (event.streams[0]) {         console.log(' remoteVideo')         _self.remoteVideo.srcObject = event.streams[0];         _self.remoteStream = event.streams[0];       }     },     handleConnectionChange(event) {       const peerConnection = event.target;       console.log('ICE state change event: ', event);       console.log(`ICE state: ` + `${peerConnection.iceConnectionState}.`);     },     log(v) {       console.log(v)     },   },   created() {     let that = this;     request({       url: '/user',       method: 'get',       params: {}     }).then(response => {       console.log(response.data)       that.userId = response.data;       this.startWebsocket(response.data)       debugger     })     debugger    } }  </script> <style lang="scss"> .spreadsheet {   padding: 0 10px;   margin: 20px 0; }  .main-box {   display: flex;   flex-direction: row;   align-items: center;   justify-content: center; } </style>  

最终演示效果

在这里插入图片描述

具体代码查看

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!