interface SFUMessage { type: string; data?: any; } class SFUSubscriber { private ws: WebSocket | null = null; private peerConnection: RTCPeerConnection | null = null; private isConnected = false; private rtcConfiguration: RTCConfiguration = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } ] }; // UI Elements private statusElement: HTMLElement; private qualityIndicator: HTMLElement; private bitrateIndicator: HTMLElement; private latencyIndicator: HTMLElement; private connectButton: HTMLButtonElement; private disconnectButton: HTMLButtonElement; private remoteVideo: HTMLVideoElement; private videoPlaceholder: HTMLElement; constructor() { this.statusElement = document.getElementById('status')!; this.qualityIndicator = document.getElementById('qualityIndicator')!; this.bitrateIndicator = document.getElementById('bitrateIndicator')!; this.latencyIndicator = document.getElementById('latencyIndicator')!; this.connectButton = document.getElementById('connectBtn') as HTMLButtonElement; this.disconnectButton = document.getElementById('disconnectBtn') as HTMLButtonElement; this.remoteVideo = document.getElementById('remoteVideo') as HTMLVideoElement; this.videoPlaceholder = document.getElementById('videoPlaceholder')!; this.setupEventHandlers(); } private setupEventHandlers(): void { this.connectButton.addEventListener('click', () => this.connect()); this.disconnectButton.addEventListener('click', () => this.disconnect()); } private async connect(): Promise { try { this.updateStatus('Connecting to SFU server...', 'waiting'); this.setButtonStates(false, false); await this.connectToServer(); await this.createPeerConnection(); await this.startSFUConsumption(); this.isConnected = true; this.updateStatus('Connected - Waiting for stream via SFU', 'waiting'); this.setButtonStates(false, true); } catch (error) { console.error('Error connecting:', error); this.updateStatus('Failed to connect to server: ' + error.message, 'disconnected'); this.setButtonStates(true, false); } } private disconnect(): void { this.isConnected = false; if (this.peerConnection) { this.peerConnection.close(); this.peerConnection = null; } if (this.ws) { this.ws.close(); this.ws = null; } this.showVideoPlaceholder(); this.updateStatus('Disconnected', 'disconnected'); this.setButtonStates(true, false); this.resetIndicators(); } private async connectToServer(): Promise { return new Promise((resolve, reject) => { this.ws = new WebSocket('ws://localhost:3001?role=subscriber'); this.ws.onopen = () => resolve(); this.ws.onerror = (error) => reject(error); this.ws.onmessage = async (event) => { try { const message: SFUMessage = JSON.parse(event.data); await this.handleServerMessage(message); } catch (error) { console.error('Failed to parse message:', error); } }; this.ws.onclose = () => { if (this.isConnected) { this.updateStatus('Connection to server lost', 'disconnected'); } }; }); } private async handleServerMessage(message: SFUMessage): Promise { console.log('Received message:', message.type); switch (message.type) { case 'join': console.log('Joined as subscriber:', message.data); break; case 'newProducer': console.log('New producer available:', message.data.producerId); this.updateStatus('Publisher found - Requesting stream', 'waiting'); // Request to consume the producer this.sendMessage({ type: 'consume', data: { producerId: message.data.producerId, rtpCapabilities: {} // Simplified } }); break; case 'consumed': console.log('Consumer created:', message.data.consumerId); this.updateStatus('Connected - Receiving optimized stream', 'connected'); this.hideVideoPlaceholder(); this.updateStreamIndicators(); // Resume the consumer this.sendMessage({ type: 'resume', data: { consumerId: message.data.consumerId } }); break; case 'resumed': console.log('Consumer resumed:', message.data.consumerId); break; case 'producers': console.log('Available producers:', message.data.producers); if (message.data.producers.length > 0) { // Try to consume the first available producer this.sendMessage({ type: 'consume', data: { producerId: message.data.producers[0].id, rtpCapabilities: {} } }); } break; case 'error': console.error('Server error:', message.data.message); this.updateStatus('Server error: ' + message.data.message, 'disconnected'); break; default: console.log('Unknown message type:', message.type); } } private async createPeerConnection(): Promise { this.peerConnection = new RTCPeerConnection(this.rtcConfiguration); this.peerConnection.ontrack = (event) => { console.log('Received remote stream'); const [remoteStream] = event.streams; this.remoteVideo.srcObject = remoteStream; this.hideVideoPlaceholder(); this.updateStatus('Connected - Receiving stream via SFU', 'connected'); this.updateStreamIndicators(); }; this.peerConnection.onicecandidate = (event) => { if (event.candidate) { this.sendMessage({ type: 'ice-candidate', data: event.candidate }); } }; this.peerConnection.onconnectionstatechange = () => { console.log('Connection state:', this.peerConnection?.connectionState); switch (this.peerConnection?.connectionState) { case 'connected': this.updateStatus('Connected - Receiving stream via SFU', 'connected'); break; case 'connecting': this.updateStatus('Connecting to stream...', 'waiting'); break; case 'disconnected': case 'failed': this.updateStatus('Connection lost', 'disconnected'); this.showVideoPlaceholder(); break; } }; } private async startSFUConsumption(): Promise { // Simulate SFU consumption process // In a real SFU implementation, this would involve: // 1. Getting router RTP capabilities // 2. Creating WebRTC transport for receiving // 3. Requesting available producers // 4. Creating consumers for available streams this.sendMessage({ type: 'getRouterRtpCapabilities' }); setTimeout(() => { this.sendMessage({ type: 'createWebRtcTransport' }); }, 100); setTimeout(() => { this.sendMessage({ type: 'connectWebRtcTransport', data: { dtlsParameters: { fingerprints: [], role: 'client' } } }); }, 200); setTimeout(() => { this.sendMessage({ type: 'getProducers' }); }, 300); } private sendMessage(message: SFUMessage): void { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); } } private updateStatus(status: string, className: string): void { this.statusElement.textContent = status; this.statusElement.className = `status ${className}`; } private setButtonStates(connectEnabled: boolean, disconnectEnabled: boolean): void { this.connectButton.disabled = !connectEnabled; this.disconnectButton.disabled = !disconnectEnabled; } private hideVideoPlaceholder(): void { this.videoPlaceholder.style.display = 'none'; this.remoteVideo.style.display = 'block'; } private showVideoPlaceholder(): void { this.videoPlaceholder.style.display = 'flex'; this.remoteVideo.style.display = 'none'; this.remoteVideo.srcObject = null; } private updateStreamIndicators(): void { // Simulate SFU optimizations this.qualityIndicator.textContent = 'HD'; this.bitrateIndicator.textContent = '2.5M'; this.latencyIndicator.textContent = '45ms'; } private resetIndicators(): void { this.qualityIndicator.textContent = '-'; this.bitrateIndicator.textContent = '-'; this.latencyIndicator.textContent = '-'; } } new SFUSubscriber();