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

孤独和弦 2024-07-02 09:03:04 阅读 85

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

原理简单解释使用技术搭建websocket环境依赖最终演示效果

原理简单解释

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

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

websocket传递信令服务

使用技术

java jdk17springboot 3.2.2websocket前端使用 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>

最终演示效果

在这里插入图片描述

具体代码查看



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。