feat: add Metered turn server

This commit is contained in:
kushal.saha 2026-08-05 12:55:08 +00:00
parent bc0d249dba
commit 1f22185ecf
2 changed files with 260 additions and 79 deletions

View File

@ -13,6 +13,7 @@
<!-- 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://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://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
@ -428,7 +429,7 @@
javascript: "// JavaScript (Node.js) Solution\nconsole.log(\"Hello from SingleLogin Assessment!\");\nconst nums = [10, 20, 30];\nconsole.log(\"Total:\", nums.reduce((a, b) => a + b, 0));"
};
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' } });
require(['vs/editor/editor.main'], function () {
const initialLang = '{{ strtolower($interview->language) }}';
@ -592,7 +593,7 @@ function captureAndUploadTabScreenshot(reason = 'tab_switch') {
let drewScreenStream = false;
const isCallStart = (reason === 'call_start');
const bannerLabel = isCallStart
const bannerLabel = isCallStart
? '📸 CALL STARTED • CANDIDATE SYSTEM SCREEN SNAPSHOT • ' + new Date().toLocaleString()
: '🚨 CANDIDATE SYSTEM COMPUTER DESKTOP SNAPSHOT • ' + new Date().toLocaleString();
@ -697,7 +698,7 @@ function fallbackCompositeSnapshot(reason = 'tab_switch') {
tempCtx.fillRect(0, tempCanvas.height - 42, tempCanvas.width, 42);
tempCtx.fillStyle = isCallStart ? '#10b981' : '#ef4444';
tempCtx.font = 'bold 14px sans-serif';
const text = isCallStart
const text = isCallStart
? '📸 CALL STARTED • CANDIDATE SYSTEM SNAPSHOT • ' + new Date().toLocaleString()
: '🚨 CANDIDATE SYSTEM SNAPSHOT • ' + new Date().toLocaleString();
tempCtx.fillText(text, 16, tempCanvas.height - 15);
@ -1016,7 +1017,7 @@ class CandRingtoneEngine {
this.init();
if (this.isPlaying) return;
this.isPlaying = true;
this.playTone();
if (this.interval) clearInterval(this.interval);
this.interval = setInterval(() => {
@ -1211,7 +1212,7 @@ function showCandidateJoinOption() {
if (joinBtn) joinBtn.style.display = 'none';
if (!localMediaStream) {
if (!localMediaStream || localMediaStream.getVideoTracks().length === 0) {
await startCandidateCameraStream();
}
@ -1224,7 +1225,6 @@ function showCandidateJoinOption() {
badge.style.color = '#34d399';
}
// Dual 2-Way Connection: Answer incoming call AND establish connection to interviewer peer
const stream = localMediaStream || new MediaStream();
if (activeCallInstance) {
@ -1249,8 +1249,7 @@ function showCandidateJoinOption() {
const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo) {
remoteVideo.srcObject = remoteStream;
remoteVideo.play().catch(e => console.log('Remote video play error:', e));
safePlayMediaStream(remoteVideo, remoteStream);
}
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 peerInstance = null;
let candidatePeerConnection = null;
@ -1319,7 +1371,22 @@ function showCandidateJoinOption() {
if (msg.type === 'interviewer_joined' || msg.type === 'offer') {
latestCallStatus = 'active';
triggerCandidateRing(msg.offer || null);
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);
}
} else if (msg.type === 'answer' && msg.sender !== 'candidate' && candidatePeerConnection) {
await candidatePeerConnection.setRemoteDescription(new RTCSessionDescription(msg.answer));
} else if (msg.type === 'ice-candidate' && msg.sender !== 'candidate' && candidatePeerConnection && msg.candidate) {
@ -1341,13 +1408,10 @@ function showCandidateJoinOption() {
async function createCandidatePeerConnection() {
if (candidatePeerConnection) return candidatePeerConnection;
await initMeteredIceServers();
candidatePeerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
iceServers: globalIceServers
});
if (localMediaStream) {
@ -1360,8 +1424,7 @@ function showCandidateJoinOption() {
const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo && event.streams[0]) {
remoteVideo.srcObject = event.streams[0];
remoteVideo.play().catch(e => console.log(e));
safePlayMediaStream(remoteVideo, event.streams[0]);
}
if (placeholder) placeholder.style.display = 'none';
@ -1373,7 +1436,7 @@ function showCandidateJoinOption() {
}
};
candidatePeerConnection.onconnectionstatechange = () => {
candidatePeerConnection.onconnectionstatechange = async () => {
const state = candidatePeerConnection.connectionState;
if (state === 'connected') {
const badge = document.getElementById('call-status-badge');
@ -1382,7 +1445,9 @@ function showCandidateJoinOption() {
badge.style.background = 'rgba(16,185,129,0.3)';
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();
}
};
@ -1411,8 +1476,9 @@ function showCandidateJoinOption() {
callSigChannel.postMessage({ type: 'answer', answer: answer, sender: 'candidate' });
}
function initCandidatePeer() {
async function initCandidatePeer() {
if (typeof Peer === 'undefined') return;
await initMeteredIceServers();
const peerId = 'cand_' + interviewId;
if (peerInstance) {
@ -1421,11 +1487,7 @@ function initCandidatePeer() {
peerInstance = new Peer(peerId, {
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
iceServers: globalIceServers
}
});
@ -1441,15 +1503,11 @@ function initCandidatePeer() {
triggerCandidateRing();
}
const sendStream = localMediaStream || new MediaStream();
call.answer(sendStream);
call.on('stream', remoteStream => {
const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo) {
remoteVideo.srcObject = remoteStream;
remoteVideo.play().catch(e => console.log('Candidate remote video play error:', e));
safePlayMediaStream(remoteVideo, remoteStream);
}
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', () => {
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() {
const avatarPlaceholder = document.getElementById('webcam-avatar-placeholder');
const audioConstraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
};
return navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
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: true, audio: false }))
.then(stream => {
localMediaStream = stream;
const vid = document.getElementById('webcam');
if (vid) {
vid.muted = true;
vid.srcObject = stream;
vid.play().catch(e => console.log('Candidate webcam play error:', e));
safePlayMediaStream(vid, stream, true);
}
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">
<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="//cdn.metered.ca/sdk/video/1.4.6/sdk.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
@ -1292,7 +1293,6 @@ function openReviewModal(id, name, uid, autoJoinCall = false) {
document.getElementById('review-modal').classList.add('active');
resetZoomScreen();
initInterviewerPeer();
switchIdeTab('code');
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;
await initMeteredIceServers();
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 => {
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
const screenPeerId = 'interviewer_screen_' + currentInterviewId;
if (screenReceiverPeer && !screenReceiverPeer.destroyed) {
try { screenReceiverPeer.destroy(); } catch(e) {}
}
screenReceiverPeer = new Peer(screenPeerId, {
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
iceServers: globalIceServers
}
});
screenReceiverPeer.on('call', call => {
@ -1712,7 +1793,7 @@ function initInterviewerPeer() {
call.on('stream', screenStream => {
const screenVid = document.getElementById('interviewer-screen-video');
const placeholder = document.getElementById('screen-video-placeholder');
if (screenVid) screenVid.srcObject = screenStream;
if (screenVid) safePlayMediaStream(screenVid, screenStream);
if (placeholder) placeholder.style.display = 'none';
});
});
@ -1767,13 +1848,10 @@ function initInterviewerSigChannel() {
async function createInterviewerPeerConnection() {
if (interviewerPeerConnection) return interviewerPeerConnection;
await initMeteredIceServers();
interviewerPeerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
iceServers: globalIceServers
});
if (interviewerLocalStream) {
@ -1786,8 +1864,7 @@ function initInterviewerSigChannel() {
const candVid = document.getElementById('interviewer-cand-video');
const placeholder = document.getElementById('cand-video-placeholder');
if (candVid && event.streams[0]) {
candVid.srcObject = event.streams[0];
candVid.play().catch(e => console.log(e));
safePlayMediaStream(candVid, event.streams[0]);
}
if (placeholder) placeholder.style.display = 'none';
@ -1796,6 +1873,20 @@ function initInterviewerSigChannel() {
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) => {
if (event.candidate) {
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');
if (selfVid) {
selfVid.muted = true;
selfVid.srcObject = stream;
selfVid.play().catch(e => console.log(e));
safePlayMediaStream(selfVid, stream, true);
}
document.getElementById('self-video-placeholder').style.display = 'none';
@ -1870,23 +1960,13 @@ function startInterviewerCall(isRejoin = false) {
interviewerSigChannel.postMessage({ type: 'interviewer_joined', sender: 'interviewer' });
}
// PeerJS fallback
const panelistPeerId = 'interviewer_' + currentInterviewId + '_' + Math.floor(Math.random() * 10000);
if (interviewerPeer) {
try { interviewerPeer.destroy(); } catch(e) {}
// PeerJS connection
if (!interviewerPeer || interviewerPeer.destroyed) {
initInterviewerPeer();
}
interviewerPeer = new Peer(panelistPeerId, {
config: {
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 makePeerJSCall = () => {
if (!interviewerPeer || interviewerPeer.destroyed) return;
const candPeerId = 'cand_' + currentInterviewId;
activeCall = interviewerPeer.call(candPeerId, stream);
@ -1895,37 +1975,48 @@ function startInterviewerCall(isRejoin = false) {
const candVid = document.getElementById('interviewer-cand-video');
const placeholder = document.getElementById('cand-video-placeholder');
if (candVid) {
candVid.srcObject = candRemoteStream;
candVid.play().catch(e => console.log('Interviewer video play error:', e));
safePlayMediaStream(candVid, candRemoteStream);
}
if (placeholder) placeholder.style.display = 'none';
candRemoteStream.onaddtrack = () => {
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').style.background = 'rgba(16,185,129,0.3)';
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);
}
};
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
const audioConstraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
};
navigator.mediaDevices.getUserMedia({ video: true, audio: audioConstraints })
.then(handleStream)
.catch(() => {
navigator.mediaDevices.getUserMedia({ video: false, audio: true })
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(handleStream)
.catch(() => {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
navigator.mediaDevices.getUserMedia({ video: false, audio: true })
.then(handleStream)
.catch(err => console.log('WebRTC Stream init:', err));
.catch(() => {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(handleStream)
.catch(err => console.log('WebRTC Stream init:', err));
});
});
});
}
@ -1977,6 +2068,14 @@ function leaveInterviewerCall(isExplicitEnd = true) {
try { activeCall.close(); } catch(e) {}
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 screenPlace = document.getElementById('screen-video-placeholder');