/** * WebRTC Connection Manager * Handles peer-to-peer connections for remote desktop streaming */ export interface RTCConfig { iceServers: RTCIceServer[] } export interface ConnectionEvents { onTrack?: (stream: MediaStream) => void onDataChannel?: (channel: RTCDataChannel) => void onConnectionStateChange?: (state: RTCPeerConnectionState) => void onIceCandidate?: (candidate: RTCIceCandidate | null) => void onError?: (error: Error) => void } const DEFAULT_RTC_CONFIG: RTCConfig = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, { urls: 'stun:stun2.l.google.com:19302' }, ], } export class WebRTCConnection { private peerConnection: RTCPeerConnection | null = null private dataChannel: RTCDataChannel | null = null private events: ConnectionEvents = {} constructor(config: RTCConfig = DEFAULT_RTC_CONFIG) { if (typeof window === 'undefined') return this.peerConnection = new RTCPeerConnection(config) this.setupEventHandlers() } private setupEventHandlers() { if (!this.peerConnection) return this.peerConnection.ontrack = (event) => { this.events.onTrack?.(event.streams[0]) } this.peerConnection.ondatachannel = (event) => { this.dataChannel = event.channel this.setupDataChannel() this.events.onDataChannel?.(event.channel) } this.peerConnection.onconnectionstatechange = () => { if (this.peerConnection) { this.events.onConnectionStateChange?.(this.peerConnection.connectionState) } } this.peerConnection.onicecandidate = (event) => { this.events.onIceCandidate?.(event.candidate) } } private setupDataChannel() { if (!this.dataChannel) return this.dataChannel.onopen = () => { console.log('[WebRTC] Data channel opened') } this.dataChannel.onclose = () => { console.log('[WebRTC] Data channel closed') } this.dataChannel.onerror = (error) => { console.error('[WebRTC] Data channel error:', error) } } on(event: K, handler: ConnectionEvents[K]) { this.events[event] = handler } // Create offer (for viewer) async createOffer(): Promise { if (!this.peerConnection) throw new Error('PeerConnection not initialized') // Create data channel for input events this.dataChannel = this.peerConnection.createDataChannel('input', { ordered: true, }) this.setupDataChannel() const offer = await this.peerConnection.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true, }) await this.peerConnection.setLocalDescription(offer) return offer } // Set remote answer (for viewer) async setRemoteAnswer(answer: RTCSessionDescriptionInit) { if (!this.peerConnection) throw new Error('PeerConnection not initialized') await this.peerConnection.setRemoteDescription(answer) } // Create answer (for host/agent) async createAnswer(offer: RTCSessionDescriptionInit): Promise { if (!this.peerConnection) throw new Error('PeerConnection not initialized') await this.peerConnection.setRemoteDescription(offer) const answer = await this.peerConnection.createAnswer() await this.peerConnection.setLocalDescription(answer) return answer } // Add ICE candidate async addIceCandidate(candidate: RTCIceCandidateInit) { if (!this.peerConnection) throw new Error('PeerConnection not initialized') await this.peerConnection.addIceCandidate(candidate) } // Send input event via data channel sendInputEvent(event: InputEvent) { if (!this.dataChannel || this.dataChannel.readyState !== 'open') return this.dataChannel.send(JSON.stringify(event)) } // Get connection state getConnectionState(): RTCPeerConnectionState | null { return this.peerConnection?.connectionState ?? null } // Close connection close() { this.dataChannel?.close() this.peerConnection?.close() this.dataChannel = null this.peerConnection = null } } // Input event types export type InputEventType = 'mousemove' | 'mousedown' | 'mouseup' | 'keydown' | 'keyup' | 'scroll' export interface InputEvent { type: InputEventType timestamp: number data: MouseEventData | KeyEventData | ScrollEventData } export interface MouseEventData { x: number y: number button?: number } export interface KeyEventData { key: string code: string altKey?: boolean ctrlKey?: boolean shiftKey?: boolean metaKey?: boolean } export interface ScrollEventData { deltaX: number deltaY: number } // Helper to create input events export function createMouseEvent(type: 'mousemove' | 'mousedown' | 'mouseup', x: number, y: number, button?: number): InputEvent { return { type, timestamp: Date.now(), data: { x, y, button }, } } export function createKeyEvent(type: 'keydown' | 'keyup', event: KeyboardEvent): InputEvent { return { type, timestamp: Date.now(), data: { key: event.key, code: event.code, altKey: event.altKey, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, metaKey: event.metaKey, }, } } export function createScrollEvent(deltaX: number, deltaY: number): InputEvent { return { type: 'scroll', timestamp: Date.now(), data: { deltaX, deltaY }, } }