李优秀 发表于 2024-8-16 06:19:18

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

原理简单表明

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


[*]java jdk17
[*]springboot 3.2.2
[*]websocket
[*]前端使用 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.label}.`);
      }
      // 判断音频轨道是否有值
      if (audioTracks.length > 0) {
          console.log(`使用的设备为: ${audioTracks.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) {
      console.log(' remoteVideo')
      _self.remoteVideo.srcObject = event.streams;
      _self.remoteStream = event.streams;
      }
    },
    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>

最终演示效果

https://i-blog.csdnimg.cn/blog_migrate/416a4686ce70e60d82e81d60800071d0.jpeg#pic_center
具体代码查看

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: java版本使用springboot vue websocket webrtc实现视频通话