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";
}
}