feat: add Metered turn server
This commit is contained in:
parent
bc0d249dba
commit
1f22185ecf
@ -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';
|
||||||
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) {
|
} 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';
|
||||||
|
|
||||||
|
|||||||
@ -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,37 +1975,48 @@ 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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
const audioConstraints = {
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true,
|
||||||
|
autoGainControl: true
|
||||||
|
};
|
||||||
|
|
||||||
|
navigator.mediaDevices.getUserMedia({ video: true, audio: audioConstraints })
|
||||||
.then(handleStream)
|
.then(handleStream)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
navigator.mediaDevices.getUserMedia({ video: false, audio: true })
|
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||||
.then(handleStream)
|
.then(handleStream)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
|
navigator.mediaDevices.getUserMedia({ video: false, audio: true })
|
||||||
.then(handleStream)
|
.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) {}
|
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');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user