/** * Mirror Panel — Canvas 기반 실시간 UI 미러링 * * Screencast 프레임을 canvas에 렌더링하고 * 마우스/키보드/스크롤 이벤트를 서버로 전달 */ class MirrorPanel { constructor() { this.canvas = document.getElementById('mirrorCanvas'); this.ctx = this.canvas ? this.canvas.getContext('2d') : null; this.containerEl = document.getElementById('mirrorContainer'); this.active = false; this.sessionId = null; // 실제 대상 페이지 크기 (metadata에서 가져옴) this.deviceWidth = 0; this.deviceHeight = 0; // 콜백 this.onStartScreencast = null; // callback(sessionId) this.onStopScreencast = null; // callback(sessionId) this.onInputEvent = null; // callback(sessionId, event) // 이미지 로딩용 this._img = new Image(); this._img.onload = () => { if (!this.ctx) return; this.canvas.width = this._img.naturalWidth; this.canvas.height = this._img.naturalHeight; this.ctx.drawImage(this._img, 0, 0); }; // 마우스 이벤트 쓸모링 this._lastMouseMove = 0; this._MOUSE_THROTTLE = 50; // ms this._setupEvents(); } /** * 이벤트 바인딩 */ _setupEvents() { if (!this.canvas) return; // --- 마우스 --- this.canvas.addEventListener('mousedown', (e) => { if (!this.active) return; const pos = this._toDeviceCoords(e); this._sendMouse('mousePressed', pos, e); }); this.canvas.addEventListener('mouseup', (e) => { if (!this.active) return; const pos = this._toDeviceCoords(e); this._sendMouse('mouseReleased', pos, e); }); this.canvas.addEventListener('mousemove', (e) => { if (!this.active) return; const now = Date.now(); if (now - this._lastMouseMove < this._MOUSE_THROTTLE) return; this._lastMouseMove = now; const pos = this._toDeviceCoords(e); this._sendMouse('mouseMoved', pos, e); }); // --- 스크롤 --- this.canvas.addEventListener('wheel', (e) => { if (!this.active) return; e.preventDefault(); const pos = this._toDeviceCoords(e); this._sendInput({ type: 'scroll', x: pos.x, y: pos.y, deltaX: e.deltaX, deltaY: e.deltaY, }); }, { passive: false }); // --- 키보드 --- this.canvas.setAttribute('tabindex', '0'); this.canvas.addEventListener('keydown', (e) => { if (!this.active) return; e.preventDefault(); this._sendKey('keyDown', e); // char 이벤트도 보내야 텍스트 입력이 됨 if (e.key.length === 1) { this._sendKey('char', e); } }); this.canvas.addEventListener('keyup', (e) => { if (!this.active) return; e.preventDefault(); this._sendKey('keyUp', e); }); // canvas 클릭 시 포커스 this.canvas.addEventListener('click', () => { this.canvas.focus(); }); // 우클릭 방지 this.canvas.addEventListener('contextmenu', (e) => e.preventDefault()); } /** * 미러링 시작 */ start(sessionId) { this.sessionId = sessionId; this.active = true; if (this.containerEl) this.containerEl.style.display = 'flex'; if (this.onStartScreencast) this.onStartScreencast(sessionId); this.canvas?.focus(); } /** * 미러링 중지 */ stop() { if (this.active && this.onStopScreencast) { this.onStopScreencast(this.sessionId); } this.active = false; this.sessionId = null; if (this.containerEl) this.containerEl.style.display = 'none'; // canvas 초기화 if (this.ctx && this.canvas) { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } } /** * Screencast 프레임 수신 */ updateFrame(data, metadata) { if (!this.active) return; if (metadata) { this.deviceWidth = metadata.deviceWidth || this.deviceWidth; this.deviceHeight = metadata.deviceHeight || this.deviceHeight; } // base64 JPEG를 이미지로 로드 → canvas에 그리기 this._img.src = `data:image/jpeg;base64,${data}`; } /** * canvas 좌표 → 디바이스(Antigravity) 좌표 변환 */ _toDeviceCoords(e) { const rect = this.canvas.getBoundingClientRect(); const scaleX = this.canvas.width / rect.width; const scaleY = this.canvas.height / rect.height; return { x: Math.round((e.clientX - rect.left) * scaleX), y: Math.round((e.clientY - rect.top) * scaleY), }; } /** * 마우스 이벤트 전송 */ _sendMouse(action, pos, e) { const buttonMap = { 0: 'left', 1: 'middle', 2: 'right' }; this._sendInput({ type: 'mouse', action, x: pos.x, y: pos.y, button: buttonMap[e.button] || 'left', clickCount: action === 'mousePressed' ? 1 : 0, modifiers: this._getModifiers(e), }); } /** * 키보드 이벤트 전송 */ _sendKey(action, e) { this._sendInput({ type: 'key', action, key: e.key, code: e.code, text: action === 'char' ? e.key : '', keyCode: e.keyCode, modifiers: this._getModifiers(e), }); } /** * 입력 이벤트 전송 (공통) */ _sendInput(event) { if (this.onInputEvent && this.sessionId) { this.onInputEvent(this.sessionId, event); } } /** * 수정키 비트마스크 계산 (CDP 규격) */ _getModifiers(e) { let m = 0; if (e.altKey) m |= 1; if (e.ctrlKey) m |= 2; if (e.metaKey) m |= 4; if (e.shiftKey) m |= 8; return m; } }