@@ -1110,141 +1114,107 @@ class CandRingtoneEngine {
let candRingTimer = null;
let latestCallStartedAt = null;
- function getCallSessionKey(timestamp) {
- return 'handled_call_' + interviewId + '_' + (timestamp || latestCallStartedAt || 'active');
+ let candidateInterviewerTiles = new Map();
+
+ function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName = 'Interviewer Panelist') {
+ const grid = document.getElementById('interviewer-mesh-grid');
+ const placeholder = document.getElementById('interviewer-placeholder-box');
+ if (placeholder) placeholder.style.display = 'none';
+
+ let tile = document.getElementById('cand-iv-tile-' + peerId);
+ let videoElem;
+
+ if (!tile) {
+ tile = document.createElement('div');
+ tile.id = 'cand-iv-tile-' + peerId;
+ tile.className = 'webcam-box';
+ tile.style.position = 'relative';
+ tile.style.borderColor = '#6366f1';
+
+ videoElem = document.createElement('video');
+ videoElem.id = 'cand-iv-video-' + peerId;
+ videoElem.autoplay = true;
+ videoElem.playsInline = true;
+ videoElem.style.width = '100%';
+ videoElem.style.height = '100%';
+ videoElem.style.objectFit = 'cover';
+ videoElem.style.background = '#050811';
+
+ const labelDiv = document.createElement('div');
+ labelDiv.style.position = 'absolute';
+ labelDiv.style.bottom = '8px';
+ labelDiv.style.left = '8px';
+ labelDiv.style.background = 'rgba(99,102,241,0.85)';
+ labelDiv.style.padding = '2px 8px';
+ labelDiv.style.borderRadius = '4px';
+ labelDiv.style.fontSize = '0.65rem';
+ labelDiv.style.color = 'white';
+ labelDiv.style.fontWeight = '600';
+ labelDiv.style.zIndex = '10';
+ labelDiv.innerText = '🎙️ ' + labelName;
+
+ tile.appendChild(videoElem);
+ tile.appendChild(labelDiv);
+ if (grid) grid.appendChild(tile);
+ } else {
+ videoElem = document.getElementById('cand-iv-video-' + peerId);
+ }
+
+ if (videoElem && stream) {
+ safePlayMediaStream(videoElem, stream);
+ }
+
+ candidateInterviewerTiles.set(peerId, stream);
+ isCandidateCallConnected = true;
+
+ const badge = document.getElementById('call-status-badge');
+ if (badge) {
+ badge.innerText = '🟢 CALL CONNECTED (LIVE)';
+ badge.style.background = 'rgba(16,185,129,0.3)';
+ badge.style.color = '#34d399';
+ }
+ }
+
+ function removeCandidateInterviewerTile(peerId) {
+ const tile = document.getElementById('cand-iv-tile-' + peerId);
+ if (tile) tile.remove();
+ candidateInterviewerTiles.delete(peerId);
+
+ if (candidateInterviewerTiles.size === 0) {
+ const placeholder = document.getElementById('interviewer-placeholder-box');
+ if (placeholder) placeholder.style.display = 'block';
+
+ const badge = document.getElementById('call-status-badge');
+ if (badge) {
+ badge.innerText = 'READY (Waiting for Interviewer)';
+ badge.style.background = 'rgba(16,185,129,0.2)';
+ badge.style.color = '#34d399';
+ }
+ }
}
function triggerCandidateRing(offer = null, callStartedAt = null) {
- if (isCandidateCallConnected) return;
- if (latestCallStatus === 'ended' || latestCallStatus === 'idle') return;
- if (callStartedAt) latestCallStartedAt = callStartedAt;
-
- const sessionKey = getCallSessionKey(callStartedAt);
- const isHandled = sessionStorage.getItem(sessionKey) === 'handled';
-
- if (isHandled || candidateDeclinedOrLeftCall) {
- showCandidateJoinOption();
- return;
- }
-
+ // Disabled ringing modal: Auto-connect is active
if (offer) pendingOffer = offer;
-
- const badge = document.getElementById('call-status-badge');
- if (badge) {
- badge.innerText = '🔔 INCOMING CALL... RINGING';
- badge.style.background = 'rgba(234,179,8,0.3)';
- badge.style.color = '#fde047';
+ if (!isCandidateCallConnected && localMediaStream) {
+ acceptCandidateCall();
}
-
- const modal = document.getElementById('candidate-incoming-modal');
- if (modal) {
- modal.style.opacity = '1';
- modal.style.pointerEvents = 'auto';
- }
- candRingtone.start();
-
- if (candRingTimer) clearTimeout(candRingTimer);
- candRingTimer = setTimeout(() => {
- stopCandidateRing();
- }, 12000);
}
- function declineCandidateCall() {
- candRingtone.stop();
- if (candRingTimer) {
- clearTimeout(candRingTimer);
- candRingTimer = null;
- }
- const sessionKey = getCallSessionKey();
- sessionStorage.setItem(sessionKey, 'handled');
- const modal = document.getElementById('candidate-incoming-modal');
- if (modal) {
- modal.style.opacity = '0';
- modal.style.pointerEvents = 'none';
- }
- candidateDeclinedOrLeftCall = true;
- showCandidateJoinOption();
- }
-
- function stopCandidateRing() {
- candRingtone.stop();
- if (candRingTimer) {
- clearTimeout(candRingTimer);
- candRingTimer = null;
- }
- const sessionKey = getCallSessionKey();
- sessionStorage.setItem(sessionKey, 'handled');
- const modal = document.getElementById('candidate-incoming-modal');
- if (modal) {
- modal.style.opacity = '0';
- modal.style.pointerEvents = 'none';
- }
- candidateDeclinedOrLeftCall = true;
- showCandidateJoinOption();
- }
+ function declineCandidateCall() {}
+ function stopCandidateRing() {}
function showCandidateJoinOption() {
- if (isCandidateCallConnected) return;
- if (latestCallStatus === 'ended' || latestCallStatus === 'idle') {
- const joinBtn = document.getElementById('candidate-join-active-call-btn');
- if (joinBtn) {
- joinBtn.style.display = 'none';
- joinBtn.disabled = true;
- }
- return;
- }
const badge = document.getElementById('call-status-badge');
- if (badge) {
- badge.innerText = '📞 CALL IN PROGRESS';
- badge.style.background = 'rgba(59,130,246,0.3)';
- badge.style.color = '#93c5fd';
- }
- const joinBtn = document.getElementById('candidate-join-active-call-btn');
- if (joinBtn) {
- joinBtn.style.display = 'block';
- joinBtn.disabled = false;
- joinBtn.innerText = '🟢 📞 Call in Progress • Rejoin Call Now';
+ if (badge && !isCandidateCallConnected) {
+ badge.innerText = 'READY (Waiting for Interviewer)';
+ badge.style.background = 'rgba(16,185,129,0.2)';
+ badge.style.color = '#34d399';
}
}
async function acceptCandidateCall() {
- const sessionKey = getCallSessionKey();
- sessionStorage.setItem(sessionKey, 'handled');
-
candidateDeclinedOrLeftCall = false;
- candRingtone.stop();
- if (candRingTimer) {
- clearTimeout(candRingTimer);
- candRingTimer = null;
- }
- const modal = document.getElementById('candidate-incoming-modal');
- if (modal) {
- modal.style.opacity = '0';
- modal.style.pointerEvents = 'none';
- }
- const joinBtn = document.getElementById('candidate-join-active-call-btn');
-
- if (latestCallStatus === 'ended' || latestCallStatus === 'idle') {
- if (joinBtn) {
- joinBtn.style.display = 'none';
- joinBtn.disabled = true;
- }
- if (typeof Swal !== 'undefined') {
- Swal.fire({
- icon: 'warning',
- title: 'Call Ended',
- text: 'The interview call has been ended by the host.',
- toast: true,
- position: 'top-end',
- timer: 3000,
- showConfirmButton: false
- });
- }
- return;
- }
-
- if (joinBtn) joinBtn.style.display = 'none';
-
if (!localMediaStream || localMediaStream.getVideoTracks().length === 0) {
await startCandidateCameraStream();
}
@@ -1265,31 +1235,13 @@ function showCandidateJoinOption() {
}
// Signal candidate ready via WebRTC BroadcastChannel
- callSigChannel.postMessage({ type: 'candidate_ready', sender: 'candidate' });
+ callSigChannel.postMessage({ type: 'candidate_ready', sender: 'candidate', peer_id: 'cand_' + interviewId });
if (pendingOffer) {
await handleInterviewerOffer(pendingOffer);
pendingOffer = null;
}
- // Call interviewer peer directly if interviewer registered
- if (peerInstance) {
- const interviewerPeerId = 'interviewer_' + interviewId;
- try {
- const outCall = peerInstance.call(interviewerPeerId, stream);
- if (outCall) {
- outCall.on('stream', remoteStream => {
- const remoteVideo = document.getElementById('interviewer-video');
- const placeholder = document.getElementById('interviewer-video-placeholder');
- if (remoteVideo) {
- safePlayMediaStream(remoteVideo, remoteStream);
- }
- if (placeholder) placeholder.style.display = 'none';
- });
- }
- } catch(e) {}
- }
-
// Automatically capture candidate system screenshot on call start (if enabled)
if (!hasCapturedCallStartScreenshot && isTabSwitchScreenshotEnabled) {
hasCapturedCallStartScreenshot = true;
@@ -1323,7 +1275,6 @@ function safePlayMediaStream(videoElem, stream, isSelf = false) {
}
}
- // Metered TURN Server Integration (Managed via window.getMeteredIceServers in app.js / metered.js)
let globalIceServers = window.globalIceServers || [
{ urls: 'stun:stun.l.google.com:19302' }
];
@@ -1340,11 +1291,8 @@ function initMeteredIceServers() {
return Promise.resolve(globalIceServers);
}
- // Initiate TURN credentials pre-fetch immediately
initMeteredIceServers();
-
-
let localMediaStream = null;
let peerInstance = null;
let candidatePeerConnection = null;
@@ -1362,22 +1310,10 @@ function initMeteredIceServers() {
if (data.type === 'incoming_call' && data.interview_id == interviewId) {
latestCallStatus = 'active';
- triggerCandidateRing(null, data.call_started_at || null);
+ if (!isCandidateCallConnected) acceptCandidateCall();
} else if (data.type === 'call_ended' && data.interview_id == interviewId) {
- candRingtone.stop();
- if (candRingTimer) clearTimeout(candRingTimer);
isCandidateCallConnected = false;
latestCallStatus = 'ended';
- const modal = document.getElementById('candidate-incoming-modal');
- if (modal) {
- modal.style.opacity = '0';
- modal.style.pointerEvents = 'none';
- }
- const joinBtn = document.getElementById('candidate-join-active-call-btn');
- if (joinBtn) {
- joinBtn.style.display = 'none';
- joinBtn.disabled = true;
- }
const badge = document.getElementById('call-status-badge');
if (badge) {
badge.innerText = 'CALL ENDED BY HOST';
@@ -1390,28 +1326,27 @@ function initMeteredIceServers() {
callSigChannel = new BroadcastChannel('webrtc_call_' + interviewId);
-
callSigChannel.onmessage = async (event) => {
const msg = event.data;
if (!msg) return;
- if (msg.type === 'interviewer_joined' || msg.type === 'offer') {
+ if (msg.type === 'interviewer_joined' || msg.type === 'peer_joined' || msg.type === 'peer_presence_ack' || msg.type === 'offer') {
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);
+ if (msg.peer_id && msg.peer_id.startsWith('interviewer_')) {
+ if (localMediaStream && peerInstance) {
+ try {
+ const outCall = peerInstance.call(msg.peer_id, localMediaStream);
+ if (outCall) {
+ outCall.on('stream', remoteStream => {
+ addOrUpdateCandidateInterviewerTile(msg.peer_id, remoteStream, msg.user_name || 'Interviewer Panelist');
+ });
+ }
+ } catch(e) {}
+ }
+ }
+ } else if (msg.type === 'peer_left' || msg.type === 'interviewer_left') {
+ if (msg.peer_id) {
+ removeCandidateInterviewerTile(msg.peer_id);
}
} else if (msg.type === 'answer' && msg.sender !== 'candidate' && candidatePeerConnection) {
await candidatePeerConnection.setRemoteDescription(new RTCSessionDescription(msg.answer));
@@ -1420,13 +1355,14 @@ function initMeteredIceServers() {
await candidatePeerConnection.addIceCandidate(new RTCIceCandidate(msg.candidate));
} catch(e) {}
} else if (msg.type === 'end_call_all') {
- candRingtone.stop();
isCandidateCallConnected = false;
latestCallStatus = 'ended';
- const modal = document.getElementById('candidate-incoming-modal');
- if (modal) {
- modal.style.opacity = '0';
- modal.style.pointerEvents = 'none';
+ candidateInterviewerTiles.forEach((_, pid) => removeCandidateInterviewerTile(pid));
+ const badge = document.getElementById('call-status-badge');
+ if (badge) {
+ badge.innerText = 'CALL ENDED BY HOST';
+ badge.style.background = 'rgba(239,68,68,0.2)';
+ badge.style.color = '#fca5a5';
}
candidateLeaveCall(true);
}
@@ -1447,34 +1383,8 @@ function initMeteredIceServers() {
}
candidatePeerConnection.ontrack = (event) => {
- const remoteVideo = document.getElementById('interviewer-video');
- const placeholder = document.getElementById('interviewer-video-placeholder');
- if (remoteVideo && event.streams[0]) {
- safePlayMediaStream(remoteVideo, event.streams[0]);
- }
- if (placeholder) placeholder.style.display = 'none';
-
- const badge = document.getElementById('call-status-badge');
- if (badge) {
- badge.innerText = '🟢 CALL CONNECTED (LIVE)';
- badge.style.background = 'rgba(16,185,129,0.3)';
- badge.style.color = '#34d399';
- }
- };
-
- candidatePeerConnection.onconnectionstatechange = async () => {
- const state = candidatePeerConnection.connectionState;
- if (state === 'connected') {
- const badge = document.getElementById('call-status-badge');
- if (badge) {
- badge.innerText = '🟢 CALL CONNECTED (LIVE)';
- badge.style.background = 'rgba(16,185,129,0.3)';
- badge.style.color = '#34d399';
- }
- } else if (state === 'failed' || state === 'disconnected') {
- console.warn('Candidate P2P Connection State:', state);
- } else if (state === 'closed') {
- candidateLeaveCall();
+ if (event.streams[0]) {
+ addOrUpdateCandidateInterviewerTile('default_pc', event.streams[0], 'Interviewer');
}
};
@@ -1519,47 +1429,35 @@ function initMeteredIceServers() {
peerInstance.on('open', id => {
console.log('Candidate WebRTC Peer Registered:', id);
+ // Broadcast presence so interviewers (even those who joined before candidate) discover candidate instantly
+ callSigChannel.postMessage({
+ type: 'peer_joined',
+ peer_id: id,
+ role: 'candidate',
+ candidate_name: '{{ addslashes($interview->candidate_name) }}'
+ });
});
peerInstance.on('call', call => {
activeCallInstance = call;
latestCallStatus = 'active';
- if (!isCandidateCallConnected && !candidateDeclinedOrLeftCall) {
- triggerCandidateRing();
- }
+ const sendStream = localMediaStream || new MediaStream();
+ try { call.answer(sendStream); } catch(e) {}
call.on('stream', remoteStream => {
- const remoteVideo = document.getElementById('interviewer-video');
- const placeholder = document.getElementById('interviewer-video-placeholder');
- if (remoteVideo) {
- safePlayMediaStream(remoteVideo, remoteStream);
- }
- if (placeholder) placeholder.style.display = 'none';
-
- if (isCandidateCallConnected) {
- const badge = document.getElementById('call-status-badge');
- if (badge) {
- badge.innerText = '🟢 CALL CONNECTED (LIVE)';
- badge.style.background = 'rgba(16,185,129,0.3)';
- badge.style.color = '#34d399';
- }
- }
+ addOrUpdateCandidateInterviewerTile(call.peer, remoteStream, 'Interviewer Panelist');
});
- if (localMediaStream && localMediaStream.getVideoTracks().length > 0) {
- try { call.answer(localMediaStream); } catch(e) {}
- }
call.on('close', () => {
- candidateLeaveCall();
+ removeCandidateInterviewerTile(call.peer);
});
});
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...');
+ const meteredServers = await initMeteredIceServers();
if (peerInstance) {
try { peerInstance.destroy(); } catch(e) {}
}
@@ -1570,6 +1468,7 @@ function initMeteredIceServers() {
});
}
+
function startCandidateCameraStream() {
const avatarPlaceholder = document.getElementById('webcam-avatar-placeholder');
@@ -2166,14 +2065,63 @@ function saveCanvasDrawing(force = false) {
}
}
+ function sendCandidatePeerHeartbeat(action = 'heartbeat') {
+ if (!interviewId) return;
+ const candPeerId = 'cand_' + interviewId;
+ fetch('/candidate/peer-heartbeat/' + interviewId, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-TOKEN': '{{ csrf_token() }}'
+ },
+ body: JSON.stringify({
+ peer_id: candPeerId,
+ name: '{{ addslashes($interview->candidate_name) }}',
+ role: 'candidate',
+ action: action
+ })
+ }).catch(e => {});
+ }
+
setInterval(() => {
- fetch(`{{ route('interview.candidate.poll', $interview->submission_unique_id) }}`, {
+ if (!interviewId) return;
+ sendCandidatePeerHeartbeat('heartbeat');
+
+ fetch('/candidate/poll/' + interviewId, {
headers: { 'Accept': 'application/json' }
})
.then(r => r.json())
.then(data => {
if (!data) return;
+ if (data.active_peers && Array.isArray(data.active_peers)) {
+ const activePeerIds = new Set(data.active_peers.map(p => p.peer_id));
+
+ data.active_peers.forEach(p => {
+ if (p.peer_id && p.peer_id.startsWith('interviewer_')) {
+ let roleLabel = p.role === 'admin' ? ('Admin ' + (p.name || '')) : ('Interviewer ' + (p.name || ''));
+ if (localMediaStream && peerInstance && peerInstance.open) {
+ if (!candidateInterviewerTiles.has(p.peer_id)) {
+ try {
+ const outCall = peerInstance.call(p.peer_id, localMediaStream);
+ if (outCall) {
+ outCall.on('stream', remoteStream => {
+ addOrUpdateCandidateInterviewerTile(p.peer_id, remoteStream, roleLabel);
+ });
+ }
+ } catch(e) {}
+ }
+ }
+ }
+ });
+
+ candidateInterviewerTiles.forEach((_, pid) => {
+ if (!activePeerIds.has(pid)) {
+ removeCandidateInterviewerTile(pid);
+ }
+ });
+ }
+
if (data.enable_tab_switch_screenshot !== undefined) {
isTabSwitchScreenshotEnabled = Boolean(data.enable_tab_switch_screenshot);
}
@@ -2189,42 +2137,15 @@ function saveCanvasDrawing(force = false) {
}, 1200);
}
- const sessionKey = getCallSessionKey(data.call_started_at);
- const isHandledInSession = sessionStorage.getItem(sessionKey) === 'handled';
-
- if (!isCandidateCallConnected) {
- if (isHandledInSession || candidateDeclinedOrLeftCall) {
- showCandidateJoinOption();
- } else {
- triggerCandidateRing(null, data.call_started_at);
- }
+ if (!isCandidateCallConnected && localMediaStream) {
+ acceptCandidateCall();
}
} else if (data.call_status === 'ended' || data.call_status === 'idle') {
latestCallStatus = data.call_status;
hasCapturedCallStartScreenshot = false;
candidateDeclinedOrLeftCall = false;
- candRingtone.stop();
- if (candRingTimer) clearTimeout(candRingTimer);
isCandidateCallConnected = false;
- // Clear session storage handled keys for ended/idle call sessions
- for (let i = sessionStorage.length - 1; i >= 0; i--) {
- const key = sessionStorage.key(i);
- if (key && key.startsWith('handled_call_' + interviewId)) {
- sessionStorage.removeItem(key);
- }
- }
-
- const modal = document.getElementById('candidate-incoming-modal');
- if (modal) {
- modal.style.opacity = '0';
- modal.style.pointerEvents = 'none';
- }
- const joinBtn = document.getElementById('candidate-join-active-call-btn');
- if (joinBtn) {
- joinBtn.style.display = 'none';
- joinBtn.disabled = true;
- }
const badge = document.getElementById('call-status-badge');
if (badge) {
badge.innerText = (data.call_status === 'ended') ? 'CALL ENDED BY HOST' : 'READY';
@@ -2234,7 +2155,7 @@ function saveCanvasDrawing(force = false) {
}
})
.catch(() => {});
- }, 800);
+ }, 6000);