export class WebRTCTransport { private pc: RTCPeerConnection | null = null; private dc: RTCDataChannel | null = null; private iceServers: RTCIceServer[]; constructor(iceServers: RTCIceServer[]) { this.iceServers = iceServers; } async createOffer(): Promise<{ sdp: string; candidates: RTCIceCandidate[] }> { this.pc = new RTCPeerConnection({ iceServers: this.iceServers }); this.dc = this.pc.createDataChannel('filedrop', { ordered: true }); const offer = await this.pc.createOffer(); await this.pc.setLocalDescription(offer); const candidates = await this.waitForIceGathering(); return { sdp: this.pc.localDescription?.sdp || '', candidates, }; } async handleAnswer(answer: { sdp: string }) { if (!this.pc) return; await this.pc.setRemoteDescription( new RTCSessionDescription({ type: 'answer', sdp: answer.sdp }) ); } async handleCandidate(candidate: RTCIceCandidateInit) { await this.pc?.addIceCandidate(candidate); } onDataChannel(callback: (dc: RTCDataChannel) => void) { if (this.pc) { this.pc.ondatachannel = (e) => callback(e.channel); } } getDataChannel() { return this.dc; } private waitForIceGathering(): Promise { return new Promise((resolve) => { const candidates: RTCIceCandidate[] = []; if (!this.pc) return resolve(candidates); this.pc.onicecandidate = (e) => { if (e.candidate) candidates.push(e.candidate); }; this.pc.onicegatheringstatechange = () => { if (this.pc?.iceGatheringState === 'complete') { resolve(candidates); } }; setTimeout(() => resolve(candidates), 5000); }); } close() { this.dc?.close(); this.pc?.close(); this.dc = null; this.pc = null; } }