使用java版本 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的配置类
packagecom.example.webrtc.config;importcom.example.webrtc.Interceptor.AuthHandshakeInterceptor;importcom.example.webrtc.Interceptor.MyChannelInterceptor;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.messaging.converter.MessageConverter;importorg.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;importorg.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;importorg.springframework.messaging.simp.config.ChannelRegistration;importorg.springframework.messaging.simp.config.MessageBrokerRegistry;importorg.springframework.web.socket.config.annotation.*;importorg.springframework.web.socket.server.standard.ServerEndpointExporter;importjava.util.List;@Configuration@EnableWebSocketMessageBrokerpublicclassWebSocketConfigextendsWebSocketMessageBrokerConfigurationSupportimplementsWebSocketMessageBrokerConfigurer{privatestaticfinalLogger log =LoggerFactory.getLogger(WebSocketConfig.class);@AutowiredprivateAuthHandshakeInterceptor authHandshakeInterceptor;@AutowiredprivateMyChannelInterceptor myChannelInterceptor;@BeanpublicServerEndpointExporterserverEndpointExporter(){returnnewServerEndpointExporter();}@OverridepublicvoidregisterStompEndpoints(StompEndpointRegistry registry){
registry.addEndpoint("/chat-websocket").setAllowedOriginPatterns("*").addInterceptors(authHandshakeInterceptor).setAllowedOriginPatterns("*")// .setHandshakeHandler(myHandshakeHandler).withSockJS();}@OverridepublicvoidconfigureWebSocketTransport(WebSocketTransportRegistration registry){
registry.setMessageSizeLimit(Integer.MAX_VALUE);
registry.setSendBufferSizeLimit(Integer.MAX_VALUE);super.configureWebSocketTransport(registry);}@OverridepublicvoidconfigureMessageBroker(MessageBrokerRegistry registry){//客户端需要把消息发送到/message/xxx地址
registry.setApplicationDestinationPrefixes("/webSocket");//服务端广播消息的路径前缀,客户端需要相应订阅/topic/yyy这个地址的消息
registry.enableSimpleBroker("/topic","/user");//给指定用户发送消息的路径前缀,默认值是/user/
registry.setUserDestinationPrefix("/user/");}@OverridepublicvoidconfigureClientInboundChannel(ChannelRegistration registration){
registration.interceptors(myChannelInterceptor);}@OverridepublicvoidconfigureClientOutboundChannel(ChannelRegistration registration){WebSocketMessageBrokerConfigurer.super.configureClientOutboundChannel(registration);}@OverridepublicvoidaddArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers){WebSocketMessageBrokerConfigurer.super.addArgumentResolvers(argumentResolvers);}@OverridepublicvoidaddReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers){WebSocketMessageBrokerConfigurer.super.addReturnValueHandlers(returnValueHandlers);}@OverridepublicbooleanconfigureMessageConverters(List<MessageConverter> messageConverters){returnWebSocketMessageBrokerConfigurer.super.configureMessageConverters(messageConverters);}}
控制层 WebSocketController
packagecom.example.webrtc.controller;importcom.example.webrtc.config.Message;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.messaging.handler.annotation.MessageMapping;importorg.springframework.messaging.simp.SimpMessagingTemplate;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjava.security.Principal;importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.atomic.AtomicInteger;// 私信聊天的控制器@RestControllerpublicclassWebSocketController{@AutowiredprivateSimpMessagingTemplate messagingTemplate;privateAtomicInteger i=newAtomicInteger(1);@RequestMapping("/user")publicStringfindUser(){return"00"+i.decrementAndGet();}@MessageMapping("/api/chat")//在springmvc 中可以直接获得principal,principal 中包含当前用户的信息publicvoidhandleChat(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>importrequest from '@/utils/reeques'
importWebsocket from '@/utils/websocket'
importStomp from "stompjs";importSockJS from "sockjs-client";importadapter from "webrtc-adapter";importaxios 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')returnfalse}elseif(!that.stompClient){alert('请先打开websocket')returnfalse}elseif(that.toUserId == that.userId){alert('自己不能和自己连接')returnfalse}//准备连接
that.startHandle().then(()=>{
that.stompClient.send("/api/chat", that.toUserId,{'type': 'connect'})})},startWebsocket(user){
let that =this;
that.stompClient =newWebsocket(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 =newRTCPeerConnection()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 =truethis.$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(newRTCSessionDescription(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 =newRTCIceCandidate(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(newRTCSessionDescription(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:010px;
margin:20px 0;}.main-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;}</style>
最终演示效果
具体代码查看
本文转载自: https://blog.csdn.net/weixin_44327322/article/details/135701642
版权归原作者 孤独和弦 所有, 如有侵权,请联系我们删除。
版权归原作者 孤独和弦 所有, 如有侵权,请联系我们删除。