Ramble-FE / services / webrtc / webrtc-connection-manager.ts
webrtc-connection-manager.ts
Raw
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<RTCConnectionEventMap> 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<void> {
        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<RTCSessionDescriptionInit> {
        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<RTCSessionDescriptionInit> {
        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<void> {
        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<void> {
        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<RTCSessionDescriptionInit> {
        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";
    }

}