// 控制模块:处理键鼠、协议封装与 DataChannel 交互 // 依赖:页面已有的 DOM 元素(video、日志与输入区域) (function () { const dataChannelStateSpan = document.getElementById('datachannel-state'); const dataChannelLog = document.getElementById('data-channel'); const dcInput = document.getElementById('dc-input'); const dcSendBtn = document.getElementById('dc-send'); const audioCaptureChk = document.getElementById('audio-capture'); const displayIdInput = document.getElementById('display-id'); let dc = null; // Pointer/mouse 状态 let lastPointerPos = null; let isPointerLocked = false; let videoRect = null; let normalizedPos = { x: 0.5, y: 0.5 }; let _pointerlock_toast_timeout = null; // 协议枚举 const ControlType = { mouse: 0, keyboard: 1, audio_capture: 2, host_infomation: 3, display_id: 4 }; const MouseFlag = { move: 0, left_down: 1, left_up: 2, right_down: 3, right_up: 4, middle_down: 5, middle_up: 6, wheel_vertical: 7, wheel_horizontal: 8 }; function showPointerLockToast(text, duration = 2500) { let el = document.getElementById('pointerlock-toast'); if (!el) { el = document.createElement('div'); el.id = 'pointerlock-toast'; Object.assign(el.style, { position: 'fixed', left: '50%', bottom: '24px', transform: 'translateX(-50%)', background: 'rgba(0,0,0,0.75)', color: '#fff', padding: '8px 12px', borderRadius: '6px', fontSize: '13px', zIndex: '9999', pointerEvents: 'none', opacity: '1', transition: 'opacity 0.2s' }); document.body.appendChild(el); } el.textContent = text; el.style.opacity = '1'; if (_pointerlock_toast_timeout) clearTimeout(_pointerlock_toast_timeout); _pointerlock_toast_timeout = setTimeout(() => { el.style.opacity = '0'; _pointerlock_toast_timeout = null; }, duration); } function clamp01(v) { return Math.max(0, Math.min(1, v)); } function logSent(obj) { if (dataChannelLog) { dataChannelLog.textContent += '> ' + JSON.stringify(obj) + '\n'; dataChannelLog.scrollTop = dataChannelLog.scrollHeight; } } // 对外:设置显示器 ID 的入口(按钮调用) function setDisplayId() { if (!displayIdInput) return; let id = ''; if (displayIdInput.tagName === 'SELECT') { id = displayIdInput.value || ''; } else { id = (displayIdInput.value && displayIdInput.value.trim()) ? displayIdInput.value.trim() : ''; } if (!id) id = (window.CROSSDESK_TRACK_ID || ''); if (!id) { alert('暂无可用 track id'); return; } // 同步标题显示 const trackIdEl = document.getElementById('track-id'); if (trackIdEl) trackIdEl.textContent = id; sendDisplayId(id); } // 发送:鼠标/键盘/音频/显示器 function sendRemoteActionAt(normX, normY, flag, s = 0) { const numericFlag = (typeof flag === 'string') ? (MouseFlag[flag] ?? MouseFlag.move) : (flag | 0); const remote_action = { type: ControlType.mouse, mouse: { x: clamp01(normX), y: clamp01(normY), s: (s | 0), flag: numericFlag } }; if (dc && dc.readyState === 'open') dc.send(JSON.stringify(remote_action)); logSent(remote_action); } function sendKeyboardAction(keyValue, isDown) { const remote_action = { type: ControlType.keyboard, keyboard: { key_value: keyValue | 0, flag: isDown ? 0 : 1 } }; if (dc && dc.readyState === 'open') dc.send(JSON.stringify(remote_action)); logSent(remote_action); } function sendAudioCapture(enabled) { const remote_action = { type: ControlType.audio_capture, audio_capture: !!enabled }; if (dc && dc.readyState === 'open') dc.send(JSON.stringify(remote_action)); logSent(remote_action); } function sendDisplayId(id) { // 约定:显示器ID即 track id(字符串) const remote_action = { type: ControlType.display_id, display_id: id }; if (dc && dc.readyState === 'open') dc.send(JSON.stringify(remote_action)); logSent(remote_action); } // 发送自由文本(仅限协议 JSON) function sendDataChannelMessage() { const msg = (dcInput && dcInput.value) ? dcInput.value.trim() : ''; if (!msg) return; if (!dc || dc.readyState !== 'open') { alert('数据通道未打开,无法发送消息。'); return; } try { const obj = JSON.parse(msg); const isObject = obj && typeof obj === 'object' && !Array.isArray(obj); const hasNumericType = isObject && typeof obj.type === 'number'; const hasValidPayload = (('mouse' in obj) || ('keyboard' in obj) || ('audio_capture' in obj) || ('display_id' in obj)); if (!hasNumericType || !hasValidPayload) { alert('仅支持发送 RemoteAction 协议 JSON。'); return; } dc.send(JSON.stringify(obj)); logSent(obj); if (dcInput) dcInput.value = ''; } catch (e) { alert('请输入合法的 JSON。'); } } // 鼠标与键盘监听 function setupKeyboardListeners() { const onKeyDown = (e) => { const keyValue = (typeof e.keyCode === 'number') ? e.keyCode : 0; sendKeyboardAction(keyValue, true); }; const onKeyUp = (e) => { const keyValue = (typeof e.keyCode === 'number') ? e.keyCode : 0; sendKeyboardAction(keyValue, false); }; document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); } function sendMouseEvent(event) { const video = document.getElementById('video'); if (!video) return; if (!videoRect) videoRect = video.getBoundingClientRect(); if (event.type === 'mousedown') { if (event.clientX >= videoRect.left && event.clientX <= videoRect.right && event.clientY >= videoRect.top && event.clientY <= videoRect.bottom) { normalizedPos.x = (event.clientX - videoRect.left) / videoRect.width; normalizedPos.y = (event.clientY - videoRect.top) / videoRect.height; try { video.requestPointerLock && video.requestPointerLock(); } catch (e) { } const flag = event.button === 0 ? 'left_down' : (event.button === 2 ? 'right_down' : 'middle_down'); sendRemoteActionAt(normalizedPos.x, normalizedPos.y, flag); } return; } if (event.type === 'mouseup') { if (isPointerLocked) { const flag = event.button === 0 ? 'left_up' : (event.button === 2 ? 'right_up' : 'middle_up'); sendRemoteActionAt(normalizedPos.x, normalizedPos.y, flag); } else if (event.clientX >= videoRect.left && event.clientX <= videoRect.right && event.clientY >= videoRect.top && event.clientY <= videoRect.bottom) { const x = (event.clientX - videoRect.left) / videoRect.width; const y = (event.clientY - videoRect.top) / videoRect.height; const flag = event.button === 0 ? 'left_up' : (event.button === 2 ? 'right_up' : 'middle_up'); sendRemoteActionAt(x, y, flag); } return; } if (event.type === 'mousemove') { if (isPointerLocked) { videoRect = video.getBoundingClientRect(); normalizedPos.x = clamp01(normalizedPos.x + (event.movementX / videoRect.width)); normalizedPos.y = clamp01(normalizedPos.y + (event.movementY / videoRect.height)); sendRemoteActionAt(normalizedPos.x, normalizedPos.y, 'move'); } else { if (event.clientX >= videoRect.left && event.clientX <= videoRect.right && event.clientY >= videoRect.top && event.clientY <= videoRect.bottom) { const x = (event.clientX - videoRect.left) / videoRect.width; const y = (event.clientY - videoRect.top) / videoRect.height; sendRemoteActionAt(x, y, 'move'); } } return; } if (event.type === 'wheel') { let x, y; if (isPointerLocked) { x = normalizedPos.x; y = normalizedPos.y; } else { videoRect = video.getBoundingClientRect(); if (!(event.clientX >= videoRect.left && event.clientX <= videoRect.right && event.clientY >= videoRect.top && event.clientY <= videoRect.bottom)) return; x = (event.clientX - videoRect.left) / videoRect.width; y = (event.clientY - videoRect.top) / videoRect.height; } const flag = event.deltaY === 0 ? 'wheel_horizontal' : 'wheel_vertical'; sendRemoteActionAt(x, y, flag, event.deltaY || event.deltaX); return; } } function setupMouseListeners() { const video = document.getElementById('video'); if (!video) return; try { video.style.touchAction = 'none'; } catch (e) { } document.addEventListener('pointerlockchange', () => { isPointerLocked = (document.pointerLockElement === video); if (dataChannelLog) { dataChannelLog.textContent += `[pointerlock ${isPointerLocked ? 'entered' : 'exited'}]\n`; dataChannelLog.scrollTop = dataChannelLog.scrollHeight; } if (isPointerLocked) { videoRect = video.getBoundingClientRect(); } else { videoRect = null; showPointerLockToast('已退出鼠标锁定,按 Esc 或点击视频重新锁定(释放可按 Ctrl+Esc)', 3000); } }); document.addEventListener('pointerlockerror', () => { showPointerLockToast('鼠标锁定失败', 2500); }); document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'Escape') { if (document.exitPointerLock) document.exitPointerLock(); } }); // Pointer Events video.addEventListener('pointerdown', (e) => { if (e.button < 0) return; e.preventDefault(); lastPointerPos = { x: e.clientX, y: e.clientY }; try { video.setPointerCapture && video.setPointerCapture(e.pointerId); } catch (err) { } sendMouseEvent({ type: 'mousedown', clientX: e.clientX, clientY: e.clientY, button: (typeof e.button === 'number') ? e.button : 0 }); }, { passive: false }); document.addEventListener('pointermove', (e) => { const movementX = (lastPointerPos ? (e.clientX - lastPointerPos.x) : 0); const movementY = (lastPointerPos ? (e.clientY - lastPointerPos.y) : 0); lastPointerPos = { x: e.clientX, y: e.clientY }; sendMouseEvent({ type: 'mousemove', clientX: e.clientX, clientY: e.clientY, movementX, movementY }); }, { passive: false }); document.addEventListener('pointerup', (e) => { try { video.releasePointerCapture && video.releasePointerCapture(e.pointerId); } catch (err) { } sendMouseEvent({ type: 'mouseup', clientX: e.clientX, clientY: e.clientY, button: (typeof e.button === 'number') ? e.button : 0 }); lastPointerPos = null; }); document.addEventListener('pointercancel', () => { lastPointerPos = null; }); if (!window.PointerEvent) { video.addEventListener('touchstart', (e) => { if (!e.touches || e.touches.length === 0) return; const t = e.touches[0]; lastPointerPos = { x: t.clientX, y: t.clientY }; e.preventDefault(); sendMouseEvent({ type: 'mousedown', clientX: t.clientX, clientY: t.clientY, button: 0 }); }, { passive: false }); document.addEventListener('touchmove', (e) => { if (!e.touches || e.touches.length === 0) return; const t = e.touches[0]; const movementX = (lastPointerPos ? (t.clientX - lastPointerPos.x) : 0); const movementY = (lastPointerPos ? (t.clientY - lastPointerPos.y) : 0); lastPointerPos = { x: t.clientX, y: t.clientY }; e.preventDefault(); sendMouseEvent({ type: 'mousemove', clientX: t.clientX, clientY: t.clientY, movementX, movementY }); }, { passive: false }); document.addEventListener('touchend', (e) => { const t = (e.changedTouches && e.changedTouches[0]) || null; if (t) { sendMouseEvent({ type: 'mouseup', clientX: t.clientX, clientY: t.clientY, button: 0 }); } else { sendMouseEvent({ type: 'mouseup', clientX: 0, clientY: 0, button: 0 }); } lastPointerPos = null; }, { passive: false }); } document.addEventListener('wheel', sendMouseEvent, { passive: true }); } // DataChannel 生命周期钩子(供 WebRTC 调用) function onDataChannelOpen(dataChannel) { dc = dataChannel; if (dataChannelStateSpan) dataChannelStateSpan.textContent = 'open'; if (dataChannelLog) { dataChannelLog.textContent += '[datachannel open]\n'; dataChannelLog.scrollTop = dataChannelLog.scrollHeight; } setupMouseListeners(); setupKeyboardListeners(); if (dcInput) dcInput.disabled = false; if (dcSendBtn) dcSendBtn.disabled = false; if (audioCaptureChk) { audioCaptureChk.disabled = true; audioCaptureChk.checked = false; audioCaptureChk.disabled = false; audioCaptureChk.onchange = (e) => sendAudioCapture(!!e.target.checked); } if (displayIdInput) displayIdInput.disabled = false; const setDisplayBtn = document.getElementById('set-display'); if (setDisplayBtn) setDisplayBtn.disabled = false; } function onDataChannelClose() { if (dataChannelStateSpan) dataChannelStateSpan.textContent = 'closed'; if (dataChannelLog) { dataChannelLog.textContent += '[datachannel closed]\n'; dataChannelLog.scrollTop = dataChannelLog.scrollHeight; } if (dcInput) { dcInput.disabled = true; dcInput.value = ''; } if (dcSendBtn) dcSendBtn.disabled = true; if (audioCaptureChk) { audioCaptureChk.disabled = true; audioCaptureChk.checked = false; audioCaptureChk.onchange = null; } if (displayIdInput) displayIdInput.disabled = true; const setDisplayBtn = document.getElementById('set-display'); if (setDisplayBtn) setDisplayBtn.disabled = true; dc = null; } // 暴露到全局 window.CrossDeskControl = { onDataChannelOpen, onDataChannelClose, sendDataChannelMessage, setDisplayId, }; })();