import { RTC_CONFIG } from "@/config"; import { EventEmitter } from "eventemitter3"; import { IPlatformWebRTC } from "../platform"; import { IRTCConnectionManager } from "./webrtc.interface"; import { RTCConnectionEventMap } from "./webrtc.types"; export class RTCConnectionManager extends EventEmitter implements IRTCConnectionManager { private pc: RTCPeerConnection | null = null; private iceCandidateQueue: RTCIceCandidateInit[] = []; private isProcessingQueue: boolean = false; constructor(private platform: IPlatformWebRTC) { super(); } private setupEventHandlers(): void { if (!this.pc) { throw new Error("Peer connection is not initialized"); } this.pc.ontrack = ({ streams }: any) => { console.log("[PeerConnectionManager] Remote track received: ", streams); if (streams && streams.length > 0) { this.emit("rtc:track-added", streams[0]); } }; this.pc.onicecandidate = ({ candidate }: any) => { if (!candidate) return; console.log("[PeerConnectionManager] ICE candidate generated"); this.emit("rtc:ice-candidate", candidate); }; this.pc.onconnectionstatechange = () => { const state = this.pc!.connectionState; console.log("[PeerConnectionManager] Connection state changed:", state); this.emit("rtc:connection-state-change", state); if (state === "failed" || state === "closed") { this.emit("rtc:error", new Error(`Connection state: ${state}`)); } }; this.pc.oniceconnectionstatechange = () => { const state = this.pc!.iceConnectionState; console.log("[PeerConnectionManager] ICE connection state changed:", state); this.emit("rtc:ice-connection-state-change", state); if (state === "failed") { this.emit("rtc:error", new Error("ICE connection failed")); } }; this.pc.onsignalingstatechange = () => { const state = this.pc!.signalingState; console.log("[PeerConnectionManager] Signaling state changed:", state); this.emit("rtc:signaling-state-change", state); }; } private async processIceCandidateQueue(): Promise { if (this.isProcessingQueue || !this.pc || !this.pc.remoteDescription) { return; } this.isProcessingQueue = true; try { const candidates = this.iceCandidateQueue.splice(0); const results = await Promise.allSettled( candidates.map(c => this.pc!.addIceCandidate(this.platform.createIceCandidate(c))) ); const failed = results.filter(r => r.status === 'rejected').length; if (failed > 0) { console.warn(`[PeerConnectionManager] ${failed}/${candidates.length} ICE candidates failed`); } else { console.log(`[PeerConnectionManager] Processed ${candidates.length} queued ICE candidates`); } } finally { this.isProcessingQueue = false; } } initialize(): void { if (this.pc) { console.warn("[PeerConnectionManager] Peer connection already exists"); return; } try { this.pc = this.platform.createPeerConnection(RTC_CONFIG); this.setupEventHandlers(); console.log("[PeerConnectionManager] Peer connection initialized"); } catch (error) { console.error("[PeerConnectionManager] Failed to create peer connection:", error); throw error; } } addLocalStream(stream: MediaStream): void { if (!this.pc) { throw new Error("Peer connection is not initialized"); } console.log("[PeerConnectionManager] Adding local stream to peer connection"); // 이미 추가된 트랙은 추가하지 않음 const existingSenders = this.pc.getSenders(); const existingTracks = existingSenders.map(sender => sender.track); stream.getTracks().forEach((track) => { const isAlreadyAdded = existingTracks.some( existingTrack => existingTrack?.id === track.id ); if (!isAlreadyAdded) { this.pc!.addTrack(track, stream); console.log(`[PeerConnectionManager] Added track: ${track.kind} (${track.id})`); } else { console.log(`[PeerConnectionManager] Track already added, skipping: ${track.kind} (${track.id})`); } }); } async createOffer(): Promise { if (!this.pc) { throw new Error("Peer connection is not initialized"); } try { const offer = await this.pc.createOffer(); await this.pc.setLocalDescription(offer); console.log("[PeerConnectionManager] Offer created and set as local description"); return offer; } catch (error) { console.error("[PeerConnectionManager] Failed to create offer:", error); throw error; } } async createAnswer(): Promise { if (!this.pc) { throw new Error("Peer connection is not initialized"); } try { const answer = await this.pc.createAnswer(); await this.pc.setLocalDescription(answer); console.log("[PeerConnectionManager] Answer created and set as local description"); return answer; } catch (error) { console.error("[PeerConnectionManager] Failed to create answer:", error); throw error; } } async setRemoteDescription(description: RTCSessionDescriptionInit): Promise { if (!this.pc) { throw new Error("Peer connection is not initialized"); } try { const remoteDesc = this.platform.createSessionDescription(description); await this.pc.setRemoteDescription(remoteDesc); console.log("[PeerConnectionManager] Remote description set successfully"); if (this.iceCandidateQueue.length > 0) { console.log("[PeerConnectionManager] Processing queued ICE candidates..."); await this.processIceCandidateQueue(); } } catch (error) { console.error("[PeerConnectionManager] Failed to set remote description:", error); throw error; } } async addIceCandidate(candidate: RTCIceCandidateInit): Promise { if (!this.pc) { throw new Error("Peer connection is not initialized"); } // Validate candidate if (!candidate || (typeof candidate.sdpMid !== 'string' && typeof candidate.sdpMLineIndex !== 'number')) { console.warn("[PeerConnectionManager] Invalid ICE candidate, skipping"); return; } // Skip end-of-candidates signal if (!candidate.candidate || candidate.candidate === "") { console.log("[PeerConnectionManager] End of ICE candidates"); return; } // Queue candidates if remote description is not set if (!this.pc.remoteDescription) { if (this.iceCandidateQueue.length >= RTC_CONFIG.maxIceCandidateQueueSize) { console.warn("[PeerConnectionManager] ICE candidate queue full, removing oldest"); this.iceCandidateQueue.shift(); } this.iceCandidateQueue.push(candidate); console.log(`[PeerConnectionManager] ICE candidate queued (${this.iceCandidateQueue.length} in queue)`); return; } // Add candidate immediately try { const iceCandidate = this.platform.createIceCandidate(candidate); await this.pc.addIceCandidate(iceCandidate); console.log("[PeerConnectionManager] ICE candidate added successfully"); } catch (error: any) { console.error("[PeerConnectionManager] Failed to add ICE candidate:", error); } } getConnectionState(): RTCPeerConnectionState { return this.pc?.connectionState || "closed"; } async restartIce(): Promise { if (!this.pc) { throw new Error("Peer connection is not initialized"); } try { console.log("[PlatformAdapter] Restarting ICE..."); const offer = await this.pc.createOffer({ iceRestart: true }); await this.pc.setLocalDescription(offer); console.log("[PlatformAdapter] ICE restart initiated"); return offer; } catch (error: any) { console.error("[PlatformAdapter] Failed to restart ICE:", error); throw error; } } close(): void { if (!this.pc) { return; } console.log("[PeerConnectionManager] Closing peer connection"); try { // 모든 receiver의 트랙 정지, sender는 정리 X. sender를 정리하면 자신의 track이 정지됨 this.pc.getReceivers().forEach(receiver => { if (receiver.track) { receiver.track.stop(); } }); if (this.pc.connectionState !== 'closed') { this.pc.close(); } } catch (error) { console.error("[PeerConnectionManager] Error closing peer connection:", error); } this.pc = null; this.iceCandidateQueue = []; } dispose(): void { this.close(); this.removeAllListeners(); } isClosed(): boolean { return !this.pc || this.pc.connectionState === "closed"; } }