/** * React hook for managing remote desktop session connection */ import { useState, useEffect, useCallback, useRef } from 'react'; import type { ConnectionStatus, VideoFrame, MouseEvent as ProtoMouseEvent, KeyEvent as ProtoKeyEvent, MouseEventType, KeyEventType, Modifiers } from '../types/protocol'; import { encodeMouseEvent, encodeKeyEvent, decodeVideoFrame } from '../lib/protobuf'; interface UseRemoteSessionOptions { serverUrl: string; sessionId: string; onFrame?: (frame: VideoFrame) => void; onStatusChange?: (status: ConnectionStatus) => void; } interface UseRemoteSessionReturn { status: ConnectionStatus; connect: () => void; disconnect: () => void; sendMouseEvent: (event: ProtoMouseEvent) => void; sendKeyEvent: (event: ProtoKeyEvent) => void; } export function useRemoteSession(options: UseRemoteSessionOptions): UseRemoteSessionReturn { const { serverUrl, sessionId, onFrame, onStatusChange } = options; const [status, setStatus] = useState({ connected: false, }); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); const frameCountRef = useRef(0); const lastFpsUpdateRef = useRef(Date.now()); // Update status and notify const updateStatus = useCallback((newStatus: Partial) => { setStatus(prev => { const updated = { ...prev, ...newStatus }; onStatusChange?.(updated); return updated; }); }, [onStatusChange]); // Calculate FPS const updateFps = useCallback(() => { const now = Date.now(); const elapsed = now - lastFpsUpdateRef.current; if (elapsed >= 1000) { const fps = Math.round((frameCountRef.current * 1000) / elapsed); updateStatus({ fps }); frameCountRef.current = 0; lastFpsUpdateRef.current = now; } }, [updateStatus]); // Handle incoming WebSocket messages const handleMessage = useCallback((event: MessageEvent) => { if (event.data instanceof Blob) { event.data.arrayBuffer().then(buffer => { const data = new Uint8Array(buffer); const frame = decodeVideoFrame(data); if (frame) { frameCountRef.current++; updateFps(); onFrame?.(frame); } }); } else if (event.data instanceof ArrayBuffer) { const data = new Uint8Array(event.data); const frame = decodeVideoFrame(data); if (frame) { frameCountRef.current++; updateFps(); onFrame?.(frame); } } }, [onFrame, updateFps]); // Connect to server const connect = useCallback(() => { if (wsRef.current?.readyState === WebSocket.OPEN) { return; } // Clear any pending reconnect if (reconnectTimeoutRef.current) { window.clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } const wsUrl = `${serverUrl}/ws/viewer?session_id=${encodeURIComponent(sessionId)}`; const ws = new WebSocket(wsUrl); ws.binaryType = 'arraybuffer'; ws.onopen = () => { updateStatus({ connected: true, sessionId, }); }; ws.onmessage = handleMessage; ws.onclose = (event) => { updateStatus({ connected: false, latencyMs: undefined, fps: undefined, }); // Auto-reconnect after 2 seconds if (!event.wasClean) { reconnectTimeoutRef.current = window.setTimeout(() => { connect(); }, 2000); } }; ws.onerror = () => { updateStatus({ connected: false }); }; wsRef.current = ws; }, [serverUrl, sessionId, handleMessage, updateStatus]); // Disconnect from server const disconnect = useCallback(() => { if (reconnectTimeoutRef.current) { window.clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } if (wsRef.current) { wsRef.current.close(1000, 'User disconnected'); wsRef.current = null; } updateStatus({ connected: false, sessionId: undefined, latencyMs: undefined, fps: undefined, }); }, [updateStatus]); // Send mouse event const sendMouseEvent = useCallback((event: ProtoMouseEvent) => { if (wsRef.current?.readyState === WebSocket.OPEN) { const data = encodeMouseEvent(event); wsRef.current.send(data); } }, []); // Send key event const sendKeyEvent = useCallback((event: ProtoKeyEvent) => { if (wsRef.current?.readyState === WebSocket.OPEN) { const data = encodeKeyEvent(event); wsRef.current.send(data); } }, []); // Cleanup on unmount useEffect(() => { return () => { disconnect(); }; }, [disconnect]); return { status, connect, disconnect, sendMouseEvent, sendKeyEvent, }; } /** * Helper to create mouse event from DOM mouse event */ export function createMouseEvent( domEvent: React.MouseEvent, canvasRect: DOMRect, displayWidth: number, displayHeight: number, eventType: MouseEventType ): ProtoMouseEvent { // Calculate position relative to canvas and scale to display coordinates const scaleX = displayWidth / canvasRect.width; const scaleY = displayHeight / canvasRect.height; const x = Math.round((domEvent.clientX - canvasRect.left) * scaleX); const y = Math.round((domEvent.clientY - canvasRect.top) * scaleY); return { x, y, buttons: { left: (domEvent.buttons & 1) !== 0, right: (domEvent.buttons & 2) !== 0, middle: (domEvent.buttons & 4) !== 0, x1: (domEvent.buttons & 8) !== 0, x2: (domEvent.buttons & 16) !== 0, }, wheelDeltaX: 0, wheelDeltaY: 0, eventType, }; } /** * Helper to create key event from DOM keyboard event */ export function createKeyEvent( domEvent: React.KeyboardEvent, down: boolean ): ProtoKeyEvent { const modifiers: Modifiers = { ctrl: domEvent.ctrlKey, alt: domEvent.altKey, shift: domEvent.shiftKey, meta: domEvent.metaKey, capsLock: domEvent.getModifierState('CapsLock'), numLock: domEvent.getModifierState('NumLock'), }; // Use key code for special keys, unicode for regular characters const isCharacter = domEvent.key.length === 1; return { down, keyType: isCharacter ? 2 : 0, // KEY_UNICODE or KEY_VK vkCode: domEvent.keyCode, scanCode: 0, // Not available in browser unicode: isCharacter ? domEvent.key : undefined, modifiers, }; }