+
+
+
-
-
-
-
-
-
-
-
-
-
-
- {{ hostUid }}
-
-
-
-
-
-
-
- 摄像头已关闭
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
{{ participant.identity }}
-
- 正在共享
-
-
-
-
+
+
+ {{ enlargedParticipant.identity === hostUid ? '我' : enlargedParticipant.identity }}
+
+
+
+
+
+
+ 激光笔模式中
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ screenSharingUser }}
+
+
+ 激光笔模式中
+
+
+
-
-
-
-
-
-
-
+
-
-
- {{ cameraEnabled ? '关闭摄像头' : '开启摄像头' }}
-
-
-
-
-
-
-
-
-
-
- {{ device.label || `摄像头 ${cameraDevices.indexOf(device) + 1}` }}
-
-
-
-
- 刷新设备列表
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {{ hostUid }}
+
+
+
+
+
+
+
+ 摄像头已关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 摄像头已关闭
+
+
+
+ {{ participant.identity }}
+
+
+
+
+
+
+
+
+
-
-
- {{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
-
-
-
-
-
-
-
-
-
-
- {{ device.label || `麦克风 ${microphoneDevices.indexOf(device) + 1}` }}
-
-
-
-
- 刷新设备列表
-
-
-
-
-
-
-
-
- 停止共享
- 他人共享中
- 共享屏幕
-
-
- {{ isWhiteboardActive ? '退出白板' : '共享白板' }}
-
-
- {{ isLaserPointerActive ? '关闭激光笔' : '激光笔' }}
-
-
- 文件
-
-
- 邀请人员
-
-
- 离开会议
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {{ cameraEnabled ? '关闭摄像头' : '开启摄像头' }}
+
+
+
+
+
+
+
+
+
+
+ {{ device.label || `摄像头 ${cameraDevices.indexOf(device) + 1}` }}
+
+
+
+
+ 刷新设备列表
+
+
+
+
+
+
+
+ {{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
+
+
+
+
+
+
+
+
+
+
+ {{ device.label || `麦克风 ${microphoneDevices.indexOf(device) + 1}` }}
+
+
+
+
+ 刷新设备列表
+
+
+
+
+
+
+ 停止共享
+ 他人共享中
+ 共享屏幕
+
+
+ {{ isWhiteboardActive ? '退出白板' : '共享白板' }}
+
+
+ {{ isLaserPointerActive ? '关闭激光笔' : '激光笔' }}
+
+
+ 文件
+
+
+ 邀请人员
+
+
+ 离开会议
+
+
+
+
+
+
+
+
+
@@ -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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无视频流
+
+
+
+ {{ enlargedParticipant.identity === hostUid ? '我' : enlargedParticipant.identity }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ screenSharingUser }}
+
+
+ 激光笔模式中
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ hostUid }}
+
+
+
+
+
+
+
+ 摄像头已关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 摄像头已关闭
+
+
+
+ {{ participant.identity }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ cameraEnabled ? '关闭摄像头' : '开启摄像头' }}
+
+
+
+
+
+
+
+
+
+
+ {{ device.label || `摄像头 ${cameraDevices.indexOf(device) + 1}` }}
+
+
+
+
+ 刷新设备列表
+
+
+
+
+
+
+
+ {{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
+
+
+
+
+
+
+
+
+
+
+ {{ device.label || `麦克风 ${microphoneDevices.indexOf(device) + 1}` }}
+
+
+
+
+ 刷新设备列表
+
+
+
+
+
+
+ 停止共享
+ 他人共享中
+ 共享屏幕
+
+
+ {{ isWhiteboardActive ? '退出白板' : '共享白板' }}
+
+
+ {{ isLaserPointerActive ? '关闭激光笔' : '激光笔' }}
+
+
+ 文件
+
+
+ 邀请人员
+
+
+ 离开会议
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 {