Merge pull request 'feat: add Metered turn server' (#16) from feature/TURN-server-integration into main
Reviewed-on: #16
This commit is contained in:
commit
4cae90c5ec
@ -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';
|
||||
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user