mirror of
https://git.hmsn.ink/call/client.git
synced 2026-03-19 15:54:58 +09:00
기능 추가
This commit is contained in:
@@ -10,11 +10,12 @@ const hangupButton = document.getElementById('hangup-btn');
|
|||||||
const recordButton = document.getElementById('record-btn');
|
const recordButton = document.getElementById('record-btn');
|
||||||
const stopRecordButton = document.getElementById('stop-record-btn');
|
const stopRecordButton = document.getElementById('stop-record-btn');
|
||||||
const audioPlayback = document.getElementById('audio-playback');
|
const audioPlayback = document.getElementById('audio-playback');
|
||||||
|
const peerIdInput = document.getElementById('peer-id-input');
|
||||||
|
|
||||||
// 인스턴스 생성
|
// 인스턴스 생성
|
||||||
const rtcManager = new WebRTCManager('http://localhost:3001');
|
const rtcManager = new WebRTCManager('http://localhost:3001');
|
||||||
const recorder = new AudioRecorder();
|
const recorder = new AudioRecorder();
|
||||||
const ui = new UIController({ callButton, hangupButton, recordButton, stopRecordButton, audioPlayback });
|
const ui = new UIController({ callButton, hangupButton, recordButton, stopRecordButton, audioPlayback, peerIdInput });
|
||||||
|
|
||||||
// UI 이벤트 바인딩
|
// UI 이벤트 바인딩
|
||||||
ui.bindEvents(rtcManager, recorder);
|
ui.bindEvents(rtcManager, recorder);
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export class UIController {
|
|||||||
const peerId = this.peerIdInput.value.trim();
|
const peerId = this.peerIdInput.value.trim();
|
||||||
if (!peerId) return alert('상대방 ID를 입력하세요');
|
if (!peerId) return alert('상대방 ID를 입력하세요');
|
||||||
rtcManager.createOffer(peerId);
|
rtcManager.createOffer(peerId);
|
||||||
|
console.log(rtcManager)
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 통화 종료
|
// 통화 종료
|
||||||
@@ -24,6 +26,7 @@ export class UIController {
|
|||||||
// 녹음 시작
|
// 녹음 시작
|
||||||
this.recordButton.onclick = () => {
|
this.recordButton.onclick = () => {
|
||||||
const stream = rtcManager.remoteStream || rtcManager.localStream;
|
const stream = rtcManager.remoteStream || rtcManager.localStream;
|
||||||
|
console.log(stream)
|
||||||
if (!stream) return alert('스트림이 없습니다');
|
if (!stream) return alert('스트림이 없습니다');
|
||||||
recorder.start(stream);
|
recorder.start(stream);
|
||||||
};
|
};
|
||||||
@@ -45,6 +48,10 @@ export class UIController {
|
|||||||
// 원격 스트림 수신 시 자동 재생
|
// 원격 스트림 수신 시 자동 재생
|
||||||
rtcManager.onRemoteStream = (stream) => {
|
rtcManager.onRemoteStream = (stream) => {
|
||||||
const audio = new Audio();
|
const audio = new Audio();
|
||||||
|
console.log(rtcManager)
|
||||||
|
console.log(stream)
|
||||||
|
audio.muted = false;
|
||||||
|
audio.volume = 1;
|
||||||
audio.srcObject = stream;
|
audio.srcObject = stream;
|
||||||
audio.play().catch(e => console.warn('자동 재생 실패:', e));
|
audio.play().catch(e => console.warn('자동 재생 실패:', e));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,23 +10,42 @@ export class WebRTCManager {
|
|||||||
this.ws = null;
|
this.ws = null;
|
||||||
this.myId = null;
|
this.myId = null;
|
||||||
this.peerId = null; // 상대방 ID (수동 입력 또는 자동 할당)
|
this.peerId = null; // 상대방 ID (수동 입력 또는 자동 할당)
|
||||||
|
this.isInitialized = false; // ✅ 설정
|
||||||
|
this.addIceCandidateQueue = []; // ❗ 이전 코드에서 선언 누락 → 여기 추가
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.requestMicPermission()
|
try {
|
||||||
|
await this.acquireLocalStream(); // ✅ 이름도 의미에 맞게 변경
|
||||||
this.setupSignaling();
|
this.setupSignaling();
|
||||||
|
console.log('✅ WebRTC 매니저 초기화 완료');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ WebRTC 초기화 실패:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestMicPermission() {
|
async acquireLocalStream() {
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
|
// ✅ 실제 사용할 스트림을 획득하고 저장
|
||||||
console.log('✅ 마이크 접근 성공');
|
this.localStream = await navigator.mediaDevices.getUserMedia({
|
||||||
stream.getTracks().forEach(track => track.stop()); // 임시로 열었다가 닫음
|
audio: {
|
||||||
return true;
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true,
|
||||||
|
autoGainControl: true,
|
||||||
|
sampleRate: 48000,
|
||||||
|
channelCount: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
console.log('🎤 마이크 스트림 획득 및 저장 완료');
|
||||||
|
console.log('✅ 획득한 트랙 수:', this.localStream.getTracks().length); // → 1 이상이어야 함
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ 마이크 권한 거부 또는 장치 없음:', err.message);
|
this.isInitialized = false;
|
||||||
|
console.error('❌ 마이크 스트림 획득 실패:', err.message);
|
||||||
alert('마이크 접근이 필요합니다. 브라우저 설정에서 마이크 권한을 허용해주세요.');
|
alert('마이크 접근이 필요합니다. 브라우저 설정에서 마이크 권한을 허용해주세요.');
|
||||||
return false;
|
throw err; // 상위에서 처리하도록
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,18 +63,34 @@ export class WebRTCManager {
|
|||||||
console.log('내 ID:', this.myId);
|
console.log('내 ID:', this.myId);
|
||||||
} else if (msg.type === 'offer') {
|
} else if (msg.type === 'offer') {
|
||||||
this.setRemoteDescription(msg.sdp);
|
this.setRemoteDescription(msg.sdp);
|
||||||
this.addIceCandidateQueue.forEach(c => this.pc.addIceCandidate(c));
|
// ❗ 큐 처리 전에 pc가 생성되어 있어야 함 → createOffer 또는 setRemoteDescription에서 pc 생성됨
|
||||||
|
if (this.addIceCandidateQueue?.length) {
|
||||||
|
this.addIceCandidateQueue.forEach(c => this.addIceCandidate(c));
|
||||||
this.addIceCandidateQueue = [];
|
this.addIceCandidateQueue = [];
|
||||||
|
}
|
||||||
} else if (msg.type === 'answer') {
|
} else if (msg.type === 'answer') {
|
||||||
this.setRemoteDescription(msg.sdp);
|
this.setRemoteDescription(msg.sdp);
|
||||||
} else if (msg.type === 'candidate') {
|
} else if (msg.type === 'candidate') {
|
||||||
this.addIceCandidate(msg.candidate);
|
this.addIceCandidate(msg.candidate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (err) => {
|
||||||
|
console.error('WebSocket 오류:', err);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createPeerConnection() {
|
createPeerConnection() {
|
||||||
// ✅ STUN 서버 없이 빈 설정 — 인트라넷 내부 IP로 직접 연결
|
if (!this.isInitialized || !this.localStream) {
|
||||||
|
throw new Error('RTC가 초기화되지 않았거나 마이크 스트림이 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 이미 pc가 있으면 재생성 방지 (옵션)
|
||||||
|
if (this.pc) {
|
||||||
|
console.warn('⚠️ PeerConnection이 이미 존재합니다. 기존 연결을 종료하고 새로 생성합니다.');
|
||||||
|
this.pc.close();
|
||||||
|
}
|
||||||
|
|
||||||
this.pc = new RTCPeerConnection({});
|
this.pc = new RTCPeerConnection({});
|
||||||
|
|
||||||
this.localStream.getTracks().forEach(track => {
|
this.localStream.getTracks().forEach(track => {
|
||||||
@@ -67,10 +102,8 @@ export class WebRTCManager {
|
|||||||
if (this.onRemoteStream) this.onRemoteStream(this.remoteStream);
|
if (this.onRemoteStream) this.onRemoteStream(this.remoteStream);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ICE Candidate → 시그널링으로 전송
|
|
||||||
this.pc.onicecandidate = (event) => {
|
this.pc.onicecandidate = (event) => {
|
||||||
if (event.candidate) {
|
if (event.candidate && this.ws?.readyState === WebSocket.OPEN) {
|
||||||
console.log('📤 ICE Candidate:', event.candidate.candidate);
|
|
||||||
this.ws.send(JSON.stringify({
|
this.ws.send(JSON.stringify({
|
||||||
target: this.peerId,
|
target: this.peerId,
|
||||||
type: 'candidate',
|
type: 'candidate',
|
||||||
@@ -78,9 +111,23 @@ export class WebRTCManager {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.pc.oniceconnectionstatechange = () => {
|
||||||
|
console.log('❄️ ICE 상태:', this.pc.iceConnectionState);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pc.onconnectionstatechange = () => {
|
||||||
|
console.log('🔌 연결 상태:', this.pc.connectionState);
|
||||||
|
};
|
||||||
|
console.log('로컬 스트림 트랙 수:', this.localStream.getTracks().length);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOffer(targetId) {
|
async createOffer(targetId) {
|
||||||
|
// ✅ 추가 검사
|
||||||
|
if (!this.isInitialized || !this.localStream) {
|
||||||
|
throw new Error('마이크 스트림이 준비되지 않았습니다. 초기화를 완료해주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
this.peerId = targetId;
|
this.peerId = targetId;
|
||||||
this.createPeerConnection();
|
this.createPeerConnection();
|
||||||
const offer = await this.pc.createOffer();
|
const offer = await this.pc.createOffer();
|
||||||
@@ -92,18 +139,30 @@ export class WebRTCManager {
|
|||||||
sdp: offer
|
sdp: offer
|
||||||
}));
|
}));
|
||||||
console.log('📤 Offer 전송 완료');
|
console.log('📤 Offer 전송 완료');
|
||||||
|
console.log('📤 Offer SDP:', offer.sdp);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRemoteDescription(sdp) {
|
async setRemoteDescription(sdp) {
|
||||||
|
// ✅ PeerConnection이 없으면 자동 생성 (수신 측 대응)
|
||||||
|
if (!this.pc) {
|
||||||
|
this.createPeerConnection(); // ← pc 생성 + localStream 트랙 추가
|
||||||
|
}
|
||||||
|
|
||||||
await this.pc.setRemoteDescription(new RTCSessionDescription(sdp));
|
await this.pc.setRemoteDescription(new RTCSessionDescription(sdp));
|
||||||
|
|
||||||
if (sdp.type === 'offer') {
|
if (sdp.type === 'offer') {
|
||||||
const answer = await this.pc.createAnswer();
|
const answer = await this.pc.createAnswer();
|
||||||
await this.pc.setLocalDescription(answer);
|
await this.pc.setLocalDescription(answer);
|
||||||
|
|
||||||
this.ws.send(JSON.stringify({
|
this.ws.send(JSON.stringify({
|
||||||
target: this.peerId || 'unknown',
|
target: this.peerId || 'unknown',
|
||||||
type: 'answer',
|
type: 'answer',
|
||||||
sdp: answer
|
sdp: answer
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log('📥 Answer 전송 완료');
|
||||||
|
console.log('로컬 스트림 트랙 수:', this.localStream.getTracks().length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user