|
@@ -0,0 +1,242 @@
|
|
|
+<template>
|
|
|
+
|
|
|
+ <div class="video-group-container">
|
|
|
+
|
|
|
+ <div v-for="video in videos" :key="video.userId" class="video-card">
|
|
|
+ <!-- 遍历渲染多人音视频控件 -->
|
|
|
+ <div class="group-video-container">
|
|
|
+ <video autoplay class="group-video" :srcObject="video.stream"></video>
|
|
|
+ <span class="video-username">{{video.userName}}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+
|
|
|
+ <div style="clear: both;"></div>
|
|
|
+ <!-- 挂断通话 -->
|
|
|
+ <svg class="leave-btn icon" t="1739368256646" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3572" width="40" height="40"><path d="M0 512a512 512 0 1 0 1024 0A512 512 0 1 0 0 512z" fill="#F75855" p-id="3573"></path><path d="M502.24 414.2c78.71 2.19 155.93 11.61 225.57 52.4 46.19 27.06 59.2 52.68 52.6 96.3-4.54 30-21.84 44.86-51.68 46.7-58.81 3.61-84.16-15.87-90.44-72.24-2.11-18.94-14-26.82-29.03-30.22-63.34-14.31-127.14-13.52-190.84-2.28-20.32 3.59-29.81 17.2-28.68 37.58 2.13 38.35-20.49 54.18-54.04 60.39-41.35 7.65-70-3.01-84.89-31.53-15.68-30.04-10.26-65.69 14.51-90.09 28.34-27.91 64.05-42.34 101.72-51.48 44.29-10.76 89.33-17.04 135.2-15.53z" fill="#FEFEFE" p-id="3574"></path></svg>
|
|
|
+ </div>
|
|
|
+
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import axios from 'axios';
|
|
|
+ import { inject, defineComponent, ref, onMounted, onUnmounted, watch, onBeforeUnmount } from 'vue';
|
|
|
+ export default defineComponent({
|
|
|
+ props: {
|
|
|
+ openVideo: Boolean,
|
|
|
+ selectChatUserRef: Object
|
|
|
+ },
|
|
|
+ setup(props) {
|
|
|
+
|
|
|
+ const socket = ref(null);
|
|
|
+ const messageQueue = ref([]); // 存储未发送的消息队列
|
|
|
+ const isConnected = ref(false);
|
|
|
+ const reconnectInterval = ref(5000); // 重连间隔时间,单位:毫秒
|
|
|
+ const heartbeatInterval = ref(30000); // 心跳间隔时间,单位:毫秒
|
|
|
+
|
|
|
+ const loginUser = ref(inject('loginUser'));
|
|
|
+ const selectChatUser = ref(props.selectChatUserRef);
|
|
|
+ // 本地音视频流
|
|
|
+ const localVideoRef = ref(null)
|
|
|
+ const localStream = ref(null)
|
|
|
+
|
|
|
+ const videos = ref([])
|
|
|
+
|
|
|
+ // 配置ICE Server(用于网络穿透)
|
|
|
+ const iceServerConfiguration = {
|
|
|
+ "iceServers": [
|
|
|
+ {
|
|
|
+ "urls": "stun:stun.l.google.com:19302"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 每个用户进度,先获取本地媒体流
|
|
|
+ // const peerConn = ref(null);
|
|
|
+ // 修改点 1:使用标准的 RTCPeerConnection
|
|
|
+ // const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
|
|
+ // 修改点 2:重构初始化本地流的逻辑
|
|
|
+ const initLocalStream = async () => {
|
|
|
+
|
|
|
+ // 作为呼叫方呼叫
|
|
|
+ sendObjMessage({
|
|
|
+ type: "call",
|
|
|
+ userId: loginUser.value.userId,
|
|
|
+ groupId: selectChatUser.value.userId
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 获取本地媒体流
|
|
|
+ // localStream.value = await navigator.mediaDevices.getUserMedia({
|
|
|
+ // video: true,
|
|
|
+ // audio: true
|
|
|
+ // });
|
|
|
+
|
|
|
+ // videos.value.push({
|
|
|
+ // userId: loginUser.value.userId,
|
|
|
+ // userName: loginUser.value.userName,
|
|
|
+ // stream: localStream.value
|
|
|
+ // })
|
|
|
+ // 初始化 PeerConnection
|
|
|
+ // peerConn.value = new PeerConnection(iceServerConfiguration);
|
|
|
+ // // 添加本地轨道(替代已废弃的 addStream)
|
|
|
+ // localStream.value.getTracks().forEach(track => {
|
|
|
+ // peerConn.value.addTrack(track, localStream.value);
|
|
|
+ // });
|
|
|
+ // // 修改点 3:使用现代 ontrack 事件监听
|
|
|
+ // peerConn.value.ontrack = (event) => {
|
|
|
+ // if (event.streams && event.streams.length > 0) {
|
|
|
+ // remoteStream.value = event.streams[0];
|
|
|
+ // remoteVideoRef.value.srcObject = remoteStream.value;
|
|
|
+ // }
|
|
|
+ // };
|
|
|
+ // // ICE 候选处理
|
|
|
+ // peerConn.value.onicecandidate = (event) => {
|
|
|
+ // if (event.candidate) {
|
|
|
+ // sendObjMessage({
|
|
|
+ // type: "candidate",
|
|
|
+ // candidate: event.candidate,
|
|
|
+ // userId: loginUser.value.userId,
|
|
|
+ // targetUserId: selectChatUser.value.userId
|
|
|
+ // });
|
|
|
+ // }
|
|
|
+ // };
|
|
|
+ // // 创建并发送 Offer
|
|
|
+ // if (offerUser.value) {
|
|
|
+ // const offer = await peerConn.value.createOffer();
|
|
|
+ // await peerConn.value.setLocalDescription(offer);
|
|
|
+ // sendObjMessage({
|
|
|
+ // type: "offer",
|
|
|
+ // offer: offer,
|
|
|
+ // userId: loginUser.value.userId,
|
|
|
+ // targetUserId: selectChatUser.value.userId
|
|
|
+ // });
|
|
|
+ // alert(selectChatUser.value.userName);
|
|
|
+ // }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('初始化本地流失败:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+
|
|
|
+ // 初始化信令服务器连接(ws)
|
|
|
+ const initSignalServerConnection = () => {
|
|
|
+ socket.value = new WebSocket(`/connect-service/ws/group/video/PC_WEB_CHAT/` + selectChatUser.value.userId + `/` + loginUser.value.userId);
|
|
|
+ socket.value.onopen = () => {
|
|
|
+ console.log('WebSocket 已连接');
|
|
|
+ isConnected.value = true;
|
|
|
+ // 发送队列中的消息
|
|
|
+ messageQueue.value.forEach(message => socket.value.send(message));
|
|
|
+ messageQueue.value = [];
|
|
|
+ startHeartbeat();
|
|
|
+ // 调起本地摄像头,初始化音视频流
|
|
|
+ // 初始化用户本地音视频资源
|
|
|
+ initLocalStream();
|
|
|
+ };
|
|
|
+
|
|
|
+ socket.value.onmessage = (event) => {
|
|
|
+ console.log('收到WS消息:', event.data);
|
|
|
+ const messageData = JSON.parse(event.data);
|
|
|
+ switch(messageData.type) {
|
|
|
+ case "offer":
|
|
|
+ handleOffer(messageData.offer);
|
|
|
+ break;
|
|
|
+ case "answer":
|
|
|
+ handleAnswer(messageData.answer);
|
|
|
+ break;
|
|
|
+ case "candidate" :
|
|
|
+ handleCandidate(messageData.candidate);
|
|
|
+ break;
|
|
|
+ case "leave" :
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const startHeartbeat = () => {
|
|
|
+ const heartbeat = () => {
|
|
|
+ if (isConnected.value) {
|
|
|
+ socket.value.send('ping'); // 发送心跳包
|
|
|
+ }
|
|
|
+ };
|
|
|
+ setInterval(heartbeat, heartbeatInterval.value);
|
|
|
+ };
|
|
|
+
|
|
|
+ socket.value.onerror = (error) => {
|
|
|
+ console.error('WebSocket 错误:', error);
|
|
|
+ isConnected.value = false;
|
|
|
+ // 关闭当前连接
|
|
|
+ socket.value.close();
|
|
|
+ // 尝试重连
|
|
|
+ setTimeout(connectWebSocket, reconnectInterval.value);
|
|
|
+ };
|
|
|
+
|
|
|
+ socket.value.onclose = () => {
|
|
|
+ console.log('WebSocket 已关闭');
|
|
|
+ isConnected.value = false;
|
|
|
+ // 尝试重连
|
|
|
+ setTimeout(connectWebSocket, reconnectInterval.value);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化ws连接
|
|
|
+ initSignalServerConnection();
|
|
|
+
|
|
|
+ const sendObjMessage = (obj) => {
|
|
|
+ sendMessage(JSON.stringify(obj))
|
|
|
+ }
|
|
|
+ const sendMessage = (message) => {
|
|
|
+ if (isConnected.value) {
|
|
|
+ socket.value.send(message);
|
|
|
+ } else {
|
|
|
+ messageQueue.value.push(message);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ onBeforeUnmount(() => {
|
|
|
+ cleanup();
|
|
|
+ }) ;
|
|
|
+ onMounted(() => {
|
|
|
+
|
|
|
+ });
|
|
|
+ onUnmounted(() => {
|
|
|
+
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ selectChatUser,
|
|
|
+ videos,
|
|
|
+ initLocalStream,
|
|
|
+ initSignalServerConnection
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style>
|
|
|
+
|
|
|
+ .group-video-container {
|
|
|
+ position: relative;
|
|
|
+ float: left;
|
|
|
+ width: 250px;
|
|
|
+ height: 200px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ .group-video-container video {
|
|
|
+ position: absolute;
|
|
|
+ left: 0px;
|
|
|
+ top: 0px;
|
|
|
+ width: 250px;
|
|
|
+ height: 200px;
|
|
|
+ }
|
|
|
+ .video-username {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ right: 20px;
|
|
|
+ background-color: black;
|
|
|
+ opacity: 0.5;
|
|
|
+ color: white;
|
|
|
+ padding: 3px 15px;
|
|
|
+ border-radius: 50px;
|
|
|
+ }
|
|
|
+
|
|
|
+</style>
|