interface SFUMessage { type: string; data?: any; } class SFUPublisher { private ws: WebSocket | null = null; private peerConnection: RTCPeerConnection | null = null; private localStream: MediaStream | null = null; private isStreaming = 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 subscribersCountElement: HTMLElement; private producersCountElement: HTMLElement; private startButton: HTMLButtonElement; private stopButton: HTMLButtonElement; private localVideo: HTMLVideoElement; constructor() { this.statusElement = document.getElementById('status')!; this.subscribersCountElement = document.getElementById('subscribersCount')!; this.producersCountElement = document.getElementById('producersCount')!; this.startButton = document.getElementById('startBtn') as HTMLButtonElement; this.stopButton = document.getElementById('stopBtn') as HTMLButtonElement; this.localVideo = document.getElementById('localVideo') as HTMLVideoElement; this.setupEventHandlers(); } private setupEventHandlers(): void { this.startButton.addEventListener('click', () => this.startBroadcasting()); this.stopButton.addEventListener('click', () => this.stopBroadcasting()); } private async startBroadcasting(): Promise { try { this.updateStatus('Connecting to SFU server...', 'waiting'); this.setButtonStates(false, false); await this.connectToServer(); await this.getLocalStream(); await this.createPeerConnection(); await this.startSFUNegotiation(); this.isStreaming = true; this.updateStatus('Broadcasting via SFU - Optimized for scalability!', 'connected'); this.setButtonStates(false, true); this.updateStats(); } catch (error) { console.error('Error starting broadcast:', error); this.updateStatus('Failed to start broadcast: ' + error.message, 'disconnected'); this.setButtonStates(true, false); } } private async stopBroadcasting(): Promise { this.isStreaming = false; if (this.peerConnection) { this.peerConnection.close(); this.peerConnection = null; } if (this.localStream) { this.localStream.getTracks().forEach(track => track.stop()); this.localStream = null; } if (this.localVideo) { this.localVideo.srcObject = null; } if (this.ws) { this.ws.close(); this.ws = null; } this.updateStatus('Broadcast stopped', 'disconnected'); this.setButtonStates(true, false); this.resetStats(); } private async connectToServer(): Promise { return new Promise((resolve, reject) => { this.ws = new WebSocket('ws://localhost:3001?role=publisher'); 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.isStreaming) { 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 publisher:', message.data); break; case 'routerRtpCapabilities': console.log('Received router capabilities'); // In a full SFU implementation, this would configure the device break; case 'webRtcTransportCreated': console.log('Transport created, connecting...'); break; case 'webRtcTransportConnected': console.log('Transport connected, starting to produce'); break; case 'produced': console.log('Producer created:', message.data.producerId); break; case 'error': console.error('Server error:', message.data.message); break; default: console.log('Unknown message type:', message.type); } } private async getLocalStream(): Promise { this.localStream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280, max: 1920 }, height: { ideal: 720, max: 1080 }, frameRate: { ideal: 30, max: 60 } }, audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); this.localVideo.srcObject = this.localStream; } private async createPeerConnection(): Promise { this.peerConnection = new RTCPeerConnection(this.rtcConfiguration); // Add local stream to peer connection if (this.localStream) { this.localStream.getTracks().forEach(track => { this.peerConnection!.addTrack(track, this.localStream!); }); } 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); }; } private async startSFUNegotiation(): Promise { // Simulate SFU negotiation process // In a real SFU implementation, this would involve: // 1. Getting router RTP capabilities // 2. Creating WebRTC transport // 3. Connecting transport // 4. Creating producers for audio/video this.sendMessage({ type: 'getRouterRtpCapabilities' }); // Simulate successful setup setTimeout(() => { this.sendMessage({ type: 'createWebRtcTransport' }); }, 100); setTimeout(() => { this.sendMessage({ type: 'connectWebRtcTransport', data: { dtlsParameters: { fingerprints: [], role: 'client' } } }); }, 200); setTimeout(() => { if (this.localStream) { const videoTrack = this.localStream.getVideoTracks()[0]; const audioTrack = this.localStream.getAudioTracks()[0]; if (videoTrack) { this.sendMessage({ type: 'produce', data: { kind: 'video', rtpParameters: { codecs: [], encodings: [] } } }); } if (audioTrack) { this.sendMessage({ type: 'produce', data: { kind: 'audio', rtpParameters: { codecs: [], encodings: [] } } }); } } }, 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(startEnabled: boolean, stopEnabled: boolean): void { this.startButton.disabled = !startEnabled; this.stopButton.disabled = !stopEnabled; } private updateStats(): void { this.subscribersCountElement.textContent = '0'; this.producersCountElement.textContent = this.isStreaming ? '2' : '0'; } private resetStats(): void { this.subscribersCountElement.textContent = '0'; this.producersCountElement.textContent = '0'; } } new SFUPublisher();