// webrtcManager.js (수정본 — STUN 없이 ICE만) export class WebRTCManager { constructor(signalUrl) { this.pc = null; this.localStream = null; this.remoteStream = null; this.onRemoteStream = null; this.signalUrl = signalUrl; // 시그널링 서버 주소 this.ws = null; this.myId = null; this.peerId = null; // 상대방 ID (수동 입력 또는 자동 할당) } async init() { await this.requestMicPermission() this.setupSignaling(); } async requestMicPermission() { try { const stream = await navigator.mediaDevices.getUserMedia({audio: true}); console.log('✅ 마이크 접근 성공'); stream.getTracks().forEach(track => track.stop()); // 임시로 열었다가 닫음 return true; } catch (err) { console.error('❌ 마이크 권한 거부 또는 장치 없음:', err.message); alert('마이크 접근이 필요합니다. 브라우저 설정에서 마이크 권한을 허용해주세요.'); return false; } } setupSignaling() { this.ws = new WebSocket(this.signalUrl); this.ws.onopen = () => { console.log('🔌 시그널링 서버 연결됨'); }; this.ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'your-id') { this.myId = msg.id; console.log('내 ID:', this.myId); } else if (msg.type === 'offer') { this.setRemoteDescription(msg.sdp); this.addIceCandidateQueue.forEach(c => this.pc.addIceCandidate(c)); this.addIceCandidateQueue = []; } else if (msg.type === 'answer') { this.setRemoteDescription(msg.sdp); } else if (msg.type === 'candidate') { this.addIceCandidate(msg.candidate); } }; } createPeerConnection() { // ✅ STUN 서버 없이 빈 설정 — 인트라넷 내부 IP로 직접 연결 this.pc = new RTCPeerConnection({}); this.localStream.getTracks().forEach(track => { this.pc.addTrack(track, this.localStream); }); this.pc.ontrack = (event) => { this.remoteStream = event.streams[0]; if (this.onRemoteStream) this.onRemoteStream(this.remoteStream); }; // ICE Candidate → 시그널링으로 전송 this.pc.onicecandidate = (event) => { if (event.candidate) { console.log('📤 ICE Candidate:', event.candidate.candidate); this.ws.send(JSON.stringify({ target: this.peerId, type: 'candidate', candidate: event.candidate })); } }; } async createOffer(targetId) { this.peerId = targetId; this.createPeerConnection(); const offer = await this.pc.createOffer(); await this.pc.setLocalDescription(offer); this.ws.send(JSON.stringify({ target: targetId, type: 'offer', sdp: offer })); console.log('📤 Offer 전송 완료'); } async setRemoteDescription(sdp) { await this.pc.setRemoteDescription(new RTCSessionDescription(sdp)); if (sdp.type === 'offer') { const answer = await this.pc.createAnswer(); await this.pc.setLocalDescription(answer); this.ws.send(JSON.stringify({ target: this.peerId || 'unknown', type: 'answer', sdp: answer })); } } async addIceCandidate(candidate) { if (this.pc && this.pc.remoteDescription) { await this.pc.addIceCandidate(new RTCIceCandidate(candidate)); } else { // SDP 설정 전이라면 큐에 저장 this.addIceCandidateQueue = this.addIceCandidateQueue || []; this.addIceCandidateQueue.push(candidate); } } hangup() { this.pc?.close(); this.localStream?.getTracks().forEach(t => t.stop()); this.ws?.close(); console.log('📞 통화 종료'); } }