diff --git a/dist.zip b/dist.zip index 8f2584e..496d99d 100644 Binary files a/dist.zip and b/dist.zip differ diff --git a/src/api/conferencingRoom.js b/src/api/conferencingRoom.js index 7022406..bb4a8db 100644 --- a/src/api/conferencingRoom.js +++ b/src/api/conferencingRoom.js @@ -121,3 +121,11 @@ export function getConvertStatusApi(taskId,roomId) { }) } +//获取当前会议中所有参与者信息 +export function getParticipantsApi(roomId) { + return request({ + url: `/api/v1/meeting/${ roomId }/participants`, + method: 'get', + }) +} + \ No newline at end of file diff --git a/src/assets/images/amplify.png b/src/assets/images/amplify.png new file mode 100644 index 0000000..4e4306a Binary files /dev/null and b/src/assets/images/amplify.png differ diff --git a/src/assets/images/shrink.png b/src/assets/images/shrink.png new file mode 100644 index 0000000..8e60666 Binary files /dev/null and b/src/assets/images/shrink.png differ diff --git a/src/utils/request.js b/src/utils/request.js index 6d82683..90ab7b0 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -135,14 +135,14 @@ service.interceptors.response.use( return Promise.resolve(responseData); case 401: // return Promise.resolve(responseData); - const currentPath = router.currentRoute.value.name; - if(currentPath == 'ConferencingRoom'){ - return Promise.resolve(responseData); - }else{ + // const currentPath = router.currentRoute.value.name; + // if(currentPath == 'ConferencingRoom'){ + // return Promise.resolve(responseData); + // }else{ return handleUnauthorized().then(() => { return Promise.reject({ code: 401, message: '未授权' }); }); - } + // } case 500: const serverErrorMsg = responseData.meta?.message || '服务器内部错误'; ElMessage({ message: serverErrorMsg, type: 'error' }); @@ -210,9 +210,7 @@ function handleUnauthorized() { // 使用 nextTick 确保路由状态已更新 import('vue').then(({ nextTick }) => { nextTick(() => { - const currentPath = router.currentRoute.value.fullPath; - console.log('当前路由:', currentPath); - + const currentPath = router.currentRoute.value.fullPath; if (router.currentRoute.value.path !== '/login') { router.push({ path: '/login', diff --git a/src/utils/whiteboardSync.js b/src/utils/whiteboardSync.js index 58f9677..713fbb4 100644 --- a/src/utils/whiteboardSync.js +++ b/src/utils/whiteboardSync.js @@ -24,17 +24,14 @@ function getLocalUserData() { export const WhiteboardSync = { async init(canvas, roomUid) { - if (!canvas || !roomUid) return; - console.log('初始化多人同步:', roomUid); + if (!canvas || !roomUid) return; canvasInstance = canvas; const localUser = getLocalUserData(); const localUid = localUser?.uid; try { // 先连接 MQTT - await mqttClient.connect(meterStore.getSudid()); - console.log("✅ MQTT 已连接"); - + await mqttClient.connect(meterStore.getSudid()); // 获取历史数据 const res = await getWhiteboardHistory({ after_timestamp: 0 }, roomUid); if (res.meta.code === 200 && res.data.shapes.length > 0) { @@ -47,8 +44,7 @@ export const WhiteboardSync = { const topic = `xSynergy/ROOM/${roomUid}/whiteboard/#`; mqttClient.subscribe(topic, async (shapeData) => { const shapeDataNew = JSON.parse(shapeData.toString()) - // const shapeDataNew = decode(message); - // console.log(shapeDataNew, '格式解码') + // const shapeDataNew = decode(message); try { isRemote = true; // 如果 shape 来自本地用户,则跳过 @@ -65,9 +61,7 @@ export const WhiteboardSync = { } finally { isRemote = false; } - }); - - console.log("✅ 已订阅:", topic); + }); } catch (err) { console.log("初始化多人同步失败:", err) // console.error("❌ 连接或订阅失败:", err); diff --git a/src/views/conferencingRoom/business/index.js b/src/views/conferencingRoom/business/index.js index 91040ea..5cdeeb3 100644 --- a/src/views/conferencingRoom/business/index.js +++ b/src/views/conferencingRoom/business/index.js @@ -43,11 +43,11 @@ export function getDeviceName(devices, deviceId) { } export function handleVideoLoaded(identity, type) { - console.log(`视频加载完成: ${identity}的${type}视频`); + // console.log(`视频加载完成: ${identity}的${type}视频`); } export function handleConnectionStateChanged(state) { - console.log('连接状态改变:', state); + // console.log('连接状态改变:', state); } // 平滑曲线绘制函数 @@ -99,4 +99,9 @@ export async function calculateFileSHA1(file) { // 用于比较两个对象是否相等 export function simpleDeepEqual(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2); +} + +// 生成唯一ID +export function generateElementId() { + return `laser_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } \ No newline at end of file diff --git a/src/views/conferencingRoom/components/InviterJoinRoom/index.vue b/src/views/conferencingRoom/components/InviterJoinRoom/index.vue index 3bfc8cf..1c9cffa 100644 --- a/src/views/conferencingRoom/components/InviterJoinRoom/index.vue +++ b/src/views/conferencingRoom/components/InviterJoinRoom/index.vue @@ -1,11 +1,10 @@ - - - + + - + + \ No newline at end of file diff --git a/src/views/conferencingRoom/pathTransit.vue b/src/views/conferencingRoom/pathTransit.vue new file mode 100644 index 0000000..6bb0774 --- /dev/null +++ b/src/views/conferencingRoom/pathTransit.vue @@ -0,0 +1,3175 @@ + + + + + \ No newline at end of file diff --git a/src/views/conferencingRoom/text.vue b/src/views/conferencingRoom/text.vue index fb2be08..988320d 100644 --- a/src/views/conferencingRoom/text.vue +++ b/src/views/conferencingRoom/text.vue @@ -1,245 +1,356 @@ @@ -249,12 +360,15 @@ import { ElMessage ,ElMessageBox} from 'element-plus'; import { ArrowDown, Refresh, Check } from '@element-plus/icons-vue'; import { getRoomToken, getRoomList ,getInvite,getTokenApi,exitRoomApi,getRoomInfoApi} from "@/api/conferencingRoom.js" import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client"; -import { errorHandling ,handleDataReceived ,handleReconnected,getDeviceName,handleVideoLoaded,handleConnectionStateChanged,drawSmoothCurve} from './business/index.js' +import { errorHandling ,handleDataReceived ,handleReconnected,getDeviceName,handleVideoLoaded,handleConnectionStateChanged,drawSmoothCurve,simpleDeepEqual,generateElementId} from './business/index.js' import { useRoute, useRouter } from 'vue-router' import { useRoomStore } from '@/stores/modules/room.js' import { useUserStore } from '@/stores/modules/user.js' import tabulaRase from '@/views/custom/tabulaRase/index.vue' import { mqttClient } from "@/utils/mqtt.js"; +import { emitter } from "@/utils/bus.js"; +import Login from "@/components/Login/index.vue"; + import InviterJoinRoom from "@/views/conferencingRoom/components/InviterJoinRoom/index.vue" import FileList from "@/views/conferencingRoom/components/fileUpload/fileList.vue" @@ -302,20 +416,44 @@ const inviterJoinRoomRef = ref() const isLaserPointerActive = ref(false); const laserPointerCanvas = ref(null); const laserPointerContext = ref(null); -const laserPointerElements = ref([]); -const laserPointerTimeout = ref(null); +const laserPointerElements = ref(new Map()); // 改为 Map 结构,key 为时间戳 +const laserPointerTimeouts = ref(new Map()); // 管理每个元素的定时器 +const laserPointerTimeout = ref(null); +const canvasSizeObserver = ref(null); +const screenShareVideoElement = ref(null); + //文件上传 const fileListRef = ref() +//mqtt相关 +const isMqttFileuploadSucc = ref(false) +const isMqttFilePreview = ref(false) +const isMqttFileConversionStatus = ref(false) +const isMqttEnlargeVideo = ref(false) +const showLogin = ref(false); // 是否显示登录页面 + +// 放大视图相关 +const enlargedParticipant = ref(null); // 当前放大的参与者 +const enlargedVideo = ref(null); // 放大视频元素引用 +const enlargedLaserPointerCanvas = ref(null); // 放大视频激光笔 Canvas +const enlargedLaserPointerContext = ref(null); // 放大视频激光笔上下文 + +// 路径更新节流处理 +let lastPublishTime = 0; +const publishThrottleTime = 100; // 100ms 节流 // 鼠标状态跟踪 const mouseState = reactive({ isDrawing: false, - lastX: 0, - lastY: 0, startX: 0, - startY: 0, - currentPath: [] // 添加当前路径数组 + startY: 0 }); +// 放大视频鼠标状态跟踪 +const enlargedMouseState = reactive({ + isDrawing: false, + startX: 0, + startY: 0 +}); + // 激光笔样式配置 const laserPointerConfig = reactive({ color: '#ff0000', // 红色激光 @@ -335,8 +473,12 @@ const LASER_POINTER_MESSAGE_TYPES = { DRAW: 'laser_draw', CLEAR: 'laser_clear' }; +//视屏缩放类型 +const VIDEO_ENLARGE_MESSAGE_TYPES = { + ENLARGE: 'enlarge_video', + SHRINK: 'shrink_video' +}; -// 计算属性 const participantCount = computed(() => { return remoteParticipants.value.size + 1; // 包括自己 }); @@ -349,6 +491,25 @@ const hasActiveScreenShare = computed(() => { return screenSharingUser.value !== '' || isScreenSharing.value; }); +// 添加激光笔可用性计算属性 +const canUseLaserPointer = computed(() => { + return cameraEnabled.value || hasActiveScreenShare.value || enlargedParticipant.value; +}); +// 是否显示放大按钮(当有屏幕共享时隐藏) +const shouldShowEnlargeButtons = computed(() => { + return !hasActiveScreenShare.value; +}); + +// 是否允许屏幕共享(当有放大视频时禁用) +const canScreenShare = computed(() => { + return !enlargedParticipant.value; +}); + +// 是否允许白板共享 +const canWhiteboardShare = computed(() => { + return !enlargedParticipant.value; +}); + // 添加一个计算属性来显示屏幕共享状态提示 const screenShareStatusText = computed(() => { if (isGlobalScreenSharing.value) { @@ -359,7 +520,7 @@ const screenShareStatusText = computed(() => { } } return '暂无屏幕共享'; -}); +}); // 创建 Room 实例 const room = new Room({ @@ -392,26 +553,617 @@ const room = new Room({ } }); -// 激光笔功能 -function toggleLaserPointer() { - if (!hasActiveScreenShare.value || isWhiteboardActive.value) { - ElMessage.warning('请在屏幕共享模式下使用激光笔'); +emitter.on('whiteboardFailed',whiteboardFailedHandle); + +/** 登录成功回调 */ +function handleLoginSuccess() { + showLogin.value = false; +} + +function whiteboardFailedHandle(e){ + showLogin.value = e; +} + +// 放大参与者视频 +function enlargeParticipant(participant) { + // 只允许放大自己的视频 + if (participant.identity !== hostUid.value) { + ElMessage.warning('只能放大自己的视频'); return; } - - isLaserPointerActive.value = !isLaserPointerActive.value; + if (participantCount.value <= 1) { + ElMessage.warning('需要至少2名参与者才能使用放大功能'); + return; + } + + if (!cameraEnabled.value) { + ElMessage.warning('请先打开摄像头'); + return; + } + // 如果正在屏幕共享,自动停止 + if (isScreenSharing.value) { + room.localParticipant.setScreenShareEnabled(false); + isScreenSharing.value = false; + ElMessage.info('已自动停止屏幕共享,开启视频放大模式'); + } + // 如果正在使用白板,自动退出 + if (isWhiteboardActive.value) { + exitWhiteboard(); + ElMessage.info('已自动退出白板,开启视频放大模式'); + } + + // 如果正在放大其他用户,先关闭 + if (enlargedParticipant.value && enlargedParticipant.value.identity !== hostUid.value) { + closeEnlargedView(); + } + + enlargedParticipant.value = participant; + ElMessage.success(`已放大您的视频`); + + // 发布放大消息给其他用户 + publishEnlargeVideoMessage(VIDEO_ENLARGE_MESSAGE_TYPES.ENLARGE, { + participant: { + identity: participant.identity, + hasCameraTrack: participant.hasCameraTrack, + audioEnabled: participant.audioEnabled + } + }); + // 初始化放大视频激光笔 + nextTick(() => { + initEnlargedLaserPointerCanvas(); + }); +} + +// 关闭放大视图 +function closeEnlargedView() { + // 检查激光笔是否开启,如果开启则先关闭激光笔 if (isLaserPointerActive.value) { - initLaserPointerCanvas(); + // 先关闭激光笔 + isLaserPointerActive.value = false; + cleanupAllLaserElements(); + + // 更新 Canvas 样式 + if (enlargedLaserPointerCanvas.value) { + enlargedLaserPointerCanvas.value.style.pointerEvents = 'none'; + enlargedLaserPointerCanvas.value.style.cursor = 'default'; + } + if (laserPointerCanvas.value) { + laserPointerCanvas.value.style.pointerEvents = 'none'; + laserPointerCanvas.value.style.cursor = 'default'; + } + } + // 如果当前放大的是自己的视频,发送缩小消息 + if (enlargedParticipant.value && enlargedParticipant.value.identity === hostUid.value) { + publishEnlargeVideoMessage(VIDEO_ENLARGE_MESSAGE_TYPES.SHRINK, { + participant: { + identity: hostUid.value + } + }); + } + enlargedParticipant.value = null; + ElMessage.info('已关闭放大视图'); + // 清理放大视频激光笔 + cleanupEnlargedLaserPointer(); +} +// 设置放大视频引用 +function setEnlargedVideoRef(el) { + if (!el) return; + enlargedVideo.value = el; + + // 如果已经有轨道数据,立即附加 + if (enlargedParticipant.value) { + if (enlargedParticipant.value.identity === hostUid.value) { + // 本地视频 + if (localVideo.value && localVideo.value.srcObject) { + el.srcObject = localVideo.value.srcObject; + } + } else { + // 远程参与者 + const participantData = remoteParticipants.value.get(enlargedParticipant.value.identity); + if (participantData && participantData.cameraTrack) { + attachTrackToVideo(el, participantData.cameraTrack); + } + } + } +} + +// 处理放大视频加载 +function handleEnlargedVideoLoaded() { + // console.log('放大视频已加载'); + // 初始化放大视频激光笔 + initEnlargedLaserPointerCanvas(); +} + +// 初始化放大视频激光笔 Canvas +function initEnlargedLaserPointerCanvas() { + if (!enlargedLaserPointerCanvas.value || !enlargedParticipant.value) return; + + const targetElement = document.querySelector('.enlarged-video-element'); + if (!targetElement) return; + + // 设置 Canvas 尺寸与目标元素一致 + updateEnlargedCanvasSize(targetElement); + + // 获取上下文 + enlargedLaserPointerContext.value = enlargedLaserPointerCanvas.value.getContext('2d'); + + // 设置 Canvas 样式 + enlargedLaserPointerCanvas.value.style.position = 'absolute'; + enlargedLaserPointerCanvas.value.style.top = '0'; + enlargedLaserPointerCanvas.value.style.left = '0'; + enlargedLaserPointerCanvas.value.style.zIndex = '10'; + enlargedLaserPointerCanvas.value.style.cursor = isLaserPointerActive.value ? 'crosshair' : 'default'; + enlargedLaserPointerCanvas.value.style.pointerEvents = isLaserPointerActive.value ? 'auto' : 'none'; + + // 初始化观察器监听目标元素大小变化 + initEnlargedResizeObserver(targetElement); +} + + + +function updateEnlargedCanvasSize(targetElement) { + if (!enlargedLaserPointerCanvas.value || !targetElement) return; + + const rect = targetElement.getBoundingClientRect(); + enlargedLaserPointerCanvas.value.width = rect.width; + enlargedLaserPointerCanvas.value.height = rect.height; + enlargedLaserPointerCanvas.value.style.width = `${rect.width}px`; + enlargedLaserPointerCanvas.value.style.height = `${rect.height}px`; + + // 重新绘制所有元素 + redrawEnlargedLaserElements(); +} + +// 重绘放大视频激光笔元素 +function redrawEnlargedLaserElements() { + if (!enlargedLaserPointerContext.value || !enlargedLaserPointerCanvas.value) return; + + const ctx = enlargedLaserPointerContext.value; + ctx.clearRect(0, 0, enlargedLaserPointerCanvas.value.width, enlargedLaserPointerCanvas.value.height); + + // 绘制所有活跃的元素 + laserPointerElements.value.forEach(element => { + if (element.type === 'circle') { + const centerPixel = getEnlargedPixelFromPercentage(element.data.center); + drawEnlargedCircle(centerPixel); + } else if (element.type === 'line') { + const startPixel = getEnlargedPixelFromPercentage(element.data.start); + const endPixel = getEnlargedPixelFromPercentage(element.data.end); + drawEnlargedLine(startPixel.x, startPixel.y, endPixel.x, endPixel.y); + } + }); +} + +// 放大视频绘制函数 +function drawEnlargedCircle(centerPixel) { + if (!enlargedLaserPointerContext.value) return; + + const ctx = enlargedLaserPointerContext.value; + ctx.beginPath(); + ctx.arc(centerPixel.x, centerPixel.y, laserPointerConfig.thickness, 0, Math.PI * 2); + ctx.fillStyle = laserPointerConfig.color; + ctx.fill(); + ctx.stroke(); +} + +function drawEnlargedLine(startX, startY, endX, endY) { + if (!enlargedLaserPointerContext.value) return; + + const ctx = enlargedLaserPointerContext.value; + ctx.beginPath(); + ctx.moveTo(startX, startY); + ctx.lineTo(endX, endY); + ctx.strokeStyle = laserPointerConfig.color; + ctx.lineWidth = laserPointerConfig.thickness; + ctx.lineCap = 'round'; + ctx.stroke(); +} + +// 初始化放大视频 ResizeObserver +function initEnlargedResizeObserver(targetElement) { + // 先清理之前的观察器 + if (canvasSizeObserver.value) { + canvasSizeObserver.value.disconnect(); + } + + canvasSizeObserver.value = new ResizeObserver((entries) => { + for (let entry of entries) { + if (entry.target === targetElement) { + updateEnlargedCanvasSize(targetElement); + } + } + }); + + canvasSizeObserver.value.observe(targetElement); +} + +// 清理放大视频激光笔 +function cleanupEnlargedLaserPointer() { + if (enlargedLaserPointerContext.value && enlargedLaserPointerCanvas.value) { + enlargedLaserPointerContext.value.clearRect(0, 0, enlargedLaserPointerCanvas.value.width, enlargedLaserPointerCanvas.value.height); + } + enlargedMouseState.isDrawing = false; +} + +// 放大视频 Canvas 事件处理 +function handleEnlargedCanvasDoubleClick(e) { + if (!isLaserPointerActive.value || !enlargedParticipant.value) return; + + const coords = getEnlargedMouseCoordinates(e); + + // 创建圆形标记元素 + const circleElement = { + type: 'circle', + data: { + color: laserPointerConfig.color, + center: coords, + thickness: laserPointerConfig.thickness + } + }; + + const elementId = addLaserElement(circleElement); + publishLaserPointerData({ + ...circleElement, + id: elementId, + target: 'enlarged' // 标记为放大视频激光笔 + }); +} + +function handleEnlargedCanvasMouseDown(e) { + if (!isLaserPointerActive.value || !enlargedParticipant.value) return; + + const pixelCoords = getEnlargedPixelCoordinates(e); + enlargedMouseState.isDrawing = true; + enlargedMouseState.startX = pixelCoords.x; + enlargedMouseState.startY = pixelCoords.y; +} + +function handleEnlargedCanvasMouseMove(e) { + // 可以添加实时绘制预览 +} + +function handleEnlargedCanvasMouseUp(e) { + if (!isLaserPointerActive.value || !enlargedMouseState.isDrawing || !enlargedParticipant.value) return; + + const endCoords = getEnlargedMouseCoordinates(e); + const startCoords = getEnlargedMouseCoordinates({ + clientX: enlargedMouseState.startX + enlargedLaserPointerCanvas.value.getBoundingClientRect().left, + clientY: enlargedMouseState.startY + enlargedLaserPointerCanvas.value.getBoundingClientRect().top + }); + + // 创建线条元素 + const lineElement = { + type: 'line', + data: { + color: laserPointerConfig.color, + start: startCoords, + end: endCoords, + thickness: laserPointerConfig.thickness + } + }; + + const startPixel = getEnlargedPixelFromPercentage(lineElement.data.start); + const endPixel = getEnlargedPixelFromPercentage(lineElement.data.end); + + // 只有当起点和终点不同时才创建线条 + if (!simpleDeepEqual(lineElement.data.start, lineElement.data.end)) { + const elementId = addLaserElement(lineElement); + publishLaserPointerData({ + ...lineElement, + id: elementId, + target: 'enlarged' // 标记为放大视频激光笔 + }); + } + + enlargedMouseState.isDrawing = false; +} + +function handleEnlargedCanvasMouseLeave() { + enlargedMouseState.isDrawing = false; +} + +// 获取放大视频鼠标坐标(转换为百分比坐标) +function getEnlargedMouseCoordinates(e) { + if (!enlargedLaserPointerCanvas.value) return { x: 0, y: 0 }; + + const rect = enlargedLaserPointerCanvas.value.getBoundingClientRect(); + return { + x: ((e.clientX - rect.left) / enlargedLaserPointerCanvas.value.width).toFixed(4), + y: ((e.clientY - rect.top) / enlargedLaserPointerCanvas.value.height).toFixed(4) + }; +} + +// 获取放大视频实际像素坐标 +function getEnlargedPixelCoordinates(e) { + if (!enlargedLaserPointerCanvas.value) return { x: 0, y: 0 }; + + const rect = enlargedLaserPointerCanvas.value.getBoundingClientRect(); + return { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; +} + +// 从百分比坐标转换为放大视频像素坐标 +function getEnlargedPixelFromPercentage(percentageCoords) { + if (!enlargedLaserPointerCanvas.value) return { x: 0, y: 0 }; + + return { + x: parseFloat(percentageCoords.x) * enlargedLaserPointerCanvas.value.width, + y: parseFloat(percentageCoords.y) * enlargedLaserPointerCanvas.value.height + }; +} + + + +//订阅视屏缩放 +async function initMqttEnlargeVideo(){ + try { + if (isMqttEnlargeVideo.value) return + const clientId = `enlargeVideo_${Date.now()}` + await mqttClient.connect(clientId) + isMqttEnlargeVideo.value = true + // 订阅主题 + subscribeToEnlargeVideoTopic() + } catch (error) { + console.error('MQTT连接失败:', error) + ElMessage.error('视屏缩放服务连接失败') + } +} + +function subscribeToEnlargeVideoTopic() { + try { + mqttClient.subscribe(`xSynergy/enlarge_video/${room.name}`, handleEnlargeVideoMessage); + } catch (error) { + console.error('订阅激光笔主题失败:', error); + } +} +//接收视屏缩放消息 +function handleEnlargeVideoMessage(payload, topic) { + try { + const messageStr = payload.toString(); + const data = JSON.parse(messageStr); + // 只处理当前房间的消息 + if (data.roomId !== room.name) return; + + // 忽略自己发送的消息 + if (data.sender === hostUid.value) return; + switch (data.type) { + case VIDEO_ENLARGE_MESSAGE_TYPES.ENLARGE: + handleRemoteVideoEnlarge(data); + break; + case VIDEO_ENLARGE_MESSAGE_TYPES.SHRINK: + handleRemoteVideoShrink(data); + break; + default: + console.warn('未知的视频放大消息类型:', data.type); + } + } catch (error) { + console.error('处理视频放大消息失败:', error); + } +} + +// 处理远程视频放大 +function handleRemoteVideoEnlarge(data) { + const { participant } = data.payload; + + // 确保参与者存在 + if (!remoteParticipants.value.has(participant.identity)) { + console.warn('收到放大消息,但参与者不存在:', participant.identity); + return; + } + // 如果当前正在屏幕共享,自动停止 + if (isScreenSharing.value) { + room.localParticipant.setScreenShareEnabled(false); + isScreenSharing.value = false; + ElMessage.info('其他用户开启了视频放大,已自动停止屏幕共享'); + } + + // 如果当前正在使用白板,自动退出 + if (isWhiteboardActive.value) { + exitWhiteboard(); + ElMessage.info('其他用户开启了视频放大,已自动退出白板'); + } + + // 如果当前正在放大其他用户,先关闭 + if (enlargedParticipant.value && enlargedParticipant.value.identity !== participant.identity) { + closeEnlargedView(); + } + + // 放大远程参与者的视频 + enlargedParticipant.value = { + identity: participant.identity, + hasCameraTrack: participant.hasCameraTrack, + audioEnabled: participant.audioEnabled + }; + + ElMessage.info(`${participant.identity} 放大了自己的视频`); + + // 确保视频轨道已附加 + nextTick(() => { + const participantData = remoteParticipants.value.get(participant.identity); + if (participantData && participantData.cameraTrack && enlargedVideo.value) { + attachTrackToVideo(enlargedVideo.value, participantData.cameraTrack); + } + }); + + // 初始化放大视频激光笔 + nextTick(() => { + initEnlargedLaserPointerCanvas(); + }); +} + +// 处理远程视频缩小 +function handleRemoteVideoShrink(data) { + const { participant } = data.payload; + + // 如果当前放大的正是这个用户,关闭放大视图 + if (enlargedParticipant.value && enlargedParticipant.value.identity === participant.identity) { + enlargedParticipant.value = null; + ElMessage.info(`${participant.identity} 关闭了放大视图`); + if(isLaserPointerActive.value){ + isLaserPointerActive.value = false; + cleanupAllLaserElements(); + } + // 清理放大视频激光笔 + cleanupEnlargedLaserPointer(); + } +} + +//发送视屏缩放消息 +function publishEnlargeVideoMessage(type, payload = {}) { + try { + const message = { + type: type, + roomId: room.name, + sender: hostUid.value, + timestamp: Date.now(), + payload: payload + }; + mqttClient.publish(`xSynergy/enlarge_video/${room.name}`, message); + return true; + } catch (error) { + console.error('发布视频放大消息失败:', error); + return false; + } +} + +// 初始化MQTT连接 文件上传成功 +async function initMqttFileUploadSucc(){ + try { + if (isMqttFileuploadSucc.value) return + const clientId = `fileUpload_${Date.now()}` + await mqttClient.connect(clientId) + isMqttFileuploadSucc.value = true + // 订阅主题 + emitter.emit('subscribeToFileUploadTopic',{roomId:roomId.value}) + } catch (error) { + console.error('MQTT连接失败:', error) + ElMessage.error('文件上传服务连接失败') + } +} + +//文件预览MQTT链接 +async function initMqttFilePreview(){ + try { + if (isMqttFilePreview.value) return + const clientId = `filePreview_${Date.now()}` + await mqttClient.connect(clientId) + isMqttFilePreview.value = true + // 订阅主题 + emitter.emit('subscribeToFilePreviewTopic',{roomId:roomId.value}) + } catch (error) { + console.error('MQTT连接失败:', error) + ElMessage.error('文件上传服务连接失败') + } +} +//订阅文件状态主题 +async function initMqttFileConversionStatus(){ + try { + if (isMqttFileConversionStatus.value) return + const clientId = `fileConversionStatus_${Date.now()}` + await mqttClient.connect(clientId) + isMqttFileConversionStatus.value = true + // 订阅主题 + emitter.emit('subscribeToFileConversionStatusTopic',{roomId:roomId.value}) + } catch (error) { + console.error('MQTT连接失败:', error) + ElMessage.error('文件上传服务连接失败') + } +} + +// 激光笔功能 +function toggleLaserPointer() { + if (!canUseLaserPointer.value) { + ElMessage.warning('请在开启摄像头或屏幕共享时使用激光笔'); + return; + } + // && !isWhiteboardActive.value + if (!hasActiveScreenShare.value && !enlargedParticipant.value) { + ElMessage.warning('请在屏幕共享、放大视图模式下使用激光笔'); + return; + } + + isLaserPointerActive.value = !isLaserPointerActive.value; + if (isLaserPointerActive.value) { + // 根据当前模式初始化对应的激光笔 Canvas + if (enlargedParticipant.value) { + initEnlargedLaserPointerCanvas(); + } else { + initLaserPointerCanvas(); + } ElMessage.success('激光笔已开启,双击添加标记,拖拽绘制线条'); } else { - cleanupLaserPointer(); + cleanupAllLaserElements(); ElMessage.info('激光笔已关闭'); } } -function handleScreenShareLoaded() { - console.log('屏幕共享视频加载完成'); +// 清理所有激光笔元素 +function cleanupAllLaserElements() { + // 清除所有定时器 + laserPointerTimeouts.value.forEach((timeoutId, elementId) => { + clearTimeout(timeoutId); + }); + laserPointerTimeouts.value.clear(); + + // 清空所有元素 + laserPointerElements.value.clear(); + + // 清除画布 + if (laserPointerContext.value && laserPointerCanvas.value) { + laserPointerContext.value.clearRect(0, 0, laserPointerCanvas.value.width, laserPointerCanvas.value.height); + } + + // 清除放大视频画布 + if (enlargedLaserPointerContext.value && enlargedLaserPointerCanvas.value) { + enlargedLaserPointerContext.value.clearRect(0, 0, enlargedLaserPointerCanvas.value.width, enlargedLaserPointerCanvas.value.height); + } + + mouseState.isDrawing = false; + enlargedMouseState.isDrawing = false; +} + +// 添加新的激光笔元素 +function addLaserElement(element) { + const elementId = generateElementId(); + element.id = elementId; + element.timestamp = Date.now(); + + laserPointerElements.value.set(elementId, element); + + // 为每个元素设置独立的定时器 + const timeoutId = setTimeout(() => { + removeLaserElement(elementId); + }, laserPointerConfig.duration); + + laserPointerTimeouts.value.set(elementId, timeoutId); + + // 重新绘制所有元素 + redrawLaserElements(); + redrawEnlargedLaserElements(); + + return elementId; +} + +// 移除单个激光笔元素 +function removeLaserElement(elementId) { + // 清除定时器 + if (laserPointerTimeouts.value.has(elementId)) { + clearTimeout(laserPointerTimeouts.value.get(elementId)); + laserPointerTimeouts.value.delete(elementId); + } + + // 移除元素 + laserPointerElements.value.delete(elementId); + + // 重新绘制 + redrawLaserElements(); + redrawEnlargedLaserElements(); +} + +function handleScreenShareLoaded() { // 视频加载完成后初始化激光笔 Canvas initLaserPointerCanvas(); } @@ -419,21 +1171,65 @@ function handleScreenShareLoaded() { // 初始化激光笔 Canvas function initLaserPointerCanvas() { if (!laserPointerCanvas.value) return; - const videoWrapper = document.querySelector('.screen-share-wrapper'); - if (!videoWrapper) return; - const rect = videoWrapper.getBoundingClientRect(); - // 设置 Canvas 尺寸 - laserPointerCanvas.value.width = rect.width; - laserPointerCanvas.value.height = rect.height; + + let targetElement; + if (hasActiveScreenShare.value) { + targetElement = document.querySelector('.screen-share-element'); + screenShareVideoElement.value = targetElement; + } else if (enlargedParticipant.value) { + targetElement = document.querySelector('.enlarged-video-element'); + screenShareVideoElement.value = targetElement; + } + + if (!targetElement) return; + + // 设置 Canvas 尺寸与目标元素一致 + updateCanvasSize(targetElement); + // 获取上下文 laserPointerContext.value = laserPointerCanvas.value.getContext('2d'); + // 设置 Canvas 样式 laserPointerCanvas.value.style.position = 'absolute'; laserPointerCanvas.value.style.top = '0'; laserPointerCanvas.value.style.left = '0'; laserPointerCanvas.value.style.zIndex = '10'; - laserPointerCanvas.value.style.cursor = 'crosshair'; + laserPointerCanvas.value.style.cursor = isLaserPointerActive.value ? 'crosshair' : 'default'; laserPointerCanvas.value.style.pointerEvents = isLaserPointerActive.value ? 'auto' : 'none'; + + // 初始化观察器监听目标元素大小变化 + initResizeObserver(targetElement); +} + +function updateCanvasSize(targetElement) { + if (!laserPointerCanvas.value || !targetElement) return; + + const rect = targetElement.getBoundingClientRect(); + laserPointerCanvas.value.width = rect.width; + laserPointerCanvas.value.height = rect.height; + laserPointerCanvas.value.style.width = `${rect.width}px`; + laserPointerCanvas.value.style.height = `${rect.height}px`; + + // 重新绘制所有元素 + redrawLaserElements(); +} + +// 初始化 ResizeObserver 监听元素大小变化 +function initResizeObserver(targetElement) { + // 先清理之前的观察器 + if (canvasSizeObserver.value) { + canvasSizeObserver.value.disconnect(); + } + + canvasSizeObserver.value = new ResizeObserver((entries) => { + for (let entry of entries) { + if (entry.target === targetElement) { + updateCanvasSize(targetElement); + } + } + }); + + canvasSizeObserver.value.observe(targetElement); } // 清理激光笔 @@ -447,10 +1243,12 @@ function cleanupLaserPointer() { laserPointerContext.value.clearRect(0, 0, laserPointerCanvas.value.width, laserPointerCanvas.value.height); } - laserPointerElements.value = []; - mouseState.isDrawing = false; - mouseState.currentPath = []; + // laserPointerElements.value = []; + // 清空所有元素 + laserPointerElements.value.clear(); + mouseState.isDrawing = false; } + // 获取鼠标坐标(转换为百分比坐标) function getMouseCoordinates(e) { if (!laserPointerCanvas.value) return { x: 0, y: 0 }; @@ -461,6 +1259,7 @@ function getMouseCoordinates(e) { y: ((e.clientY - rect.top) / laserPointerCanvas.value.height).toFixed(4) }; } + // 获取实际像素坐标 function getPixelCoordinates(e) { if (!laserPointerCanvas.value) return { x: 0, y: 0 }; @@ -473,136 +1272,91 @@ function getPixelCoordinates(e) { } // Canvas 事件处理 双击 -function handleCanvasDoubleClick(e) { - if (!isLaserPointerActive.value) return; - const coords = getMouseCoordinates(e); - const pixelCoords = getPixelCoordinates(e); - // 创建圆形标记 +function handleCanvasDoubleClick(e) { + if (!isLaserPointerActive.value) return; + const coords = getMouseCoordinates(e); // 使用归一化坐标 + // 创建圆形标记元素 - 只存储归一化坐标 const circleElement = { type: 'circle', data: { color: laserPointerConfig.color, - start: coords, - thickness: laserPointerConfig.thickness, - pixelCoords: pixelCoords, - timestamp: Date.now() + center: coords, + thickness: laserPointerConfig.thickness } }; - laserPointerElements.value.push(circleElement); - drawCircle(pixelCoords); - // 发送到其他用户(如果需要) - publishLaserPointerData(circleElement); - // 2秒后清除 - scheduleCleanup(); + + const elementId = addLaserElement(circleElement); + publishLaserPointerData({ + ...circleElement, + id: elementId + }); } function handleCanvasMouseDown(e) { if (!isLaserPointerActive.value) return; const pixelCoords = getPixelCoordinates(e); - const percentageCoords = getMouseCoordinates(e); - mouseState.isDrawing = true; mouseState.startX = pixelCoords.x; mouseState.startY = pixelCoords.y; - mouseState.lastX = pixelCoords.x; - mouseState.lastY = pixelCoords.y; - - // 初始化当前路径,添加第一个点 - mouseState.currentPath = [{ - x: percentageCoords.x, - y: percentageCoords.y - }]; } function handleCanvasMouseMove(e) { - if (!isLaserPointerActive.value || !mouseState.isDrawing) return; - - const pixelCoords = getPixelCoordinates(e); - const percentageCoords = getMouseCoordinates(e); - - // 添加当前点到路径 - mouseState.currentPath.push({ - x: percentageCoords.x, - y: percentageCoords.y - }); - - // 实时绘制临时线条 - drawLine(mouseState.lastX, mouseState.lastY, pixelCoords.x, pixelCoords.y); - - mouseState.lastX = pixelCoords.x; - mouseState.lastY = pixelCoords.y; + } function handleCanvasMouseUp(e) { if (!isLaserPointerActive.value || !mouseState.isDrawing) return; - const percentageCoords = getMouseCoordinates(e); + const endCoords = getMouseCoordinates(e); + const startCoords = getMouseCoordinates({ + clientX: mouseState.startX + laserPointerCanvas.value.getBoundingClientRect().left, + clientY: mouseState.startY + laserPointerCanvas.value.getBoundingClientRect().top + }); - // 添加最后一个点到路径 - if (mouseState.currentPath.length > 0) { - const lastPoint = mouseState.currentPath[mouseState.currentPath.length - 1]; - // 如果最后一个点不是当前点,则添加当前点 - if (lastPoint.x !== percentageCoords.x || lastPoint.y !== percentageCoords.y) { - mouseState.currentPath.push({ - x: percentageCoords.x, - y: percentageCoords.y - }); - } - } - - // 创建铅笔元素 - const pencilElement = { - type: 'pencil', + // 创建线条元素 + const lineElement = { + type: 'line', data: { color: laserPointerConfig.color, - path: [...mouseState.currentPath], // 复制路径数组 - thickness: laserPointerConfig.thickness, - timestamp: Date.now() + start: startCoords, + end: endCoords, + thickness: laserPointerConfig.thickness } }; - // 只有当路径有足够多的点时才保存和发送 - if (pencilElement.data.path.length >= 2) { - laserPointerElements.value.push(pencilElement); - - // 重新绘制所有元素(包括新的铅笔路径) - redrawLaserElements(); - - // 发送到其他用户 - publishLaserPointerData(pencilElement); - - // 安排清理 - scheduleCleanup(); + const startPixel = getPixelFromPercentage(lineElement.data.start); + const endPixel = getPixelFromPercentage(lineElement.data.end); + + // 只有当起点和终点不同时才创建线条 + if (!simpleDeepEqual(lineElement.data.start, lineElement.data.end)) { + const elementId = addLaserElement(lineElement); + publishLaserPointerData({ + ...lineElement, + id: elementId + }); } - // 重置绘图状态 mouseState.isDrawing = false; - mouseState.currentPath = []; } function handleCanvasMouseLeave() { - if (mouseState.isDrawing) { - // 如果正在绘制,完成当前路径 - handleCanvasMouseUp(new MouseEvent('mouseup')); - } mouseState.isDrawing = false; } // 绘制函数 -function drawCircle(coords) { +function drawCircle(centerPixel) { if (!laserPointerContext.value) return; const ctx = laserPointerContext.value; ctx.beginPath(); - ctx.arc(coords.x, coords.y, 2, 0, Math.PI * 2); + ctx.arc(centerPixel.x, centerPixel.y, laserPointerConfig.thickness, 0, Math.PI * 2); ctx.fillStyle = laserPointerConfig.color; ctx.fill(); - // ctx.strokeStyle = '#ffffff'; - // ctx.lineWidth = 2; ctx.stroke(); -} +} function drawLine(startX, startY, endX, endY) { - if (!laserPointerContext.value) return; + if (!laserPointerContext.value) return; + const ctx = laserPointerContext.value; ctx.beginPath(); ctx.moveTo(startX, startY); @@ -612,30 +1366,6 @@ function drawLine(startX, startY, endX, endY) { ctx.lineCap = 'round'; ctx.stroke(); } - -// 绘制铅笔路径函数 -function drawPencilPath(pencilData) { - if (!laserPointerContext.value || !pencilData.path || pencilData.path.length < 2) return; - - const ctx = laserPointerContext.value; - - // 将百分比坐标转换为像素坐标 - const pixelPath = pencilData.path.map(pt => ({ - x: parseFloat(pt.x) * laserPointerCanvas.value.width, - y: parseFloat(pt.y) * laserPointerCanvas.value.height - })); - - // 设置绘制样式 - ctx.strokeStyle = pencilData.color; - ctx.lineWidth = pencilData.thickness; - ctx.lineCap = 'round'; - ctx.lineJoin = 'round'; - - // 绘制平滑曲线 - drawSmoothCurve(ctx, pixelPath); - ctx.stroke(); -} - // 重绘所有元素 function redrawLaserElements() { if (!laserPointerContext.value || !laserPointerCanvas.value) return; @@ -643,14 +1373,15 @@ function redrawLaserElements() { const ctx = laserPointerContext.value; ctx.clearRect(0, 0, laserPointerCanvas.value.width, laserPointerCanvas.value.height); - const now = Date.now(); + // 绘制所有活跃的元素 laserPointerElements.value.forEach(element => { - if (now - element.data.timestamp < laserPointerConfig.duration) { - if (element.type === 'circle' && element.data.pixelCoords) { - drawCircle(element.data.pixelCoords); - } else if (element.type === 'pencil') { - drawPencilPath(element.data); - } + if (element.type === 'circle') { + const centerPixel = getPixelFromPercentage(element.data.center); + drawCircle(centerPixel); + } else if (element.type === 'line') { + const startPixel = getPixelFromPercentage(element.data.start); + const endPixel = getPixelFromPercentage(element.data.end); + drawLine(startPixel.x, startPixel.y, endPixel.x, endPixel.y); } }); } @@ -660,8 +1391,8 @@ function getPixelFromPercentage(percentageCoords) { if (!laserPointerCanvas.value) return { x: 0, y: 0 }; return { - x: percentageCoords.x * laserPointerCanvas.value.width, - y: percentageCoords.y * laserPointerCanvas.value.height + x: parseFloat(percentageCoords.x) * laserPointerCanvas.value.width, + y: parseFloat(percentageCoords.y) * laserPointerCanvas.value.height }; } @@ -689,19 +1420,33 @@ function scheduleCleanup() { function resizeLaserPointerCanvas() { if (!isLaserPointerActive.value || !laserPointerCanvas.value) return; - const videoWrapper = document.querySelector('.screen-share-wrapper'); - if (!videoWrapper) return; + let targetElement; + if (hasActiveScreenShare.value) { + targetElement = document.querySelector('.screen-share-element'); + } else if (enlargedParticipant.value) { + targetElement = document.querySelector('.enlarged-video-element'); + } - const rect = videoWrapper.getBoundingClientRect(); - laserPointerCanvas.value.width = rect.width; - laserPointerCanvas.value.height = rect.height; + if (targetElement) { + updateCanvasSize(targetElement); + } +} + +// 响应式调整放大视频 Canvas 大小 +function resizeEnlargedLaserPointerCanvas() { + if (!isLaserPointerActive.value || !enlargedLaserPointerCanvas.value || !enlargedParticipant.value) return; - // 重新绘制所有元素 - redrawLaserElements(); + const targetElement = document.querySelector('.enlarged-video-element'); + if (targetElement) { + updateEnlargedCanvasSize(targetElement); + } } // 监听窗口大小变化 -window.addEventListener('resize', resizeLaserPointerCanvas); +window.addEventListener('resize', () => { + resizeLaserPointerCanvas(); + resizeEnlargedLaserPointerCanvas(); +}); // 在初始化 MQTT 时订阅激光笔主题 function subscribeToLaserPointerTopic() { @@ -711,6 +1456,7 @@ function subscribeToLaserPointerTopic() { console.error('订阅激光笔主题失败:', error); } } + // 发布激光笔数据 function publishLaserPointerData(element) { try { @@ -737,18 +1483,15 @@ function handleLaserPointerMessage(payload, topic) { if (data.roomId !== room.name) return; // 忽略自己发送的消息 - if (data.sender === hostUid.value) return; + if (data.sender === hostUid.value) return; switch (data.type) { case LASER_POINTER_MESSAGE_TYPES.DRAW: - // 添加远程用户的激光笔绘制 - laserPointerElements.value.push(data.element); - redrawLaserElements(); - scheduleCleanup(); + handleRemoteLaserDraw(data); break; case LASER_POINTER_MESSAGE_TYPES.CLEAR: - cleanupLaserPointer(); + cleanupAllLaserElements(); break; } } catch (error) { @@ -756,6 +1499,33 @@ function handleLaserPointerMessage(payload, topic) { } } +function handleRemoteLaserDraw(data) { + const { element } = data; + + // 如果元素已存在,更新它;否则添加新元素 + if (element.id && laserPointerElements.value.has(element.id)) { + laserPointerElements.value.set(element.id, element); + } else { + // 为远程元素生成新的ID(避免ID冲突) + const newElementId = generateElementId(); + element.id = newElementId; + element.timestamp = Date.now(); + + laserPointerElements.value.set(newElementId, element); + + // 为远程元素设置定时器 + const timeoutId = setTimeout(() => { + removeLaserElement(newElementId); + }, laserPointerConfig.duration); + + laserPointerTimeouts.value.set(newElementId, timeoutId); + } + + // 重新绘制 + redrawLaserElements(); + redrawEnlargedLaserElements(); +} + //文件上传 async function fileUploadHandle(){ fileListRef.value.show() @@ -774,7 +1544,7 @@ async function handleCameraVisibleChange(e){ // 重要:立即停止临时媒体流,避免占用摄像头 stream.getTracks().forEach(track => { track.stop(); - console.log('临时摄像头轨道已停止'); + // console.log('临时摄像头轨道已停止'); }); } } catch (error) { @@ -785,6 +1555,7 @@ async function handleCameraVisibleChange(e){ cameraDevices.value = []; } } + // 处理摄像头设备选择 async function handleCameraCommand(deviceId) { if (deviceId === 'refresh') { @@ -939,8 +1710,7 @@ async function initMqtt() { try { // 使用随机客户端ID连接 const clientId = `whiteboard_${Date.now()}`; - await mqttClient.connect(clientId); - console.log('MQTT连接成功,客户端ID:', clientId); + await mqttClient.connect(clientId); // 订阅白板主题 subscribeToWhiteboardTopic(); } catch (error) { @@ -957,13 +1727,13 @@ function subscribeToWhiteboardTopic() { console.error('订阅白板主题失败:', error); } } + // 初始化激光笔MQTT连接 async function initToLaserPointerMqtt() { try { // 使用随机客户端ID连接 const clientId = `toLaserPointer_${Date.now()}`; - await mqttClient.connect(clientId); - console.log('MQTT连接(toLaserPointer)成功,客户端ID:', clientId); + await mqttClient.connect(clientId); //订阅激光笔主题 subscribeToLaserPointerTopic() } catch (error) { @@ -1008,16 +1778,18 @@ function handleRemoteWhiteboardOpen(data) { room.localParticipant.setScreenShareEnabled(false); isScreenSharing.value = false; } + // 如果正在放大视图,自动关闭 + if (enlargedParticipant.value) { + closeEnlargedView(); + } } // 处理远程关闭白板 -function handleRemoteWhiteboardClose(data) { - console.log('关闭白板') - ElMessage.info(`${data.senderName || data.sender} 关闭了白板`); - console.log('data',data) - if(data.roomType == '1'){ - isWhiteboardActive.value = false; - } +function handleRemoteWhiteboardClose(data) { + ElMessage.info(`${data.senderName || data.sender} 关闭了白板`); + // if(data.roomType == '1'){ + // isWhiteboardActive.value = false; + // } } // 处理白板同步消息 @@ -1033,12 +1805,11 @@ function handleWhiteboardSync(data) { //邀请进入房间 async function inviterJoinRoom(){ - inviterJoinRoomRef.value.show() + inviterJoinRoomRef.value.show(roomId.value) } // 确认选择房间 -async function handleConfirmSelection(userInfo){ - console.log(userInfo,'加入房间人员信息') +async function handleConfirmSelection(userInfo){ if(userInfo.length < 0){ ElMessage.error('请选择加入房间的人员') return @@ -1058,8 +1829,7 @@ function publishWhiteboardMessage(type, payload = {}) { timestamp: Date.now(), payload }; - mqttClient.publish(`xSynergy/shareWhiteboard/${room.name}`, message); - console.log('白板消息发布成功:', type); + mqttClient.publish(`xSynergy/shareWhiteboard/${room.name}`, message); return true; } catch (error) { console.error('发布白板消息失败:', error); @@ -1069,10 +1839,16 @@ function publishWhiteboardMessage(type, payload = {}) { } async function toggleWhiteboard() { - if(hasActiveScreenShare.value){ - ElMessage.error('请先关闭屏幕共享'); - return; - } + // if(cameraEnabled.value) { + // ElMessage.error('请先关闭摄像头才能使用白板'); + // return; + // } + + // if(hasActiveScreenShare.value){ + // ElMessage.error('请先关闭屏幕共享'); + // return; + // } + // roomId.value = room.name if (isWhiteboardActive.value) { // 如果白板已经激活,点击则退出白板 @@ -1091,6 +1867,11 @@ async function startWhiteboard() { isScreenSharing.value = false; ElMessage.info('已停止屏幕共享,开启白板'); } + // 如果正在放大视图,先关闭 + if (enlargedParticipant.value) { + closeEnlargedView(); + } + // 激活白板状态 isWhiteboardActive.value = true; const success = publishWhiteboardMessage(WHITEBOARD_MESSAGE_TYPES.OPEN, { @@ -1111,14 +1892,6 @@ async function startWhiteboard() { async function exitWhiteboard() { try { - // 确认退出 - await ElMessageBox.confirm('确定要退出白板吗?', '提示', - { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning' - } - ); // 发布关闭白板消息 publishWhiteboardMessage(WHITEBOARD_MESSAGE_TYPES.CLOSE, { action: 'close', @@ -1127,7 +1900,7 @@ async function exitWhiteboard() { }); // 执行退出白板逻辑 isWhiteboardActive.value = false; - // 可以在这里添加白板清理逻辑 + // 白板清理 if (whiteboardRef.value && whiteboardRef.value.cleanup) { whiteboardRef.value.cleanup(); } @@ -1207,11 +1980,15 @@ function setupRoomListeners() { // 事件处理函数 async function handleConnected() { - await initMqtt(); - await initToLaserPointerMqtt() roomId.value = room.name + await initMqtt(); + await initToLaserPointerMqtt(); + await initMqttFileUploadSucc() ; + await initMqttFileConversionStatus(); + await initMqttFilePreview(); + await initMqttEnlargeVideo(); status.value = false; - ElMessage.success('已成功连接到房间'); + ElMessage.success('已成功连接到房间'); // 初始化现有远程参与者 room.remoteParticipants.forEach(participant => { addRemoteParticipant(participant); @@ -1231,15 +2008,17 @@ function handleDisconnected(reason) { audioElementsMap.value.clear(); screenSharingUser.value = ''; activeScreenShareTrack.value = null; + enlargedParticipant.value = null; ElMessage.error('连接已断开'); } // 处理轨道订阅事件 function handleTrackSubscribed(track, publication, participant) { if (track) { - if (track.kind === Track.Kind.Video) { + if (track.kind === Track.Kind.Video) { // 更新参与者轨道信息 updateParticipantTrack(participant, publication.source, track); + // 如果是屏幕共享,更新屏幕共享状态 if (publication.source === Track.Source.ScreenShare) { updateScreenShareState(participant, track); @@ -1247,8 +2026,10 @@ function handleTrackSubscribed(track, publication, participant) { globalScreenSharingUser.value = participant.identity; isGlobalScreenSharing.value = true; } + // 立即附加到视频元素(如果元素已存在) attachTrackToParticipantVideo(participant.identity, publication.source, track); + } else if (track.kind === Track.Kind.Audio) { // 处理音频轨道订阅 updateParticipantAudioTrack(participant, track); @@ -1265,21 +2046,27 @@ function handleTrackUnsubscribed(track, publication, participant) { removeParticipantTrack(participant, publication.source); // 清理视频元素 detachTrackFromParticipantVideo(participant.identity, publication.source); + // 如果是屏幕共享,更新屏幕共享状态 if (publication.source === Track.Source.ScreenShare) { updateScreenShareState(participant, null); - // 清除全局屏幕共享状态 + // 清除全局屏幕共享状态 if (globalScreenSharingUser.value === participant.identity) { globalScreenSharingUser.value = ''; isGlobalScreenSharing.value = false; } } + + // 重要:立即更新参与者轨道状态 + updateParticipantTracks(participant); + } else if (track.kind === Track.Kind.Audio) { // 处理音频轨道取消订阅 removeParticipantAudioTrack(participant); detachTrackFromParticipantAudio(participant.identity); + // 立即更新参与者轨道状态 + updateParticipantTracks(participant); } - updateParticipantTracks(participant); } function handleParticipantConnected(participant) { @@ -1304,6 +2091,12 @@ function handleParticipantDisconnected(participant) { globalScreenSharingUser.value = ''; isGlobalScreenSharing.value = false; } + + // 如果离开的用户是当前放大的用户,关闭放大视图 + if (enlargedParticipant.value && enlargedParticipant.value.identity === participant.identity) { + closeEnlargedView(); + } + removeRemoteParticipant(participant); ElMessage.info(`用户离开: ${participant.identity}`); } @@ -1327,7 +2120,7 @@ function handleLocalTrackPublished(publication) { } else if (publication.kind === Track.Kind.Audio) { // 本地音频轨道发布 microphoneEnabled.value = true; - console.log('本地音频轨道已发布'); + // console.log('本地音频轨道已发布'); } } } @@ -1369,20 +2162,27 @@ function handleTrackUnmuted(publication, participant) { // 设置参与者事件监听器 function setupParticipantListeners(participant) { participant - .on(ParticipantEvent.TrackSubscribed, (track, publication) => { + .on(ParticipantEvent.TrackSubscribed, (track, publication) => { handleTrackSubscribed(track, publication, participant); }) - .on(ParticipantEvent.TrackUnsubscribed, (track, publication) => { + .on(ParticipantEvent.TrackUnsubscribed, (track, publication) => { handleTrackUnsubscribed(track, publication, participant); }) - .on(ParticipantEvent.TrackMuted, (publication) => { + .on(ParticipantEvent.TrackMuted, (publication) => { handleTrackMuted(publication, participant); }) - .on(ParticipantEvent.TrackUnmuted, (publication) => { + .on(ParticipantEvent.TrackUnmuted, (publication) => { handleTrackUnmuted(publication, participant); }) .on(ParticipantEvent.IsSpeakingChanged, (speaking) => { updateParticipantSpeaking(participant, speaking); + }) + // 添加轨道发布状态变化监听 + .on(ParticipantEvent.TrackPublished, (publication) => { + updateParticipantTracks(participant); + }) + .on(ParticipantEvent.TrackUnpublished, (publication) => { + updateParticipantTracks(participant); }); } @@ -1439,8 +2239,7 @@ function addRemoteParticipant(participant) { videoEnabled: participant.isCameraEnabled, isSpeaking: false }; - remoteParticipants.value.set(participant.identity, participantData); - console.log("添加远程参与者:", participant.identity); + remoteParticipants.value.set(participant.identity, participantData); } function removeRemoteParticipant(participant) { @@ -1570,13 +2369,15 @@ function detachTrackFromParticipantAudio(identity) { function updateParticipantTracks(participant) { const data = remoteParticipants.value.get(participant.identity); if (!data) return; - // 检查视频轨道状态 + + // 重置轨道状态 let hasCamera = false; let hasScreen = false; - let hasAudio = false; - // 检查已发布的轨道 + let hasAudio = false; + + // 检查已发布的轨道 - 只统计已订阅且未静音的轨道 participant.videoTrackPublications.forEach(publication => { - if (publication.isSubscribed && publication.track) { + if (publication.isSubscribed && publication.track && !publication.isMuted) { if (publication.source === Track.Source.Camera) { hasCamera = true; // 确保轨道信息更新 @@ -1592,9 +2393,9 @@ function updateParticipantTracks(participant) { } }); - // 检查音频轨道 + // 检查音频轨道 - 只统计已订阅且未静音的轨道 participant.audioTrackPublications.forEach(publication => { - if (publication.isSubscribed && publication.track) { + if (publication.isSubscribed && publication.track && !publication.isMuted) { hasAudio = true; if (!data.audioTrack) { data.audioTrack = publication.track; @@ -1602,11 +2403,28 @@ function updateParticipantTracks(participant) { } }); + // 更新数据 data.hasCameraTrack = hasCamera; data.hasScreenTrack = hasScreen; data.audioEnabled = hasAudio; data.videoEnabled = participant.isCameraEnabled; - remoteParticipants.value.set(participant.identity, { ...data }); + + // 如果没有摄像头轨道,清空相关数据 + if (!hasCamera) { + data.cameraTrack = null; + } + + // 如果没有屏幕共享轨道,清空相关数据 + if (!hasScreen) { + data.screenTrack = null; + } + + // 如果没有音频轨道,清空相关数据 + if (!hasAudio) { + data.audioTrack = null; + } + + remoteParticipants.value.set(participant.identity, { ...data }); } function updateParticipantSpeaking(participant, speaking) { @@ -1625,6 +2443,11 @@ function attachLocalVideoTrack(track) { mediaStream.addTrack(track.mediaStreamTrack); localVideo.value.srcObject = mediaStream; cameraEnabled.value = true; + + // 确保视频可以播放 + localVideo.value.play().catch(error => { + console.warn('本地视频播放失败:', error); + }); } catch (error) { console.error('附加本地视频轨道失败:', error); } @@ -1670,6 +2493,11 @@ async function toggleCamera() { // 清空选中的视屏设备ID selectedCameraId.value = ''; ElMessage.info('摄像头已关闭'); + if (enlargedParticipant.value && enlargedParticipant.value.identity === room.localParticipant.identity) { + closeEnlargedView(); + } + // 强制更新一次本地参与者状态 + updateParticipantTracks(room.localParticipant); } else { // 确保视频元素存在 if (!localVideo.value) { @@ -1683,10 +2511,11 @@ async function toggleCamera() { await handleCameraVisibleChange(true); } if (cameraDevices.value.length === 0) { - // ElMessage.error('未找到可用的摄像头设备'); + ElMessage.error('未找到可用的摄像头设备'); return; } - // 自动选择第一个可用设备(如果当前没有选中设备) + + // 自动选择第一个可用设备(如果当前没有选中设备) let deviceToUse = selectedCameraId.value; if (!deviceToUse && cameraDevices.value.length > 0) { deviceToUse = cameraDevices.value[0].deviceId; @@ -1698,21 +2527,47 @@ async function toggleCamera() { ElMessage.success(`摄像头已开启 - ${getDeviceName(cameraDevices.value, deviceToUse)}`); } else { // 使用默认方式开启 - await room.localParticipant.setMicrophoneEnabled(true); - microphoneEnabled.value = true; + await room.localParticipant.setCameraEnabled(true); + cameraEnabled.value = true; // 手动获取并附加视频轨道 setTimeout(() => { attachLocalCameraTrack(); }, 200); - ElMessage.success('麦克风已开启'); - } - ElMessage.success('摄像头已开启'); + ElMessage.success('摄像头已开启'); + } + + // 重要:确保轨道正确发布给其他用户 + await ensureCameraTrackPublished(); } } catch (error) { errorHandling(error,'摄像头'); } } +// 确保摄像头轨道正确发布 +async function ensureCameraTrackPublished() { + try { + // 等待轨道发布 + await new Promise(resolve => setTimeout(resolve, 500)); + + const videoPublications = Array.from(room.localParticipant.videoTrackPublications.values()); + const cameraPublication = videoPublications.find(pub => + pub.source === Track.Source.Camera && pub.track + ); + + if (cameraPublication) { + // 如果轨道存在但未正确附加,重新附加 + if (cameraPublication.track && localVideo.value) { + attachLocalVideoTrack(cameraPublication.track); + } + } else { + console.warn('未找到摄像头发布轨道'); + } + } catch (error) { + console.error('确保摄像头轨道发布失败:', error); + } +} + async function attachLocalCameraTrack() { try { // 等待一小段时间确保轨道已经创建 @@ -1797,6 +2652,11 @@ async function toggleMicrophone() { async function toggleScreenShare() { try { + // 检查是否处于放大视频模式 + if (enlargedParticipant.value) { + ElMessage.error('当前处于视频放大模式,无法进行屏幕共享'); + return; + } if(isWhiteboardActive.value){ ElMessage.error('请先关闭白板'); return; @@ -1828,8 +2688,7 @@ async function toggleScreenShare() { } } -function handleScreenShareEnded() { - console.log('用户通过浏览器控件停止了屏幕共享'); +function handleScreenShareEnded() { isScreenSharing.value = false; ElMessage.info('屏幕共享已停止'); // 移除事件监听器 @@ -1861,6 +2720,14 @@ async function joinRoomBtn() { // 离开会议函数 async function leaveRoom() { try { + // 如果正在放大视频,发送缩小消息 + if (enlargedParticipant.value && enlargedParticipant.value.identity === hostUid.value) { + publishEnlargeVideoMessage(VIDEO_ENLARGE_MESSAGE_TYPES.SHRINK, { + participant: { + identity: hostUid.value + } + }); + } // 如果白板正在运行,先退出白板 if (isWhiteboardActive.value) { publishWhiteboardMessage(WHITEBOARD_MESSAGE_TYPES.CLOSE, { @@ -1891,10 +2758,7 @@ async function leaveRoom() { await room.disconnect(); // 重置所有状态 resetRoomState(); - ElMessage.success('已离开会议'); - router.push({ - path: '/coordinate', - }) + ElMessage.success('已离开会议'); } catch (error) { console.error('离开会议失败:', error); @@ -1902,6 +2766,13 @@ async function leaveRoom() { } } +async function leaveRoomHandle(){ + await leaveRoom() + router.push({ + path: '/coordinate', + }) +} + // 重置房间状态函数 function resetRoomState() { // 重置本地状态 @@ -1918,6 +2789,7 @@ function resetRoomState() { selectedCameraId.value = '' microphoneDevices.value = []; cameraDevices.value = []; + enlargedParticipant.value = null; // 断开MQTT连接 mqttClient.disconnect(); @@ -1973,13 +2845,60 @@ watch([() => hasActiveScreenShare.value, () => isScreenSharing.value], ([newHasA if (!newHasActiveScreenShare && !newIsScreenSharing) { // 检查激光笔是否处于开启状态 if (isLaserPointerActive.value) { - console.log('屏幕共享已结束,自动关闭激光笔'); + // console.log('屏幕共享已结束,自动关闭激光笔'); // 自动关闭激光笔 closeLaserPointer(); } } }, { immediate: true }); +// 在摄像头关闭时,如果正在放大自己的视频,自动关闭放大视图 +watch(cameraEnabled, (newVal) => { + if (!newVal) { + // 摄像头关闭时,如果正在放大自己的视频,自动关闭 + if (enlargedParticipant.value && enlargedParticipant.value.identity === hostUid.value) { + closeEnlargedView(); + } + } +}); + +watch([hasActiveScreenShare, enlargedParticipant], ([newHasActiveScreenShare, newEnlargedParticipant]) => { + if (isLaserPointerActive.value) { + // 延迟执行,确保 DOM 已更新 + nextTick(() => { + initLaserPointerCanvas(); + }); + } +}); +// 在激光笔激活时初始化 Canvas +watch(isLaserPointerActive, (newVal) => { + if (newVal) { + nextTick(() => { + if (enlargedParticipant.value) { + initEnlargedLaserPointerCanvas(); + } else { + initLaserPointerCanvas(); + } + }); + } else { + // 清理观察器 + if (canvasSizeObserver.value) { + canvasSizeObserver.value.disconnect(); + canvasSizeObserver.value = null; + } + } +}); + +// 监听放大视频状态变化,初始化激光笔 +watch(enlargedParticipant, (newVal) => { + if (newVal && isLaserPointerActive.value) { + nextTick(() => { + initEnlargedLaserPointerCanvas(); + }); + } else { + cleanupEnlargedLaserPointer(); + } +}); // 添加专门的关闭激光笔函数 function closeLaserPointer() { isLaserPointerActive.value = false; @@ -1997,23 +2916,27 @@ function closeLaserPointer() { // 在组件卸载时也清理资源 onUnmounted(() => { window.removeEventListener('resize', resizeLaserPointerCanvas); - cleanupLaserPointer(); + window.removeEventListener('resize', resizeEnlargedLaserPointerCanvas); + if (canvasSizeObserver.value) { + canvasSizeObserver.value.disconnect(); + } + cleanupAllLaserElements(); if (isWhiteboardActive.value && whiteboardRef.value && whiteboardRef.value.cleanup) { whiteboardRef.value.cleanup(); } - // 清理MQTT - mqttClient.disconnect(); + mqttClient.disconnect(); if (room && room.state === 'connected') { leaveRoom(); } }); - onMounted(async () => { if(route.query.type == '1'){ await joinRoomBtn() hostUid.value = roomStore.userUid // 邀请用户参与房间 - await getInvite(room.name,{user_uids:[roomStore.detailUid], participant_role: "participant"}) + if(room.name){ + await getInvite(room.name,{user_uids:[roomStore.detailUid], participant_role: "participant"}) + } } else { const res = await getTokenApi(route.query.room_uid) if(res.meta.code == 200){ @@ -2033,6 +2956,96 @@ onMounted(async () => { \ No newline at end of file diff --git a/src/views/conferencingRoom/transits.vue b/src/views/conferencingRoom/transits.vue new file mode 100644 index 0000000..ef99f77 --- /dev/null +++ b/src/views/conferencingRoom/transits.vue @@ -0,0 +1,3326 @@ + + + + + \ No newline at end of file diff --git a/src/views/coordinate/personnelList/components/leftTab/index.vue b/src/views/coordinate/personnelList/components/leftTab/index.vue index 1834dd9..4c345a2 100644 --- a/src/views/coordinate/personnelList/components/leftTab/index.vue +++ b/src/views/coordinate/personnelList/components/leftTab/index.vue @@ -235,7 +235,7 @@ const searchList = () => { state.dataList = [] getList() } else { - console.log('treeRef.filter',state.treeRef) + // console.log('treeRef.filter',state.treeRef) state.treeRef.filter(state.queryFrom.nickName) } } @@ -304,26 +304,47 @@ const getList = async () => { /** * 通讯录 人员信息树 */ -const HandleLoadNode = async (node, resolve) => { +const HandleLoadNode = async (node, resolve) => { if(node?.level === 0){ - loadNode(resolve) - }else if(node?.level === 1){ - loadNode(resolve,node.data.directory_uid) + loadNode(resolve,'',node?.level) + }else if(node?.level > 0){ + if(node.data.directory_uid){ + loadUserNode(resolve,node.data.directory_uid,node?.level) + }else{ + resolve(resolve) + } + } } -const loadNode = async(resolve,id)=>{ +const loadNode = async(resolve,id,level)=>{ try { - state.leftListLoading = true - if(!id){ - let res = await getDirectories({level:1}) - if(res.meta.code == 200){ - resolve(res.data) - } - }else{ - let res = await getDirectoriesUsers(id,{directory_uuid:id}) - resolve(res.data) + state.leftListLoading = true + let res = await getDirectories({level:1}) + if(res.meta.code == 200){ + resolve(res.data) + } + state.leftListLoading = false + } catch (error) { + console.log(error) + state.leftListLoading = false + } +} + +const loadUserNode = async(resolve,id,level)=>{ + try { + state.leftListLoading = true + let userData = [] + let orgData = [] + const resOrg = await getDirectories({level: 1,parent_uuid:id}) + if(resOrg?.data){ + orgData = resOrg.data } + if(id){ + const res = await getDirectoriesUsers(id,{directory_uuid:id}) + userData = res.data + } + resolve([...orgData, ...userData]) state.leftListLoading = false } catch (error) { console.log(error) diff --git a/src/views/coordinate/personnelList/index.vue b/src/views/coordinate/personnelList/index.vue index fd11b4f..e354644 100644 --- a/src/views/coordinate/personnelList/index.vue +++ b/src/views/coordinate/personnelList/index.vue @@ -447,7 +447,7 @@ onMounted(async () => { const res = await userStore.getInfo() const topic = `xSynergy/ROOM/+/rooms/${res.uid}`; mqttClient.subscribe(topic, async (shapeData) => { - console.log(shapeData.toString(),'shapeData发送邀请') + // console.log(shapeData.toString(),'shapeData发送邀请') processingSocket(shapeData.toString()) }); }) diff --git a/src/views/custom/tabulaRase/index.vue b/src/views/custom/tabulaRase/index.vue index 63b5da9..dd64e29 100644 --- a/src/views/custom/tabulaRase/index.vue +++ b/src/views/custom/tabulaRase/index.vue @@ -54,7 +54,7 @@ async function joinWhiteboard() { try { const clientId = `whiteboard-${uuidv4()}`; await mqttClient.connect(clientId); - console.log("✅ 已连接 MQTT:", clientId); + // console.log("✅ 已连接 MQTT:", clientId); hasJoined.value = true; diff --git a/src/views/login.vue b/src/views/login.vue index dfa87a9..b909581 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -50,10 +50,9 @@ const redirect = ref(undefined); const loginView = ref(true) // 监听路由变化,获取重定向参数 -// watch(() => route, (newRoute) => { -// redirect.value = newRoute.query && newRoute.query.redirect; -// console.log('重定向路径:', redirect.value); -// }, { immediate: true }); +watch(() => route, (newRoute) => { + redirect.value = newRoute.query && newRoute.query.redirect; +}, { immediate: true }); const loginForm = ref({ username: '', @@ -107,10 +106,9 @@ const loading = ref(false) * 处理登录成功后的跳转逻辑 */ async function handleLoginSuccess() { - try { + try { // 如果有重定向路径且不是登录页,则跳转到重定向页面 - if (redirect.value && redirect.value !== '/login') { - console.log('跳转到重定向页面:', redirect.value); + if (redirect.value && redirect.value !== '/login') { // 确保路由存在,如果不存在则跳转到默认页面 try {