/** * WebRTC Signaling Server * Reuses WebSocket infrastructure for WebRTC signaling * Handles peer connection negotiation and ICE candidate exchange */ import { WebSocketServer } from 'ws'; import { Peer } from 'peerjs'; type SignalingMessage = { type: 'offer' | 'answer' | 'ice-candidate' | 'disconnect'; payload: RTCSessionDescriptionInit | RTCIceCandidate | { peerId: string }; targetPeerId: string; }; interface PeerConnection { peer: Peer; connections: Map; iceCandidates: Map; } export class WebRTCSignalingServer { private wss: WebSocketServer; private peers: Map = new Map(); private port: number; constructor(port: number) { this.port = port; this.wss = new WebSocketServer({ port }); this.initialize(); } private initialize(): void { console.log(`WebRTC Signaling Server starting on port ${this.port}`); this.wss.on('connection', (ws: any, req) => { const url = new URL(req.url || '', `http://${req.headers.host}`); const peerId = url.searchParams.get('peerId') || `peer-${Date.now()}-${Math.random()}`; console.log(`WebRTC peer connected: ${peerId}`); const peerConnection: PeerConnection = { peer: new Peer(peerId, { host: 'localhost', port: this.port, path: '/webrtc', }), connections: new Map(), iceCandidates: new Map(), }; this.peers.set(peerId, peerConnection); // Handle incoming signaling messages ws.on('message', (data: Buffer) => { try { const message: SignalingMessage = JSON.parse(data.toString()); this.handleSignalingMessage(peerId, message, ws); } catch (error) { console.error('Error parsing signaling message:', error); ws.send(JSON.stringify({ type: 'error', payload: { message: 'Invalid message format' } })); } }); // Handle disconnection ws.on('close', () => { console.log(`WebRTC peer disconnected: ${peerId}`); this.cleanupPeer(peerId); }); // Send confirmation ws.send(JSON.stringify({ type: 'connected', payload: { peerId } })); }); console.log(`WebRTC Signaling Server started on port ${this.port}`); } private handleSignalingMessage( sourcePeerId: string, message: SignalingMessage, ws: any ): void { const { type, payload, targetPeerId } = message; const sourceConnection = this.peers.get(sourcePeerId); if (!sourceConnection) { console.warn(`Source peer not found: ${sourcePeerId}`); return; } switch (type) { case 'offer': this.handleOffer(sourcePeerId, targetPeerId, payload as RTCSessionDescriptionInit, ws); break; case 'answer': this.handleAnswer(sourcePeerId, targetPeerId, payload as RTCSessionDescriptionInit); break; case 'ice-candidate': this.handleIceCandidate(sourcePeerId, targetPeerId, payload as RTCIceCandidate); break; case 'disconnect': this.disconnectPeer(sourcePeerId, targetPeerId); break; } } private async handleOffer( sourcePeerId: string, targetPeerId: string, offer: RTCSessionDescriptionInit, ws: any ): Promise { console.log(`Offer received from ${sourcePeerId} to ${targetPeerId}`); const targetConnection = this.peers.get(targetPeerId); if (!targetConnection) { console.warn(`Target peer not found: ${targetPeerId}`); ws.send(JSON.stringify({ type: 'error', payload: { message: `Target peer ${targetPeerId} not found` }, })); return; } // Store the connection if (!targetConnection.connections.has(sourcePeerId)) { const conn = targetConnection.peer.call(sourcePeerId, new MediaStream()); targetConnection.connections.set(sourcePeerId, conn); // Handle connection events conn.on('stream', (stream: MediaStream) => { console.log(`Media stream received: ${targetPeerId} from ${sourcePeerId}`); }); conn.on('close', () => { console.log(`Connection closed: ${targetPeerId} <-> ${sourcePeerId}`); }); conn.on('error', (error: Error) => { console.error(`Connection error: ${targetPeerId} <-> ${sourcePeerId}`, error); }); // Send accumulated ICE candidates const accumulatedCandidates = targetConnection.iceCandidates.get(sourcePeerId) || []; accumulatedCandidates.forEach(candidate => { conn.dataChannel.send(JSON.stringify({ type: 'ice-candidate', payload: candidate, targetPeerId, })); }); } // Forward offer to target peer const targetConn = targetConnection.connections.get(sourcePeerId); if (targetConn) { (targetConn as any).dataChannel.send(JSON.stringify({ type: 'offer', payload: offer, targetPeerId: targetPeerId, })); } } private handleAnswer( sourcePeerId: string, targetPeerId: string, answer: RTCSessionDescriptionInit ): void { console.log(`Answer received from ${sourcePeerId} to ${targetPeerId}`); const targetConnection = this.peers.get(targetPeerId); if (targetConnection?.connections.has(sourcePeerId)) { targetConnection.connections.get(sourcePeerId).send(JSON.stringify({ type: 'answer', payload: answer, targetPeerId: targetPeerId, })); } } private handleIceCandidate( sourcePeerId: string, targetPeerId: string, candidate: RTCIceCandidate ): void { const targetConnection = this.peers.get(targetPeerId); if (!targetConnection) { return; } // Forward ICE candidate to target peer if (targetConnection.connections.has(sourcePeerId)) { const conn = targetConnection.connections.get(sourcePeerId); if (conn) { (conn as any).send(JSON.stringify({ type: 'ice-candidate', payload: candidate, targetPeerId: targetPeerId, })); } } } private disconnectPeer(sourcePeerId: string, targetPeerId: string): void { const sourceConnection = this.peers.get(sourcePeerId); if (sourceConnection?.connections.has(targetPeerId)) { sourceConnection.connections.get(targetPeerId).close(); sourceConnection.connections.delete(targetPeerId); console.log(`Connection closed: ${sourcePeerId} <-> ${targetPeerId}`); } } private cleanupPeer(peerId: string): void { const peerConnection = this.peers.get(peerId); if (peerConnection) { // Close all connections peerConnection.connections.forEach((conn, connectedPeerId) => { conn.close(); console.log(`Cleaned up connection: ${peerId} <-> ${connectedPeerId}`); }); // Destroy PeerJS instance peerConnection.peer.destroy(); // Remove from registry this.peers.delete(peerId); } } getPeerCount(): number { return this.peers.size; } getPeers(): string[] { return Array.from(this.peers.keys()); } getPeerConnections(peerId: string): string[] { const peerConnection = this.peers.get(peerId); if (!peerConnection) return []; return Array.from(peerConnection.connections.keys()); } close(): void { this.peers.forEach((_, peerId) => this.cleanupPeer(peerId)); this.wss.close(); console.log('WebRTC Signaling Server closed'); } } export default WebRTCSignalingServer;