Files
crossdesk-web-client/control.js

278 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 控制模块:处理键鼠、协议封装与 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,
};
})();