diff --git a/resources/js/interview-call.js b/resources/js/interview-call.js
index 898c5b8..b8a7014 100644
--- a/resources/js/interview-call.js
+++ b/resources/js/interview-call.js
@@ -93,6 +93,7 @@ let screenZoomLevel = 1.0;
let candVidZoomLevel = 1.0;
let activeViolations = [];
let sosPollInterval = null;
+let peerHeartbeatTimer = null;
let acknowledgedViolationCount = 0;
let sosDismissed = false;
let sosAutoTriggerDisabled = false;
@@ -413,6 +414,14 @@ export function subscribeLiveSync(interviewId) {
if (drawingImg) drawingImg.style.display = 'none';
if (noDrawing) noDrawing.style.display = 'block';
}
+ } else if (data.type === 'peer_state_changed') {
+ const isCandPeer = data.role === 'candidate' || (data.peer_id && data.peer_id.startsWith('cand_'));
+ if (isCandPeer) {
+ const isMicOn = (data.mic_on !== undefined) ? Boolean(data.mic_on) : true;
+ const isCamOn = (data.cam_on !== undefined) ? Boolean(data.cam_on) : true;
+ updateCandidateStatusIcons(isMicOn, isCamOn);
+ }
+ pollReviewData();
} else if (data.type === 'chat_message' || data.type === 'violation_logged' || data.type === 'violation_occurred') {
pollReviewData();
}
@@ -425,7 +434,15 @@ export function subscribeLiveSync(interviewId) {
interviewerSigChannel.onmessage = function(event) {
const data = event.data;
if (!data) return;
- if (data.type === 'violation_occurred' || data.type === 'violation_logged' || data.type === 'candidate_joined' || data.type === 'tab_switch') {
+ if (data.type === 'peer_state_changed') {
+ const isCandPeer = data.role === 'candidate' || (data.peer_id && data.peer_id.startsWith('cand_'));
+ if (isCandPeer) {
+ const isMicOn = (data.mic_on !== undefined) ? Boolean(data.mic_on) : true;
+ const isCamOn = (data.cam_on !== undefined) ? Boolean(data.cam_on) : true;
+ updateCandidateStatusIcons(isMicOn, isCamOn);
+ }
+ setTimeout(pollReviewData, 100);
+ } else if (data.type === 'violation_occurred' || data.type === 'violation_logged' || data.type === 'candidate_joined' || data.type === 'tab_switch') {
setTimeout(pollReviewData, 200);
}
};
@@ -462,6 +479,11 @@ export function openReviewModal(id, name, uid, autoJoinCall = false) {
if (sosPollInterval) clearInterval(sosPollInterval);
sosPollInterval = setInterval(pollReviewData, 2500);
+ if (peerHeartbeatTimer) clearInterval(peerHeartbeatTimer);
+ peerHeartbeatTimer = setInterval(() => {
+ sendInterviewerPeerHeartbeat('heartbeat');
+ }, 6000);
+
if (autoJoinCall) {
setTimeout(() => {
startInterviewerCall(true);
@@ -487,11 +509,51 @@ export function sendInterviewerPeerHeartbeat(action = 'heartbeat') {
peer_id: myPeerId,
name: currentUserName,
role: currentUserRole,
+ mic_on: isInterviewerMicOn,
+ cam_on: isInterviewerCamOn,
action: action
})
}).catch(() => {});
}
+function isTrueVal(val) {
+ if (val === undefined || val === null) return true;
+ if (val === false || val === 'false' || val === 0 || val === '0') return false;
+ return Boolean(val);
+}
+
+export function updateCandidateStatusIcons(isMicOn, isCamOn) {
+ const micOn = isTrueVal(isMicOn);
+ const camOn = isTrueVal(isCamOn);
+
+ const candMicIcon = document.getElementById('cand-mic-status');
+ const candCamIcon = document.getElementById('cand-cam-status');
+ const candPlaceholder = document.getElementById('cand-video-placeholder');
+ const candStatusText = document.getElementById('cand-status-text') || (candPlaceholder ? candPlaceholder.querySelector('.status') : null);
+ const candVid = document.getElementById('interviewer-cand-video');
+ const candidateStreamConnected = candVid && candVid.srcObject && (candVid.srcObject.active || (candVid.srcObject.getTracks && candVid.srcObject.getTracks().length > 0));
+
+ if (candMicIcon && isMicOn !== undefined) {
+ candMicIcon.className = micOn ? 'fa-solid fa-microphone text-[#21c274] text-[10px]' : 'fa-solid fa-microphone-slash text-[#ef4a5f] text-[10px]';
+ candMicIcon.title = micOn ? 'Mic On' : 'Mic Muted';
+ }
+ if (candCamIcon && isCamOn !== undefined) {
+ candCamIcon.className = camOn ? 'fa-solid fa-video text-[#21c274] text-[10px]' : 'fa-solid fa-video-slash text-[#ef4a5f] text-[10px]';
+ candCamIcon.title = camOn ? 'Camera On' : 'Camera Off';
+ }
+ if (candPlaceholder) {
+ if (!camOn) {
+ candPlaceholder.style.display = 'flex';
+ if (candStatusText) candStatusText.innerText = 'Camera Turned Off';
+ } else if (candidateStreamConnected) {
+ candPlaceholder.style.display = 'none';
+ } else {
+ candPlaceholder.style.display = 'flex';
+ if (candStatusText) candStatusText.innerHTML = 'Waiting for candidate…';
+ }
+ }
+}
+
export function pollReviewData() {
if (!currentInterviewId) return;
if (interviewerLocalStream) {
@@ -517,8 +579,18 @@ export function pollReviewData() {
const activeIds = new Set(data.active_peers.map(p => p.peer_id));
data.active_peers.forEach(p => {
+ const isCandPeer = p.role === 'candidate' || (p.peer_id && p.peer_id.startsWith('cand_'));
+ if (isCandPeer) {
+ const isMicOn = isTrueVal(p.mic_on);
+ const isCamOn = isTrueVal(p.cam_on);
+ updateCandidateStatusIcons(isMicOn, isCamOn);
+ }
+
if (p.peer_id && p.peer_id !== myPeerId) {
let roleLabel = p.role === 'admin' ? ('Admin ' + (p.name || '')) : ('Panelist ' + (p.name || ''));
+ const isMicOn = isTrueVal(p.mic_on);
+ const isCamOn = isTrueVal(p.cam_on);
+
if (p.peer_id.startsWith('interviewer_')) {
if (interviewerLocalStream && interviewerPeer && interviewerPeer.open) {
if (!panelistMeshTiles.has(p.peer_id)) {
@@ -526,10 +598,12 @@ export function pollReviewData() {
const outCall = interviewerPeer.call(p.peer_id, interviewerLocalStream);
if (outCall) {
outCall.on('stream', remoteStream => {
- addOrUpdatePanelistTile(p.peer_id, remoteStream, roleLabel);
+ addOrUpdatePanelistTile(p.peer_id, remoteStream, roleLabel, isMicOn, isCamOn);
});
}
} catch(e) {}
+ } else {
+ addOrUpdatePanelistTile(p.peer_id, panelistMeshTiles.get(p.peer_id), roleLabel, isMicOn, isCamOn);
}
}
}
@@ -1035,7 +1109,7 @@ export function safePlayMediaStream(videoElem, stream, isSelf = false) {
}
}
-export function addOrUpdatePanelistTile(peerId, stream, name = 'Panelist') {
+export function addOrUpdatePanelistTile(peerId, stream, name = 'Panelist', micOn = true, camOn = true) {
const currentUserId = window.currentUserId || 0;
if (!peerId || peerId === ('interviewer_' + currentInterviewId + '_' + currentUserId)) return;
const grid = document.getElementById('panelist-mesh-grid');
@@ -1043,27 +1117,49 @@ export function addOrUpdatePanelistTile(peerId, stream, name = 'Panelist') {
let tile = document.getElementById('panelist-tile-' + peerId);
let videoElem;
+ const initials = getInitials(name);
if (!tile) {
tile = document.createElement('div');
tile.id = 'panelist-tile-' + peerId;
- tile.className = 'relative bg-[#050811] rounded-xl border-2 border-indigo-500 overflow-hidden h-[220px]';
+ tile.className = 'video-tile relative aspect-[16/11] bg-[#0d1220] border border-[#1b2233] rounded-[10px] overflow-hidden flex flex-col items-center justify-center transition-all';
- videoElem = document.createElement('video');
- videoElem.id = 'panelist-video-' + peerId;
- videoElem.autoplay = true;
- videoElem.playsInline = true;
- videoElem.className = 'w-full h-full object-cover';
+ tile.innerHTML = `
+
+
+
+ ${initials}
+
+
${name}
+
Camera turned off
+
+
+ ${name}
+
+
+
+
+ `;
- const labelDiv = document.createElement('div');
- labelDiv.className = 'absolute bottom-1.5 left-1.5 bg-indigo-600/85 px-2 py-0.5 rounded text-[0.65rem] text-white font-semibold z-10';
- labelDiv.innerText = '🎙️ ' + name;
-
- tile.appendChild(videoElem);
- tile.appendChild(labelDiv);
grid.appendChild(tile);
+ videoElem = document.getElementById('panelist-video-' + peerId);
} else {
videoElem = document.getElementById('panelist-video-' + peerId);
+ const meshMicIcon = document.getElementById('mesh-mic-' + peerId);
+ const meshCamIcon = document.getElementById('mesh-cam-' + peerId);
+ const meshPlaceholder = document.getElementById('mesh-placeholder-' + peerId);
+
+ if (meshMicIcon) {
+ meshMicIcon.className = micOn ? 'fa-solid fa-microphone text-[#21c274] text-[10px]' : 'fa-solid fa-microphone-slash text-[#ef4a5f] text-[10px]';
+ meshMicIcon.title = micOn ? 'Mic On' : 'Mic Muted';
+ }
+ if (meshCamIcon) {
+ meshCamIcon.className = camOn ? 'fa-solid fa-video text-[#21c274] text-[10px]' : 'fa-solid fa-video-slash text-[#ef4a5f] text-[10px]';
+ meshCamIcon.title = camOn ? 'Camera On' : 'Camera Off';
+ }
+ if (meshPlaceholder) {
+ meshPlaceholder.style.display = camOn ? 'none' : 'flex';
+ }
}
if (videoElem && stream) {
@@ -1114,9 +1210,10 @@ export async function initInterviewerPeer() {
call.on('stream', remoteStream => {
if (call.peer === 'cand_' + currentInterviewId) {
const candVid = document.getElementById('interviewer-cand-video');
- const placeholder = document.getElementById('cand-video-placeholder');
if (candVid) safePlayMediaStream(candVid, remoteStream);
- if (placeholder) placeholder.style.display = 'none';
+ const candCamIcon = document.getElementById('cand-cam-status');
+ const isCamOn = candCamIcon ? candCamIcon.classList.contains('fa-video') : true;
+ updateCandidateStatusIcons(undefined, isCamOn);
} else {
addOrUpdatePanelistTile(call.peer, remoteStream, 'Panelist');
}
@@ -1183,9 +1280,10 @@ export function initInterviewerSigChannel() {
outCall.on('stream', remoteStream => {
if (msg.peer_id === 'cand_' + currentInterviewId) {
const candVid = document.getElementById('interviewer-cand-video');
- const placeholder = document.getElementById('cand-video-placeholder');
if (candVid) safePlayMediaStream(candVid, remoteStream);
- if (placeholder) placeholder.style.display = 'none';
+ const candCamIcon = document.getElementById('cand-cam-status');
+ const isCamOn = candCamIcon ? candCamIcon.classList.contains('fa-video') : true;
+ updateCandidateStatusIcons(undefined, isCamOn);
} else {
addOrUpdatePanelistTile(msg.peer_id, remoteStream, msg.user_name || 'Panelist');
}
@@ -1653,22 +1751,30 @@ export function toggleInterviewerMic() {
audioTracks[0].enabled = isInterviewerMicOn;
const btn = document.getElementById('btn-toggle-interviewer-mic');
- if (!btn) return;
+ if (btn) {
+ const icon = btn.querySelector('i');
+ if (icon) {
+ icon.className = isInterviewerMicOn ? 'fa-solid fa-microphone' : 'fa-solid fa-microphone-slash';
+ }
- const icon = btn.querySelector('i');
- if (icon) {
- icon.className = isInterviewerMicOn ? 'fa-solid fa-microphone' : 'fa-solid fa-microphone-slash';
+ btn.title = isInterviewerMicOn ? 'Mic On (Click to Mute)' : 'Mic Muted (Click to Unmute)';
+
+ if (isInterviewerMicOn) {
+ btn.classList.remove('bg-[#ef4a5f]', 'border-[#ef4a5f]', 'hover:bg-[#d63d51]', 'text-white');
+ btn.classList.add('bg-[#0d1220]', 'text-slate-300', 'border-[#232b3d]');
+ } else {
+ btn.classList.remove('bg-[#0d1220]', 'text-slate-300', 'border-[#232b3d]');
+ btn.classList.add('bg-[#ef4a5f]', 'border-[#ef4a5f]', 'hover:bg-[#d63d51]', 'text-white');
+ }
}
- btn.title = isInterviewerMicOn ? 'Mic On (Click to Mute)' : 'Mic Muted (Click to Unmute)';
-
- if (isInterviewerMicOn) {
- btn.classList.remove('bg-[#ef4a5f]', 'border-[#ef4a5f]', 'hover:bg-[#d63d51]', 'text-white');
- btn.classList.add('bg-[#0d1220]', 'text-slate-300', 'border-[#232b3d]');
- } else {
- btn.classList.remove('bg-[#0d1220]', 'text-slate-300', 'border-[#232b3d]');
- btn.classList.add('bg-[#ef4a5f]', 'border-[#ef4a5f]', 'hover:bg-[#d63d51]', 'text-white');
+ const selfMicIcon = document.getElementById('self-mic-status');
+ if (selfMicIcon) {
+ selfMicIcon.className = isInterviewerMicOn ? 'fa-solid fa-microphone text-[#21c274] text-[10px]' : 'fa-solid fa-microphone-slash text-[#ef4a5f] text-[10px]';
+ selfMicIcon.title = isInterviewerMicOn ? 'Mic On' : 'Mic Muted';
}
+
+ sendInterviewerPeerHeartbeat();
}
export function toggleInterviewerCam() {
@@ -1694,6 +1800,12 @@ export function toggleInterviewerCam() {
}
}
+ const selfCamIcon = document.getElementById('self-cam-status');
+ if (selfCamIcon) {
+ selfCamIcon.className = isInterviewerCamOn ? 'fa-solid fa-video text-[#21c274] text-[10px]' : 'fa-solid fa-video-slash text-[#ef4a5f] text-[10px]';
+ selfCamIcon.title = isInterviewerCamOn ? 'Camera On' : 'Camera Off';
+ }
+
if (interviewerLocalStream) {
const videoTracks = interviewerLocalStream.getVideoTracks();
if (videoTracks.length > 0) {
@@ -1710,14 +1822,17 @@ export function toggleInterviewerCam() {
if (selfPlaceholder) selfPlaceholder.style.display = 'none';
})
.catch(() => {});
+ sendInterviewerPeerHeartbeat();
return;
}
if (selfPlaceholder) selfPlaceholder.style.display = isInterviewerCamOn ? 'none' : 'flex';
+ sendInterviewerPeerHeartbeat();
}
export function closeReviewModal() {
if (sosPollInterval) clearInterval(sosPollInterval);
+ if (peerHeartbeatTimer) clearInterval(peerHeartbeatTimer);
if (liveSyncChannel) {
try { liveSyncChannel.close(); } catch(e) {}
liveSyncChannel = null;
diff --git a/resources/views/components/video-tile.blade.php b/resources/views/components/video-tile.blade.php
index 5b1177a..bf7b7b5 100644
--- a/resources/views/components/video-tile.blade.php
+++ b/resources/views/components/video-tile.blade.php
@@ -4,6 +4,8 @@
'placeholderId',
'badgeText' => '',
'badgeId' => null,
+ 'micStatusId' => null,
+ 'camStatusId' => null,
'borderColor' => 'border-[#1b2233]',
'showZoom' => false,
'showFullscreen' => false,
@@ -51,7 +53,16 @@
-
- {{ $badgeText }}
+
+ {{ $badgeText }}
+ @if($micStatusId || $camStatusId)
+
+ @if($micStatusId)
+
+ @endif
+ @if($camStatusId)
+
+ @endif
+ @endif
diff --git a/resources/views/interview/candidate_room.blade.php b/resources/views/interview/candidate_room.blade.php
index e92db91..b5b4362 100644
--- a/resources/views/interview/candidate_room.blade.php
+++ b/resources/views/interview/candidate_room.blade.php
@@ -9,6 +9,7 @@
+
@@ -305,8 +306,11 @@
Camera Disabled
-
- 📷 Candidate Camera
+
+ Candidate (You)
+ |
+
+
@@ -1123,13 +1127,23 @@ class CandRingtoneEngine {
let candidateInterviewerTiles = new Map();
- function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName = 'Interviewer Panelist') {
+ function isTrueVal(val) {
+ if (val === undefined || val === null) return true;
+ if (val === false || val === 'false' || val === 0 || val === '0') return false;
+ return Boolean(val);
+ }
+
+ function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName = 'Interviewer Panelist', micOn = true, camOn = true) {
+ const micOnVal = isTrueVal(micOn);
+ const camOnVal = isTrueVal(camOn);
+
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;
+ const initials = getInitials(labelName);
if (!tile) {
tile = document.createElement('div');
@@ -1138,33 +1152,44 @@ function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName = 'Interv
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';
+ tile.innerHTML = `
+
+
+
+ ${initials}
+
+
${labelName}
+
Camera Turned Off
+
+
+ 🎙️ ${labelName}
+ |
+
+
+
+ `;
- 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);
+ videoElem = document.getElementById('cand-iv-video-' + peerId);
} else {
videoElem = document.getElementById('cand-iv-video-' + peerId);
+ const micIcon = document.getElementById('cand-iv-mic-' + peerId);
+ const camIcon = document.getElementById('cand-iv-cam-' + peerId);
+ const placeholderEl = document.getElementById('cand-iv-placeholder-' + peerId);
+
+ if (micIcon) {
+ micIcon.className = micOnVal ? 'fa-solid fa-microphone' : 'fa-solid fa-microphone-slash';
+ micIcon.style.color = micOnVal ? '#34d399' : '#ef4444';
+ micIcon.title = micOnVal ? 'Mic On' : 'Mic Muted';
+ }
+ if (camIcon) {
+ camIcon.className = camOnVal ? 'fa-solid fa-video' : 'fa-solid fa-video-slash';
+ camIcon.style.color = camOnVal ? '#34d399' : '#ef4444';
+ camIcon.title = camOnVal ? 'Camera On' : 'Camera Off';
+ }
+ if (placeholderEl) {
+ placeholderEl.style.display = camOnVal ? 'none' : 'flex';
+ }
}
if (videoElem && stream) {
@@ -1337,15 +1362,20 @@ function initMeteredIceServers() {
const msg = event.data;
if (!msg) return;
- if (msg.type === 'interviewer_joined' || msg.type === 'peer_joined' || msg.type === 'peer_presence_ack' || msg.type === 'offer') {
+ if (msg.type === 'interviewer_joined' || msg.type === 'peer_joined' || msg.type === 'peer_presence_ack' || msg.type === 'offer' || msg.type === 'peer_state_changed') {
latestCallStatus = 'active';
if (msg.peer_id && msg.peer_id.startsWith('interviewer_')) {
- if (localMediaStream && peerInstance) {
+ const isMicOn = (msg.mic_on !== undefined) ? Boolean(msg.mic_on) : true;
+ const isCamOn = (msg.cam_on !== undefined) ? Boolean(msg.cam_on) : true;
+ const existingStream = candidateInterviewerTiles.get(msg.peer_id) || null;
+ addOrUpdateCandidateInterviewerTile(msg.peer_id, existingStream, msg.user_name || msg.name || 'Interviewer Panelist', isMicOn, isCamOn);
+
+ if (localMediaStream && peerInstance && !existingStream) {
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');
+ addOrUpdateCandidateInterviewerTile(msg.peer_id, remoteStream, msg.user_name || msg.name || 'Interviewer Panelist', isMicOn, isCamOn);
});
}
} catch(e) {}
@@ -1629,6 +1659,71 @@ function candidateLeaveCall(isEndedByHost = false) {
}
}
+ let candidateHeartbeatDebounceTimer = null;
+ let candidateHeartbeatIntervalTimer = null;
+
+ function debouncedCandidatePeerHeartbeat() {
+ if (candidateHeartbeatDebounceTimer) clearTimeout(candidateHeartbeatDebounceTimer);
+ sendCandidatePeerHeartbeat();
+ candidateHeartbeatDebounceTimer = setTimeout(() => {
+ sendCandidatePeerHeartbeat();
+ }, 300);
+ }
+
+ function sendCandidatePeerHeartbeat(action = 'heartbeat') {
+ const peerId = 'cand_' + interviewId;
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || window.csrfToken;
+ fetch("{{ route('interview.candidate.peer-heartbeat', $interview->id) }}", {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-TOKEN': csrfToken
+ },
+ body: JSON.stringify({
+ peer_id: peerId,
+ name: '{{ addslashes($interview->candidate_name) }}',
+ role: 'candidate',
+ mic_on: Boolean(isMicEnabled),
+ cam_on: Boolean(isCamEnabled),
+ action: action
+ })
+ })
+ .then(r => r.ok ? r.json() : null)
+ .then(data => {
+ if (data && data.active_peers && Array.isArray(data.active_peers)) {
+ data.active_peers.forEach(p => {
+ if (p.peer_id && (p.peer_id.startsWith('interviewer_') || p.role === 'admin' || p.role === 'interviewer')) {
+ const existingStream = candidateInterviewerTiles.get(p.peer_id) || null;
+ const nameLabel = p.role === 'admin' ? ('Admin ' + (p.name || '')) : (p.name || 'Interviewer Panelist');
+ const isMicOn = isTrueVal(p.mic_on);
+ const isCamOn = isTrueVal(p.cam_on);
+
+ addOrUpdateCandidateInterviewerTile(p.peer_id, existingStream, nameLabel, isMicOn, isCamOn);
+ }
+ });
+ }
+ })
+ .catch(() => {});
+
+ if (callSigChannel) {
+ callSigChannel.postMessage({
+ type: 'peer_state_changed',
+ peer_id: peerId,
+ role: 'candidate',
+ name: '{{ addslashes($interview->candidate_name) }}',
+ mic_on: Boolean(isMicEnabled),
+ cam_on: Boolean(isCamEnabled)
+ });
+ }
+ }
+
+ // 6-Second Steady Heartbeat Timer & Immediate Load Trigger
+ if (candidateHeartbeatIntervalTimer) clearInterval(candidateHeartbeatIntervalTimer);
+ sendCandidatePeerHeartbeat();
+ candidateHeartbeatIntervalTimer = setInterval(() => {
+ sendCandidatePeerHeartbeat();
+ }, 6000);
+
function toggleMic() {
if (!localMediaStream) return;
const audioTracks = localMediaStream.getAudioTracks();
@@ -1636,9 +1731,18 @@ function toggleMic() {
isMicEnabled = !isMicEnabled;
audioTracks[0].enabled = isMicEnabled;
const btn = document.getElementById('toggle-mic-btn');
- btn.innerText = isMicEnabled ? '🎤 Mic On' : '🔇 Mic Muted';
- btn.style.background = isMicEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
- btn.style.borderColor = isMicEnabled ? '#10b981' : '#ef4444';
+ if (btn) {
+ btn.innerText = isMicEnabled ? '🎤 Mic On' : '🔇 Mic Muted';
+ btn.style.background = isMicEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
+ btn.style.borderColor = isMicEnabled ? '#10b981' : '#ef4444';
+ }
+ const icon = document.getElementById('cand-self-mic-icon');
+ if (icon) {
+ icon.className = isMicEnabled ? 'fa-solid fa-microphone' : 'fa-solid fa-microphone-slash';
+ icon.style.color = isMicEnabled ? '#34d399' : '#ef4444';
+ icon.title = isMicEnabled ? 'Mic On' : 'Mic Muted';
+ }
+ debouncedCandidatePeerHeartbeat();
}
}
@@ -1650,15 +1754,24 @@ function toggleCam() {
isCamEnabled = !isCamEnabled;
videoTracks[0].enabled = isCamEnabled;
const btn = document.getElementById('toggle-cam-btn');
- btn.innerText = isCamEnabled ? '📷 Cam On' : '🚫 Cam Off';
- btn.style.background = isCamEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
- btn.style.borderColor = isCamEnabled ? '#10b981' : '#ef4444';
+ if (btn) {
+ btn.innerText = isCamEnabled ? '📷 Cam On' : '🚫 Cam Off';
+ btn.style.background = isCamEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
+ btn.style.borderColor = isCamEnabled ? '#10b981' : '#ef4444';
+ }
if (avatarPlaceholder) avatarPlaceholder.style.display = isCamEnabled ? 'none' : 'flex';
}
} else {
isCamEnabled = !isCamEnabled;
if (avatarPlaceholder) avatarPlaceholder.style.display = isCamEnabled ? 'none' : 'flex';
}
+ const icon = document.getElementById('cand-self-cam-icon');
+ if (icon) {
+ icon.className = isCamEnabled ? 'fa-solid fa-video' : 'fa-solid fa-video-slash';
+ icon.style.color = isCamEnabled ? '#34d399' : '#ef4444';
+ icon.title = isCamEnabled ? 'Camera On' : 'Camera Off';
+ }
+ debouncedCandidatePeerHeartbeat();
}
function requestFullscreenAsPromise(elem) {
@@ -2072,24 +2185,6 @@ 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(() => {
if (!interviewId) return;
sendCandidatePeerHeartbeat('heartbeat');
diff --git a/resources/views/interview/show.blade.php b/resources/views/interview/show.blade.php
index b0b99fc..fccbd0f 100644
--- a/resources/views/interview/show.blade.php
+++ b/resources/views/interview/show.blade.php
@@ -213,7 +213,8 @@
videoId="interviewer-cand-video"
placeholderId="cand-video-placeholder"
badgeText="Candidate video"
- badgeId="cand-vid-zoom-badge"
+ micStatusId="cand-mic-status"
+ camStatusId="cand-cam-status"
showZoom="true"
zoomInFn="zoomCandVideo(0.25)"
zoomOutFn="zoomCandVideo(-0.25)"
@@ -222,7 +223,7 @@
avatarNameId="cand-avatar-name"
avatarTitle="{{ $interview->candidate_name }}"
initials="{{ $candInitials }}">
- Waiting for candidate…
+ Waiting for candidate…
@@ -231,7 +232,6 @@
videoId="interviewer-screen-video"
placeholderId="screen-video-placeholder"
badgeText="Shared screen"
- badgeId="zoom-level-badge"
focused="true"
showZoom="true"
showFullscreen="true"
@@ -255,6 +255,8 @@
videoId="interviewer-self-video"
placeholderId="self-video-placeholder"
badgeText="Panelist self"
+ micStatusId="self-mic-status"
+ camStatusId="self-cam-status"
avatarTitle="{{ Auth::user()->name }}"
initials="{{ $usrInitials }}">
Panelist (self)