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

使用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>

最终演示效果

在这里插入图片描述

具体代码查看

  • 12
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值