使用java版本 websocket webrtc实现视频通话
- 原理简单解释
- 使用技术
- 搭建websocket环境依赖
- 最终演示效果
原理简单解释
浏览器提供获取屏幕、音频等媒体数据的接口,
双方的媒体流数据通过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[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>
最终演示效果
具体代码查看