Files
WebRTC-Broadcast/public/js/publisher.ts
2025-09-05 00:36:54 -04:00

120 lines
4.0 KiB
TypeScript

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<void> {
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<void> {
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();