import { WebSocketClient, WebSocketClientStatusMapping } from './services/WebSocketClient.ts'; import { MediaHandler } from './services/MediaHandler.ts'; import { UIController } from './services/UIController.ts'; import { PublisherRTCManager } from './services/PublisherRTCManager.ts'; import type { ISignalingMessage } from './interfaces/IWebRTCClient.ts'; import { DisposableList } from '@techniker-me/tools'; class Publisher { private readonly _disposables: DisposableList = new DisposableList(); private wsClient: WebSocketClient; private mediaHandler: MediaHandler; private uiController: UIController; private rtcManager: PublisherRTCManager; private isStreaming = false; constructor() { this.wsClient = new WebSocketClient('publisher'); this.mediaHandler = new MediaHandler('localVideo'); this.uiController = new UIController('status', 'subscribersCount', 'startBtn', 'stopBtn'); this.rtcManager = new PublisherRTCManager((count) => { this.uiController.updateSubscribersCount(count); }); this.setupEventHandlers(); this.setupWebSocketHandlers(); } private setupEventHandlers(): void { this.uiController.onButtonClick('startBtn', () => this.startBroadcasting()); this.uiController.onButtonClick('stopBtn', () => this.stopBroadcasting()); } private setupWebSocketHandlers(): void { this._disposables.add(this.wsClient.status.subscribe((status) => { this.uiController.updateStatus(WebSocketClientStatusMapping.convertWebSocketClientStatusToWebSocketClientStatusType(status), status); })); this.wsClient.onMessage('join', (message) => { this.uiController.updateStatus('Connected to server', 'connected'); }); this.wsClient.onMessage('answer', async (message) => { if (message.senderId) { await this.rtcManager.handleAnswer(message.senderId, message.data); } }); this.wsClient.onMessage('ice-candidate', async (message) => { if (message.senderId && message.data) { await this.rtcManager.handleIceCandidate(message.senderId, message.data); } }); this.rtcManager.setOnIceCandidate((subscriberId, candidate) => { const message: ISignalingMessage = { type: 'ice-candidate', data: candidate, targetId: subscriberId }; this.wsClient.sendMessage(message); }); } private async startBroadcasting(): Promise { try { this.uiController.updateStatus('Starting broadcast...', 'waiting'); this.uiController.setButtonStates(false, false); await this.wsClient.connect(); const stream = await this.mediaHandler.getLocalStream(); this.rtcManager.setLocalStream(stream); this.isStreaming = true; this.uiController.updateStatus('Broadcasting - Ready for subscribers', 'connected'); this.uiController.setButtonStates(false, true); this.startOfferLoop(); } catch (error) { console.error('Error starting broadcast:', error); this.uiController.updateStatus('Failed to start broadcast', 'disconnected'); this.uiController.setButtonStates(true, false); } } private stopBroadcasting(): void { this.isStreaming = false; this.mediaHandler.stopLocalStream(); this.rtcManager.closeAllConnections(); this.wsClient.disconnect(); this.uiController.updateStatus('Broadcast stopped', 'disconnected'); this.uiController.setButtonStates(true, false); this.uiController.updateSubscribersCount(0); } private async startOfferLoop(): Promise { const offerToNewSubscribers = async () => { if (!this.isStreaming) return; try { const offer = await this.rtcManager.createOfferForSubscriber('broadcast'); const message: ISignalingMessage = { type: 'offer', data: offer }; this.wsClient.sendMessage(message); } catch (error) { console.error('Error creating offer:', error); } if (this.isStreaming) { setTimeout(offerToNewSubscribers, 2000); } }; setTimeout(offerToNewSubscribers, 1000); } } new Publisher();