Merge pull request 'feat: add Metered turn server' (#16) from feature/TURN-server-integration into main

Reviewed-on: #16
This commit is contained in:
subhajit.pal 2026-08-05 13:10:22 +00:00
commit 4cae90c5ec
2 changed files with 260 additions and 79 deletions

View File

@ -13,6 +13,7 @@
<!-- Monaco Code Editor Loader & PeerJS Realtime WebRTC Call & SweetAlert2 --> <!-- Monaco Code Editor Loader & PeerJS Realtime WebRTC Call & SweetAlert2 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script> <script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<script src="//cdn.metered.ca/sdk/video/1.4.6/sdk.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
@ -1211,7 +1212,7 @@ function showCandidateJoinOption() {
if (joinBtn) joinBtn.style.display = 'none'; if (joinBtn) joinBtn.style.display = 'none';
if (!localMediaStream) { if (!localMediaStream || localMediaStream.getVideoTracks().length === 0) {
await startCandidateCameraStream(); await startCandidateCameraStream();
} }
@ -1224,7 +1225,6 @@ function showCandidateJoinOption() {
badge.style.color = '#34d399'; badge.style.color = '#34d399';
} }
// Dual 2-Way Connection: Answer incoming call AND establish connection to interviewer peer
const stream = localMediaStream || new MediaStream(); const stream = localMediaStream || new MediaStream();
if (activeCallInstance) { if (activeCallInstance) {
@ -1249,8 +1249,7 @@ function showCandidateJoinOption() {
const remoteVideo = document.getElementById('interviewer-video'); const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder'); const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo) { if (remoteVideo) {
remoteVideo.srcObject = remoteStream; safePlayMediaStream(remoteVideo, remoteStream);
remoteVideo.play().catch(e => console.log('Remote video play error:', e));
} }
if (placeholder) placeholder.style.display = 'none'; if (placeholder) placeholder.style.display = 'none';
}); });
@ -1267,7 +1266,60 @@ function showCandidateJoinOption() {
} }
} }
// 4. WebRTC Real-Time 2-Way Interview Call Setup (Dual Signaling: BroadcastChannel + PeerJS) function safePlayMediaStream(videoElem, stream, isSelf = false) {
if (!videoElem || !stream) return;
if (isSelf) {
videoElem.muted = true;
}
if (videoElem.srcObject !== stream) {
videoElem.srcObject = stream;
}
const playPromise = videoElem.play();
if (playPromise !== undefined) {
playPromise.catch(err => {
if (err.name === 'AbortError') {
videoElem.onloadedmetadata = () => {
videoElem.play().catch(() => {});
};
} else if (err.name === 'NotAllowedError') {
videoElem.play().catch(() => {});
} else {
console.log('Media play warning:', err);
}
});
}
}
// Metered TURN Server Integration (Pre-fetched for seamless WebRTC P2P + TURN relay)
const METERED_CREDENTIALS_URL = "https://single-login-system.metered.live/api/v1/turn/credentials?apiKey=ffc9146ac5c2f75d83d7c679d8553a21c8fa";
let globalIceServers = [
{ urls: 'stun:stun.l.google.com:19302' }
];
let meteredIceFetchPromise = null;
function initMeteredIceServers() {
if (meteredIceFetchPromise) return meteredIceFetchPromise;
meteredIceFetchPromise = fetch(METERED_CREDENTIALS_URL)
.then(r => r.json())
.then(meteredIce => {
if (Array.isArray(meteredIce) && meteredIce.length > 0) {
globalIceServers = [
{ urls: 'stun:stun.l.google.com:19302' },
...meteredIce.slice(0, 3)
];
}
return globalIceServers;
})
.catch(e => {
console.warn('Metered TURN fetch warning:', e);
return globalIceServers;
});
return meteredIceFetchPromise;
}
// Initiate TURN credentials pre-fetch immediately
initMeteredIceServers();
let localMediaStream = null; let localMediaStream = null;
let peerInstance = null; let peerInstance = null;
let candidatePeerConnection = null; let candidatePeerConnection = null;
@ -1319,7 +1371,22 @@ function showCandidateJoinOption() {
if (msg.type === 'interviewer_joined' || msg.type === 'offer') { if (msg.type === 'interviewer_joined' || msg.type === 'offer') {
latestCallStatus = 'active'; latestCallStatus = 'active';
if (isCandidateCallConnected && localMediaStream && peerInstance) {
const interviewerPeerId = 'interviewer_' + interviewId;
try {
const outCall = peerInstance.call(interviewerPeerId, localMediaStream);
if (outCall) {
outCall.on('stream', remoteStream => {
const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo) safePlayMediaStream(remoteVideo, remoteStream, false);
if (placeholder) placeholder.style.display = 'none';
});
}
} catch(e) {}
} else {
triggerCandidateRing(msg.offer || null); triggerCandidateRing(msg.offer || null);
}
} else if (msg.type === 'answer' && msg.sender !== 'candidate' && candidatePeerConnection) { } else if (msg.type === 'answer' && msg.sender !== 'candidate' && candidatePeerConnection) {
await candidatePeerConnection.setRemoteDescription(new RTCSessionDescription(msg.answer)); await candidatePeerConnection.setRemoteDescription(new RTCSessionDescription(msg.answer));
} else if (msg.type === 'ice-candidate' && msg.sender !== 'candidate' && candidatePeerConnection && msg.candidate) { } else if (msg.type === 'ice-candidate' && msg.sender !== 'candidate' && candidatePeerConnection && msg.candidate) {
@ -1341,13 +1408,10 @@ function showCandidateJoinOption() {
async function createCandidatePeerConnection() { async function createCandidatePeerConnection() {
if (candidatePeerConnection) return candidatePeerConnection; if (candidatePeerConnection) return candidatePeerConnection;
await initMeteredIceServers();
candidatePeerConnection = new RTCPeerConnection({ candidatePeerConnection = new RTCPeerConnection({
iceServers: [ iceServers: globalIceServers
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
}); });
if (localMediaStream) { if (localMediaStream) {
@ -1360,8 +1424,7 @@ function showCandidateJoinOption() {
const remoteVideo = document.getElementById('interviewer-video'); const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder'); const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo && event.streams[0]) { if (remoteVideo && event.streams[0]) {
remoteVideo.srcObject = event.streams[0]; safePlayMediaStream(remoteVideo, event.streams[0]);
remoteVideo.play().catch(e => console.log(e));
} }
if (placeholder) placeholder.style.display = 'none'; if (placeholder) placeholder.style.display = 'none';
@ -1373,7 +1436,7 @@ function showCandidateJoinOption() {
} }
}; };
candidatePeerConnection.onconnectionstatechange = () => { candidatePeerConnection.onconnectionstatechange = async () => {
const state = candidatePeerConnection.connectionState; const state = candidatePeerConnection.connectionState;
if (state === 'connected') { if (state === 'connected') {
const badge = document.getElementById('call-status-badge'); const badge = document.getElementById('call-status-badge');
@ -1382,7 +1445,9 @@ function showCandidateJoinOption() {
badge.style.background = 'rgba(16,185,129,0.3)'; badge.style.background = 'rgba(16,185,129,0.3)';
badge.style.color = '#34d399'; badge.style.color = '#34d399';
} }
} else if (state === 'disconnected' || state === 'failed' || state === 'closed') { } else if (state === 'failed' || state === 'disconnected') {
console.warn('Candidate P2P Connection State:', state);
} else if (state === 'closed') {
candidateLeaveCall(); candidateLeaveCall();
} }
}; };
@ -1411,8 +1476,9 @@ function showCandidateJoinOption() {
callSigChannel.postMessage({ type: 'answer', answer: answer, sender: 'candidate' }); callSigChannel.postMessage({ type: 'answer', answer: answer, sender: 'candidate' });
} }
function initCandidatePeer() { async function initCandidatePeer() {
if (typeof Peer === 'undefined') return; if (typeof Peer === 'undefined') return;
await initMeteredIceServers();
const peerId = 'cand_' + interviewId; const peerId = 'cand_' + interviewId;
if (peerInstance) { if (peerInstance) {
@ -1421,11 +1487,7 @@ function initCandidatePeer() {
peerInstance = new Peer(peerId, { peerInstance = new Peer(peerId, {
config: { config: {
iceServers: [ iceServers: globalIceServers
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
} }
}); });
@ -1441,15 +1503,11 @@ function initCandidatePeer() {
triggerCandidateRing(); triggerCandidateRing();
} }
const sendStream = localMediaStream || new MediaStream();
call.answer(sendStream);
call.on('stream', remoteStream => { call.on('stream', remoteStream => {
const remoteVideo = document.getElementById('interviewer-video'); const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder'); const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo) { if (remoteVideo) {
remoteVideo.srcObject = remoteStream; safePlayMediaStream(remoteVideo, remoteStream);
remoteVideo.play().catch(e => console.log('Candidate remote video play error:', e));
} }
if (placeholder) placeholder.style.display = 'none'; if (placeholder) placeholder.style.display = 'none';
@ -1462,33 +1520,57 @@ function initCandidatePeer() {
} }
} }
}); });
if (localMediaStream && localMediaStream.getVideoTracks().length > 0) {
try { call.answer(localMediaStream); } catch(e) {}
}
call.on('close', () => { call.on('close', () => {
candidateLeaveCall(); candidateLeaveCall();
}); });
}); });
peerInstance.on('error', async err => {
console.warn('Candidate PeerJS error:', err);
if (err.type === 'peer-unavailable' || err.type === 'network' || err.type === 'webrtc') {
const meteredServers = await fetchMeteredTurnIceServers();
console.log('Re-initializing Candidate PeerJS with Metered TURN Relay Servers...');
if (peerInstance) {
try { peerInstance.destroy(); } catch(e) {}
}
peerInstance = new Peer(peerId, {
config: { iceServers: meteredServers }
});
}
});
} }
function startCandidateCameraStream() { function startCandidateCameraStream() {
const avatarPlaceholder = document.getElementById('webcam-avatar-placeholder'); const avatarPlaceholder = document.getElementById('webcam-avatar-placeholder');
const audioConstraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
};
return navigator.mediaDevices.getUserMedia({ return navigator.mediaDevices.getUserMedia({
video: { video: {
width: { ideal: 1280 }, width: { ideal: 1280 },
height: { ideal: 720 }, height: { ideal: 720 },
facingMode: 'user' facingMode: 'user'
}, },
audio: true audio: audioConstraints
}) })
.catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: false })) .catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: audioConstraints }))
.catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: true }))
.catch(() => navigator.mediaDevices.getUserMedia({ video: false, audio: true })) .catch(() => navigator.mediaDevices.getUserMedia({ video: false, audio: true }))
.catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: false }))
.then(stream => { .then(stream => {
localMediaStream = stream; localMediaStream = stream;
const vid = document.getElementById('webcam'); const vid = document.getElementById('webcam');
if (vid) { if (vid) {
vid.muted = true; vid.muted = true;
vid.srcObject = stream; safePlayMediaStream(vid, stream, true);
vid.play().catch(e => console.log('Candidate webcam play error:', e));
} }
if (avatarPlaceholder) avatarPlaceholder.style.display = 'none'; if (avatarPlaceholder) avatarPlaceholder.style.display = 'none';

View File

@ -10,6 +10,7 @@
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script> <script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<script src="//cdn.metered.ca/sdk/video/1.4.6/sdk.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style> <style>
@ -1292,7 +1293,6 @@ function openReviewModal(id, name, uid, autoJoinCall = false) {
document.getElementById('review-modal').classList.add('active'); document.getElementById('review-modal').classList.add('active');
resetZoomScreen(); resetZoomScreen();
initInterviewerPeer();
switchIdeTab('code'); switchIdeTab('code');
subscribeLiveSync(id); subscribeLiveSync(id);
@ -1686,25 +1686,106 @@ function blockCandidateAction() {
} }
} }
function initInterviewerPeer() { // Metered TURN Server Integration (Pre-fetched for seamless WebRTC P2P + TURN relay)
const METERED_CREDENTIALS_URL = "https://single-login-system.metered.live/api/v1/turn/credentials?apiKey=ffc9146ac5c2f75d83d7c679d8553a21c8fa";
let globalIceServers = [
{ urls: 'stun:stun.l.google.com:19302' }
];
let meteredIceFetchPromise = null;
function initMeteredIceServers() {
if (meteredIceFetchPromise) return meteredIceFetchPromise;
meteredIceFetchPromise = fetch(METERED_CREDENTIALS_URL)
.then(r => r.json())
.then(meteredIce => {
if (Array.isArray(meteredIce) && meteredIce.length > 0) {
globalIceServers = [
{ urls: 'stun:stun.l.google.com:19302' },
...meteredIce.slice(0, 3)
];
}
return globalIceServers;
})
.catch(e => {
console.warn('Metered TURN fetch warning:', e);
return globalIceServers;
});
return meteredIceFetchPromise;
}
// Initiate TURN credentials pre-fetch immediately
initMeteredIceServers();
function safePlayMediaStream(videoElem, stream, isSelf = false) {
if (!videoElem || !stream) return;
if (isSelf) {
videoElem.muted = true;
}
if (videoElem.srcObject !== stream) {
videoElem.srcObject = stream;
}
const playPromise = videoElem.play();
if (playPromise !== undefined) {
playPromise.catch(err => {
if (err.name === 'AbortError') {
videoElem.onloadedmetadata = () => {
videoElem.play().catch(() => {});
};
} else if (err.name === 'NotAllowedError') {
videoElem.play().catch(() => {});
} else {
console.log('Media play warning:', err);
}
});
}
}
async function initInterviewerPeer() {
if (!currentInterviewId || typeof Peer === 'undefined') return; if (!currentInterviewId || typeof Peer === 'undefined') return;
await initMeteredIceServers();
const peerId = 'interviewer_' + currentInterviewId; const peerId = 'interviewer_' + currentInterviewId;
interviewerPeer = new Peer(peerId); if (interviewerPeer && !interviewerPeer.destroyed) {
try { interviewerPeer.destroy(); } catch(e) {}
}
interviewerPeer = new Peer(peerId, {
config: {
iceServers: globalIceServers
}
});
interviewerPeer.on('open', id => { interviewerPeer.on('open', id => {
console.log('Interviewer WebRTC Peer Registered:', id); console.log('Interviewer WebRTC Peer Registered:', id);
}); });
interviewerPeer.on('call', call => {
activeCall = call;
const sendStream = interviewerLocalStream || new MediaStream();
call.answer(sendStream);
call.on('stream', candRemoteStream => {
const candVid = document.getElementById('interviewer-cand-video');
const placeholder = document.getElementById('cand-video-placeholder');
if (candVid) {
safePlayMediaStream(candVid, candRemoteStream);
}
if (placeholder) placeholder.style.display = 'none';
document.getElementById('interviewer-call-status').innerText = 'CONNECTED (LIVE)';
document.getElementById('interviewer-call-status').style.background = 'rgba(16,185,129,0.3)';
document.getElementById('interviewer-call-status').style.color = '#34d399';
});
});
// Listen for candidate screen share peer stream with STUN iceServers // Listen for candidate screen share peer stream with STUN iceServers
const screenPeerId = 'interviewer_screen_' + currentInterviewId; const screenPeerId = 'interviewer_screen_' + currentInterviewId;
if (screenReceiverPeer && !screenReceiverPeer.destroyed) {
try { screenReceiverPeer.destroy(); } catch(e) {}
}
screenReceiverPeer = new Peer(screenPeerId, { screenReceiverPeer = new Peer(screenPeerId, {
config: { config: {
iceServers: [ iceServers: globalIceServers
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
} }
}); });
screenReceiverPeer.on('call', call => { screenReceiverPeer.on('call', call => {
@ -1712,7 +1793,7 @@ function initInterviewerPeer() {
call.on('stream', screenStream => { call.on('stream', screenStream => {
const screenVid = document.getElementById('interviewer-screen-video'); const screenVid = document.getElementById('interviewer-screen-video');
const placeholder = document.getElementById('screen-video-placeholder'); const placeholder = document.getElementById('screen-video-placeholder');
if (screenVid) screenVid.srcObject = screenStream; if (screenVid) safePlayMediaStream(screenVid, screenStream);
if (placeholder) placeholder.style.display = 'none'; if (placeholder) placeholder.style.display = 'none';
}); });
}); });
@ -1767,13 +1848,10 @@ function initInterviewerSigChannel() {
async function createInterviewerPeerConnection() { async function createInterviewerPeerConnection() {
if (interviewerPeerConnection) return interviewerPeerConnection; if (interviewerPeerConnection) return interviewerPeerConnection;
await initMeteredIceServers();
interviewerPeerConnection = new RTCPeerConnection({ interviewerPeerConnection = new RTCPeerConnection({
iceServers: [ iceServers: globalIceServers
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
}); });
if (interviewerLocalStream) { if (interviewerLocalStream) {
@ -1786,8 +1864,7 @@ function initInterviewerSigChannel() {
const candVid = document.getElementById('interviewer-cand-video'); const candVid = document.getElementById('interviewer-cand-video');
const placeholder = document.getElementById('cand-video-placeholder'); const placeholder = document.getElementById('cand-video-placeholder');
if (candVid && event.streams[0]) { if (candVid && event.streams[0]) {
candVid.srcObject = event.streams[0]; safePlayMediaStream(candVid, event.streams[0]);
candVid.play().catch(e => console.log(e));
} }
if (placeholder) placeholder.style.display = 'none'; if (placeholder) placeholder.style.display = 'none';
@ -1796,6 +1873,20 @@ function initInterviewerSigChannel() {
document.getElementById('interviewer-call-status').style.color = '#34d399'; document.getElementById('interviewer-call-status').style.color = '#34d399';
}; };
interviewerPeerConnection.onconnectionstatechange = async () => {
const state = interviewerPeerConnection.connectionState;
if (state === 'connected') {
const badge = document.getElementById('interviewer-call-status');
if (badge) {
badge.innerText = 'CONNECTED (LIVE)';
badge.style.background = 'rgba(16,185,129,0.3)';
badge.style.color = '#34d399';
}
} else if (state === 'failed' || state === 'disconnected') {
console.warn('Interviewer P2P Connection State:', state);
}
};
interviewerPeerConnection.onicecandidate = (event) => { interviewerPeerConnection.onicecandidate = (event) => {
if (event.candidate) { if (event.candidate) {
interviewerSigChannel.postMessage({ type: 'ice-candidate', candidate: event.candidate, sender: 'interviewer' }); interviewerSigChannel.postMessage({ type: 'ice-candidate', candidate: event.candidate, sender: 'interviewer' });
@ -1854,8 +1945,7 @@ function startInterviewerCall(isRejoin = false) {
const selfVid = document.getElementById('interviewer-self-video'); const selfVid = document.getElementById('interviewer-self-video');
if (selfVid) { if (selfVid) {
selfVid.muted = true; selfVid.muted = true;
selfVid.srcObject = stream; safePlayMediaStream(selfVid, stream, true);
selfVid.play().catch(e => console.log(e));
} }
document.getElementById('self-video-placeholder').style.display = 'none'; document.getElementById('self-video-placeholder').style.display = 'none';
@ -1870,23 +1960,13 @@ function startInterviewerCall(isRejoin = false) {
interviewerSigChannel.postMessage({ type: 'interviewer_joined', sender: 'interviewer' }); interviewerSigChannel.postMessage({ type: 'interviewer_joined', sender: 'interviewer' });
} }
// PeerJS fallback // PeerJS connection
const panelistPeerId = 'interviewer_' + currentInterviewId + '_' + Math.floor(Math.random() * 10000); if (!interviewerPeer || interviewerPeer.destroyed) {
if (interviewerPeer) { initInterviewerPeer();
try { interviewerPeer.destroy(); } catch(e) {}
} }
interviewerPeer = new Peer(panelistPeerId, { const makePeerJSCall = () => {
config: { if (!interviewerPeer || interviewerPeer.destroyed) return;
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
}
});
interviewerPeer.on('open', () => {
const candPeerId = 'cand_' + currentInterviewId; const candPeerId = 'cand_' + currentInterviewId;
activeCall = interviewerPeer.call(candPeerId, stream); activeCall = interviewerPeer.call(candPeerId, stream);
@ -1895,28 +1975,38 @@ function startInterviewerCall(isRejoin = false) {
const candVid = document.getElementById('interviewer-cand-video'); const candVid = document.getElementById('interviewer-cand-video');
const placeholder = document.getElementById('cand-video-placeholder'); const placeholder = document.getElementById('cand-video-placeholder');
if (candVid) { if (candVid) {
candVid.srcObject = candRemoteStream; safePlayMediaStream(candVid, candRemoteStream);
candVid.play().catch(e => console.log('Interviewer video play error:', e));
} }
if (placeholder) placeholder.style.display = 'none'; if (placeholder) placeholder.style.display = 'none';
candRemoteStream.onaddtrack = () => { candRemoteStream.onaddtrack = () => {
if (placeholder) placeholder.style.display = 'none'; if (placeholder) placeholder.style.display = 'none';
if (candVid) candVid.play().catch(e => {}); if (candVid) safePlayMediaStream(candVid, candRemoteStream);
}; };
document.getElementById('interviewer-call-status').innerText = 'CONNECTED (LIVE)'; document.getElementById('interviewer-call-status').innerText = 'CONNECTED (LIVE)';
document.getElementById('interviewer-call-status').style.background = 'rgba(16,185,129,0.3)'; document.getElementById('interviewer-call-status').style.background = 'rgba(16,185,129,0.3)';
document.getElementById('interviewer-call-status').style.color = '#34d399'; document.getElementById('interviewer-call-status').style.color = '#34d399';
}); });
activeCall.on('close', () => {
console.log('PeerJS media call channel closed. Keeping interviewer local stream active for auto-reconnect.');
});
} }
});
}; };
if (interviewerPeer && interviewerPeer.open) {
makePeerJSCall();
} else if (interviewerPeer) {
interviewerPeer.on('open', makePeerJSCall);
}
};
const audioConstraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
};
navigator.mediaDevices.getUserMedia({ video: true, audio: audioConstraints })
.then(handleStream)
.catch(() => {
navigator.mediaDevices.getUserMedia({ video: true, audio: true }) navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(handleStream) .then(handleStream)
.catch(() => { .catch(() => {
@ -1928,6 +2018,7 @@ function startInterviewerCall(isRejoin = false) {
.catch(err => console.log('WebRTC Stream init:', err)); .catch(err => console.log('WebRTC Stream init:', err));
}); });
}); });
});
} }
function leaveInterviewerCall(isExplicitEnd = true) { function leaveInterviewerCall(isExplicitEnd = true) {
@ -1977,6 +2068,14 @@ function leaveInterviewerCall(isExplicitEnd = true) {
try { activeCall.close(); } catch(e) {} try { activeCall.close(); } catch(e) {}
activeCall = null; activeCall = null;
} }
if (interviewerPeer) {
try { interviewerPeer.destroy(); } catch(e) {}
interviewerPeer = null;
}
if (screenReceiverPeer) {
try { screenReceiverPeer.destroy(); } catch(e) {}
screenReceiverPeer = null;
}
const candPlace = document.getElementById('cand-video-placeholder'); const candPlace = document.getElementById('cand-video-placeholder');
const screenPlace = document.getElementById('screen-video-placeholder'); const screenPlace = document.getElementById('screen-video-placeholder');