原理简单表明
浏览器提供获取屏幕、音频等媒体数据的接口,
双方的媒体流数据通过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>
复制代码 最终演示效果
具体代码查看
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |