feat: mic & camera status of interview, candidate and peers
This commit is contained in:
parent
d1fbd99965
commit
5e0d2e4266
@ -93,6 +93,7 @@ let screenZoomLevel = 1.0;
|
|||||||
let candVidZoomLevel = 1.0;
|
let candVidZoomLevel = 1.0;
|
||||||
let activeViolations = [];
|
let activeViolations = [];
|
||||||
let sosPollInterval = null;
|
let sosPollInterval = null;
|
||||||
|
let peerHeartbeatTimer = null;
|
||||||
let acknowledgedViolationCount = 0;
|
let acknowledgedViolationCount = 0;
|
||||||
let sosDismissed = false;
|
let sosDismissed = false;
|
||||||
let sosAutoTriggerDisabled = false;
|
let sosAutoTriggerDisabled = false;
|
||||||
@ -413,6 +414,14 @@ export function subscribeLiveSync(interviewId) {
|
|||||||
if (drawingImg) drawingImg.style.display = 'none';
|
if (drawingImg) drawingImg.style.display = 'none';
|
||||||
if (noDrawing) noDrawing.style.display = 'block';
|
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') {
|
} else if (data.type === 'chat_message' || data.type === 'violation_logged' || data.type === 'violation_occurred') {
|
||||||
pollReviewData();
|
pollReviewData();
|
||||||
}
|
}
|
||||||
@ -425,7 +434,15 @@ export function subscribeLiveSync(interviewId) {
|
|||||||
interviewerSigChannel.onmessage = function(event) {
|
interviewerSigChannel.onmessage = function(event) {
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
if (!data) return;
|
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);
|
setTimeout(pollReviewData, 200);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -462,6 +479,11 @@ export function openReviewModal(id, name, uid, autoJoinCall = false) {
|
|||||||
if (sosPollInterval) clearInterval(sosPollInterval);
|
if (sosPollInterval) clearInterval(sosPollInterval);
|
||||||
sosPollInterval = setInterval(pollReviewData, 2500);
|
sosPollInterval = setInterval(pollReviewData, 2500);
|
||||||
|
|
||||||
|
if (peerHeartbeatTimer) clearInterval(peerHeartbeatTimer);
|
||||||
|
peerHeartbeatTimer = setInterval(() => {
|
||||||
|
sendInterviewerPeerHeartbeat('heartbeat');
|
||||||
|
}, 6000);
|
||||||
|
|
||||||
if (autoJoinCall) {
|
if (autoJoinCall) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
startInterviewerCall(true);
|
startInterviewerCall(true);
|
||||||
@ -487,11 +509,51 @@ export function sendInterviewerPeerHeartbeat(action = 'heartbeat') {
|
|||||||
peer_id: myPeerId,
|
peer_id: myPeerId,
|
||||||
name: currentUserName,
|
name: currentUserName,
|
||||||
role: currentUserRole,
|
role: currentUserRole,
|
||||||
|
mic_on: isInterviewerMicOn,
|
||||||
|
cam_on: isInterviewerCamOn,
|
||||||
action: action
|
action: action
|
||||||
})
|
})
|
||||||
}).catch(() => {});
|
}).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() {
|
export function pollReviewData() {
|
||||||
if (!currentInterviewId) return;
|
if (!currentInterviewId) return;
|
||||||
if (interviewerLocalStream) {
|
if (interviewerLocalStream) {
|
||||||
@ -517,8 +579,18 @@ export function pollReviewData() {
|
|||||||
const activeIds = new Set(data.active_peers.map(p => p.peer_id));
|
const activeIds = new Set(data.active_peers.map(p => p.peer_id));
|
||||||
|
|
||||||
data.active_peers.forEach(p => {
|
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) {
|
if (p.peer_id && p.peer_id !== myPeerId) {
|
||||||
let roleLabel = p.role === 'admin' ? ('Admin ' + (p.name || '')) : ('Panelist ' + (p.name || ''));
|
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 (p.peer_id.startsWith('interviewer_')) {
|
||||||
if (interviewerLocalStream && interviewerPeer && interviewerPeer.open) {
|
if (interviewerLocalStream && interviewerPeer && interviewerPeer.open) {
|
||||||
if (!panelistMeshTiles.has(p.peer_id)) {
|
if (!panelistMeshTiles.has(p.peer_id)) {
|
||||||
@ -526,10 +598,12 @@ export function pollReviewData() {
|
|||||||
const outCall = interviewerPeer.call(p.peer_id, interviewerLocalStream);
|
const outCall = interviewerPeer.call(p.peer_id, interviewerLocalStream);
|
||||||
if (outCall) {
|
if (outCall) {
|
||||||
outCall.on('stream', remoteStream => {
|
outCall.on('stream', remoteStream => {
|
||||||
addOrUpdatePanelistTile(p.peer_id, remoteStream, roleLabel);
|
addOrUpdatePanelistTile(p.peer_id, remoteStream, roleLabel, isMicOn, isCamOn);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} 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;
|
const currentUserId = window.currentUserId || 0;
|
||||||
if (!peerId || peerId === ('interviewer_' + currentInterviewId + '_' + currentUserId)) return;
|
if (!peerId || peerId === ('interviewer_' + currentInterviewId + '_' + currentUserId)) return;
|
||||||
const grid = document.getElementById('panelist-mesh-grid');
|
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 tile = document.getElementById('panelist-tile-' + peerId);
|
||||||
let videoElem;
|
let videoElem;
|
||||||
|
const initials = getInitials(name);
|
||||||
|
|
||||||
if (!tile) {
|
if (!tile) {
|
||||||
tile = document.createElement('div');
|
tile = document.createElement('div');
|
||||||
tile.id = 'panelist-tile-' + peerId;
|
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');
|
tile.innerHTML = `
|
||||||
videoElem.id = 'panelist-video-' + peerId;
|
<video id="panelist-video-${peerId}" autoplay playsinline class="w-full h-full object-cover origin-center transition-transform duration-200"></video>
|
||||||
videoElem.autoplay = true;
|
<div id="mesh-placeholder-${peerId}" class="absolute inset-0 flex flex-col items-center justify-center bg-[#0d1220]/95 text-slate-400 text-xs z-10 p-3" style="display: ${camOn ? 'none' : 'flex'};">
|
||||||
videoElem.playsInline = true;
|
<div class="av w-[58px] h-[58px] rounded-full bg-gradient-to-br from-[#5b8bff] to-[#3b63e0] flex items-center justify-center text-white font-semibold text-[19px] mb-2.5 shadow-md shadow-[#4b82f7]/20">
|
||||||
videoElem.className = 'w-full h-full object-cover';
|
${initials}
|
||||||
|
</div>
|
||||||
|
<div class="name text-[13.5px] font-semibold text-white">${name}</div>
|
||||||
|
<div class="status text-[11.5px] text-[#5b637a] mt-1 text-center px-3">Camera turned off</div>
|
||||||
|
</div>
|
||||||
|
<span class="tile-tag absolute left-2.5 bottom-2.5 text-[10px] font-semibold tracking-wide px-2.5 py-1 rounded-md bg-black/50 text-slate-300 border border-[#1b2233] z-20 flex items-center gap-1.5 backdrop-blur-sm">
|
||||||
|
<span>${name}</span>
|
||||||
|
<span class="w-px h-2.5 bg-white/20 mx-0.5"></span>
|
||||||
|
<i id="mesh-mic-${peerId}" class="${micOn ? 'fa-solid fa-microphone text-[#21c274]' : 'fa-solid fa-microphone-slash text-[#ef4a5f]'} text-[10px]" title="${micOn ? 'Mic On' : 'Mic Muted'}"></i>
|
||||||
|
<i id="mesh-cam-${peerId}" class="${camOn ? 'fa-solid fa-video text-[#21c274]' : 'fa-solid fa-video-slash text-[#ef4a5f]'} text-[10px]" title="${camOn ? 'Camera On' : 'Camera Off'}"></i>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
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);
|
grid.appendChild(tile);
|
||||||
|
videoElem = document.getElementById('panelist-video-' + peerId);
|
||||||
} else {
|
} else {
|
||||||
videoElem = document.getElementById('panelist-video-' + peerId);
|
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) {
|
if (videoElem && stream) {
|
||||||
@ -1114,9 +1210,10 @@ export async function initInterviewerPeer() {
|
|||||||
call.on('stream', remoteStream => {
|
call.on('stream', remoteStream => {
|
||||||
if (call.peer === 'cand_' + currentInterviewId) {
|
if (call.peer === 'cand_' + currentInterviewId) {
|
||||||
const candVid = document.getElementById('interviewer-cand-video');
|
const candVid = document.getElementById('interviewer-cand-video');
|
||||||
const placeholder = document.getElementById('cand-video-placeholder');
|
|
||||||
if (candVid) safePlayMediaStream(candVid, remoteStream);
|
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 {
|
} else {
|
||||||
addOrUpdatePanelistTile(call.peer, remoteStream, 'Panelist');
|
addOrUpdatePanelistTile(call.peer, remoteStream, 'Panelist');
|
||||||
}
|
}
|
||||||
@ -1183,9 +1280,10 @@ export function initInterviewerSigChannel() {
|
|||||||
outCall.on('stream', remoteStream => {
|
outCall.on('stream', remoteStream => {
|
||||||
if (msg.peer_id === 'cand_' + currentInterviewId) {
|
if (msg.peer_id === 'cand_' + currentInterviewId) {
|
||||||
const candVid = document.getElementById('interviewer-cand-video');
|
const candVid = document.getElementById('interviewer-cand-video');
|
||||||
const placeholder = document.getElementById('cand-video-placeholder');
|
|
||||||
if (candVid) safePlayMediaStream(candVid, remoteStream);
|
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 {
|
} else {
|
||||||
addOrUpdatePanelistTile(msg.peer_id, remoteStream, msg.user_name || 'Panelist');
|
addOrUpdatePanelistTile(msg.peer_id, remoteStream, msg.user_name || 'Panelist');
|
||||||
}
|
}
|
||||||
@ -1653,8 +1751,7 @@ export function toggleInterviewerMic() {
|
|||||||
audioTracks[0].enabled = isInterviewerMicOn;
|
audioTracks[0].enabled = isInterviewerMicOn;
|
||||||
|
|
||||||
const btn = document.getElementById('btn-toggle-interviewer-mic');
|
const btn = document.getElementById('btn-toggle-interviewer-mic');
|
||||||
if (!btn) return;
|
if (btn) {
|
||||||
|
|
||||||
const icon = btn.querySelector('i');
|
const icon = btn.querySelector('i');
|
||||||
if (icon) {
|
if (icon) {
|
||||||
icon.className = isInterviewerMicOn ? 'fa-solid fa-microphone' : 'fa-solid fa-microphone-slash';
|
icon.className = isInterviewerMicOn ? 'fa-solid fa-microphone' : 'fa-solid fa-microphone-slash';
|
||||||
@ -1669,6 +1766,15 @@ export function toggleInterviewerMic() {
|
|||||||
btn.classList.remove('bg-[#0d1220]', 'text-slate-300', 'border-[#232b3d]');
|
btn.classList.remove('bg-[#0d1220]', 'text-slate-300', 'border-[#232b3d]');
|
||||||
btn.classList.add('bg-[#ef4a5f]', 'border-[#ef4a5f]', 'hover:bg-[#d63d51]', 'text-white');
|
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() {
|
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) {
|
if (interviewerLocalStream) {
|
||||||
const videoTracks = interviewerLocalStream.getVideoTracks();
|
const videoTracks = interviewerLocalStream.getVideoTracks();
|
||||||
if (videoTracks.length > 0) {
|
if (videoTracks.length > 0) {
|
||||||
@ -1710,14 +1822,17 @@ export function toggleInterviewerCam() {
|
|||||||
if (selfPlaceholder) selfPlaceholder.style.display = 'none';
|
if (selfPlaceholder) selfPlaceholder.style.display = 'none';
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
sendInterviewerPeerHeartbeat();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selfPlaceholder) selfPlaceholder.style.display = isInterviewerCamOn ? 'none' : 'flex';
|
if (selfPlaceholder) selfPlaceholder.style.display = isInterviewerCamOn ? 'none' : 'flex';
|
||||||
|
sendInterviewerPeerHeartbeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeReviewModal() {
|
export function closeReviewModal() {
|
||||||
if (sosPollInterval) clearInterval(sosPollInterval);
|
if (sosPollInterval) clearInterval(sosPollInterval);
|
||||||
|
if (peerHeartbeatTimer) clearInterval(peerHeartbeatTimer);
|
||||||
if (liveSyncChannel) {
|
if (liveSyncChannel) {
|
||||||
try { liveSyncChannel.close(); } catch(e) {}
|
try { liveSyncChannel.close(); } catch(e) {}
|
||||||
liveSyncChannel = null;
|
liveSyncChannel = null;
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
'placeholderId',
|
'placeholderId',
|
||||||
'badgeText' => '',
|
'badgeText' => '',
|
||||||
'badgeId' => null,
|
'badgeId' => null,
|
||||||
|
'micStatusId' => null,
|
||||||
|
'camStatusId' => null,
|
||||||
'borderColor' => 'border-[#1b2233]',
|
'borderColor' => 'border-[#1b2233]',
|
||||||
'showZoom' => false,
|
'showZoom' => false,
|
||||||
'showFullscreen' => false,
|
'showFullscreen' => false,
|
||||||
@ -51,7 +53,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="tile-tag absolute left-2.5 bottom-2.5 text-[10px] font-semibold tracking-wide px-2 py-1 rounded-md bg-black/35 text-slate-400 border border-[#1b2233] z-10">
|
<span class="tile-tag absolute left-2.5 bottom-2.5 text-[10px] font-semibold tracking-wide px-2.5 py-1 rounded-md bg-black/50 text-slate-300 border border-[#1b2233] z-20 flex items-center gap-1.5 backdrop-blur-sm">
|
||||||
{{ $badgeText }}
|
<span>{{ $badgeText }}</span>
|
||||||
|
@if($micStatusId || $camStatusId)
|
||||||
|
<span class="w-px h-2.5 bg-white/20 mx-0.5"></span>
|
||||||
|
@if($micStatusId)
|
||||||
|
<i id="{{ $micStatusId }}" class="fa-solid fa-microphone text-[#21c274] text-[10px]" title="Mic On"></i>
|
||||||
|
@endif
|
||||||
|
@if($camStatusId)
|
||||||
|
<i id="{{ $camStatusId }}" class="fa-solid fa-video text-[#21c274] text-[10px]" title="Camera On"></i>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&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=Fira+Code:wght@400;500;600&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>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||||
|
|
||||||
<!-- PeerJS, Metered SDK, SweetAlert2 & html2canvas -->
|
<!-- PeerJS, Metered SDK, SweetAlert2 & html2canvas -->
|
||||||
<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>
|
||||||
@ -305,8 +306,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="font-size: 0.7rem; color: #94a3b8; margin-top: 6px;">Camera Disabled</div>
|
<div style="font-size: 0.7rem; color: #94a3b8; margin-top: 6px;">Camera Disabled</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="position: absolute; bottom: 8px; left: 8px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: #34d399; font-weight: 600; z-index: 10;">
|
<div style="position: absolute; bottom: 8px; left: 8px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: #34d399; font-weight: 600; z-index: 10; display: flex; align-items: center; gap: 6px;">
|
||||||
📷 Candidate Camera
|
<span>Candidate (You)</span>
|
||||||
|
<span style="opacity: 0.4;">|</span>
|
||||||
|
<i id="cand-self-mic-icon" class="fa-solid fa-microphone" style="color: #34d399; font-size: 10px;" title="Mic On"></i>
|
||||||
|
<i id="cand-self-cam-icon" class="fa-solid fa-video" style="color: #34d399; font-size: 10px;" title="Camera On"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1123,13 +1127,23 @@ class CandRingtoneEngine {
|
|||||||
|
|
||||||
let candidateInterviewerTiles = new Map();
|
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 grid = document.getElementById('interviewer-mesh-grid');
|
||||||
const placeholder = document.getElementById('interviewer-placeholder-box');
|
const placeholder = document.getElementById('interviewer-placeholder-box');
|
||||||
if (placeholder) placeholder.style.display = 'none';
|
if (placeholder) placeholder.style.display = 'none';
|
||||||
|
|
||||||
let tile = document.getElementById('cand-iv-tile-' + peerId);
|
let tile = document.getElementById('cand-iv-tile-' + peerId);
|
||||||
let videoElem;
|
let videoElem;
|
||||||
|
const initials = getInitials(labelName);
|
||||||
|
|
||||||
if (!tile) {
|
if (!tile) {
|
||||||
tile = document.createElement('div');
|
tile = document.createElement('div');
|
||||||
@ -1138,33 +1152,44 @@ function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName = 'Interv
|
|||||||
tile.style.position = 'relative';
|
tile.style.position = 'relative';
|
||||||
tile.style.borderColor = '#6366f1';
|
tile.style.borderColor = '#6366f1';
|
||||||
|
|
||||||
videoElem = document.createElement('video');
|
tile.innerHTML = `
|
||||||
videoElem.id = 'cand-iv-video-' + peerId;
|
<video id="cand-iv-video-${peerId}" autoplay playsinline style="width: 100%; height: 100%; object-fit: cover; background: #050811;"></video>
|
||||||
videoElem.autoplay = true;
|
<div id="cand-iv-placeholder-${peerId}" style="position: absolute; inset: 0; display: ${camOnVal ? 'none' : 'flex'}; flex-direction: column; align-items: center; justify-content: center; background: #0f172a; color: white; z-index: 5;">
|
||||||
videoElem.playsInline = true;
|
<div style="width: 58px; height: 58px; border-radius: 50%; background: linear-gradient(135deg, #38bdf8 0%, #6366f1 100%); display: flex; align-items: center; justify-content: center; font-size: 1.3rem; font-weight: 800; color: white; box-shadow: 0 0 15px rgba(56,189,248,0.4);">
|
||||||
videoElem.style.width = '100%';
|
${initials}
|
||||||
videoElem.style.height = '100%';
|
</div>
|
||||||
videoElem.style.objectFit = 'cover';
|
<div style="font-size: 0.8rem; font-weight: 700; color: white; margin-top: 6px;">${labelName}</div>
|
||||||
videoElem.style.background = '#050811';
|
<div style="font-size: 0.65rem; color: #94a3b8; margin-top: 2px;">Camera Turned Off</div>
|
||||||
|
</div>
|
||||||
|
<div style="position: absolute; bottom: 8px; left: 8px; background: rgba(99,102,241,0.85); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: white; font-weight: 600; z-index: 10; display: flex; align-items: center; gap: 6px;">
|
||||||
|
<span>🎙️ ${labelName}</span>
|
||||||
|
<span style="opacity: 0.4;">|</span>
|
||||||
|
<i id="cand-iv-mic-${peerId}" class="${micOnVal ? 'fa-solid fa-microphone' : 'fa-solid fa-microphone-slash'}" style="color: ${micOnVal ? '#34d399' : '#ef4444'}; font-size: 10px;" title="${micOnVal ? 'Mic On' : 'Mic Muted'}"></i>
|
||||||
|
<i id="cand-iv-cam-${peerId}" class="${camOnVal ? 'fa-solid fa-video' : 'fa-solid fa-video-slash'}" style="color: ${camOnVal ? '#34d399' : '#ef4444'}; font-size: 10px;" title="${camOnVal ? 'Camera On' : 'Camera Off'}"></i>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
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);
|
if (grid) grid.appendChild(tile);
|
||||||
|
videoElem = document.getElementById('cand-iv-video-' + peerId);
|
||||||
} else {
|
} else {
|
||||||
videoElem = document.getElementById('cand-iv-video-' + peerId);
|
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) {
|
if (videoElem && stream) {
|
||||||
@ -1337,15 +1362,20 @@ function initMeteredIceServers() {
|
|||||||
const msg = event.data;
|
const msg = event.data;
|
||||||
if (!msg) return;
|
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';
|
latestCallStatus = 'active';
|
||||||
if (msg.peer_id && msg.peer_id.startsWith('interviewer_')) {
|
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 {
|
try {
|
||||||
const outCall = peerInstance.call(msg.peer_id, localMediaStream);
|
const outCall = peerInstance.call(msg.peer_id, localMediaStream);
|
||||||
if (outCall) {
|
if (outCall) {
|
||||||
outCall.on('stream', remoteStream => {
|
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) {}
|
} 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() {
|
function toggleMic() {
|
||||||
if (!localMediaStream) return;
|
if (!localMediaStream) return;
|
||||||
const audioTracks = localMediaStream.getAudioTracks();
|
const audioTracks = localMediaStream.getAudioTracks();
|
||||||
@ -1636,10 +1731,19 @@ function toggleMic() {
|
|||||||
isMicEnabled = !isMicEnabled;
|
isMicEnabled = !isMicEnabled;
|
||||||
audioTracks[0].enabled = isMicEnabled;
|
audioTracks[0].enabled = isMicEnabled;
|
||||||
const btn = document.getElementById('toggle-mic-btn');
|
const btn = document.getElementById('toggle-mic-btn');
|
||||||
|
if (btn) {
|
||||||
btn.innerText = isMicEnabled ? '🎤 Mic On' : '🔇 Mic Muted';
|
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.background = isMicEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
|
||||||
btn.style.borderColor = isMicEnabled ? '#10b981' : '#ef4444';
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleCam() {
|
function toggleCam() {
|
||||||
@ -1650,15 +1754,24 @@ function toggleCam() {
|
|||||||
isCamEnabled = !isCamEnabled;
|
isCamEnabled = !isCamEnabled;
|
||||||
videoTracks[0].enabled = isCamEnabled;
|
videoTracks[0].enabled = isCamEnabled;
|
||||||
const btn = document.getElementById('toggle-cam-btn');
|
const btn = document.getElementById('toggle-cam-btn');
|
||||||
|
if (btn) {
|
||||||
btn.innerText = isCamEnabled ? '📷 Cam On' : '🚫 Cam Off';
|
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.background = isCamEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
|
||||||
btn.style.borderColor = isCamEnabled ? '#10b981' : '#ef4444';
|
btn.style.borderColor = isCamEnabled ? '#10b981' : '#ef4444';
|
||||||
|
}
|
||||||
if (avatarPlaceholder) avatarPlaceholder.style.display = isCamEnabled ? 'none' : 'flex';
|
if (avatarPlaceholder) avatarPlaceholder.style.display = isCamEnabled ? 'none' : 'flex';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
isCamEnabled = !isCamEnabled;
|
isCamEnabled = !isCamEnabled;
|
||||||
if (avatarPlaceholder) avatarPlaceholder.style.display = isCamEnabled ? 'none' : 'flex';
|
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) {
|
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(() => {
|
setInterval(() => {
|
||||||
if (!interviewId) return;
|
if (!interviewId) return;
|
||||||
sendCandidatePeerHeartbeat('heartbeat');
|
sendCandidatePeerHeartbeat('heartbeat');
|
||||||
|
|||||||
@ -213,7 +213,8 @@
|
|||||||
videoId="interviewer-cand-video"
|
videoId="interviewer-cand-video"
|
||||||
placeholderId="cand-video-placeholder"
|
placeholderId="cand-video-placeholder"
|
||||||
badgeText="Candidate video"
|
badgeText="Candidate video"
|
||||||
badgeId="cand-vid-zoom-badge"
|
micStatusId="cand-mic-status"
|
||||||
|
camStatusId="cand-cam-status"
|
||||||
showZoom="true"
|
showZoom="true"
|
||||||
zoomInFn="zoomCandVideo(0.25)"
|
zoomInFn="zoomCandVideo(0.25)"
|
||||||
zoomOutFn="zoomCandVideo(-0.25)"
|
zoomOutFn="zoomCandVideo(-0.25)"
|
||||||
@ -222,7 +223,7 @@
|
|||||||
avatarNameId="cand-avatar-name"
|
avatarNameId="cand-avatar-name"
|
||||||
avatarTitle="{{ $interview->candidate_name }}"
|
avatarTitle="{{ $interview->candidate_name }}"
|
||||||
initials="{{ $candInitials }}">
|
initials="{{ $candInitials }}">
|
||||||
Waiting for candidate…
|
<span id="cand-status-text">Waiting for candidate…</span>
|
||||||
</x-video-tile>
|
</x-video-tile>
|
||||||
|
|
||||||
<!-- Candidate Shared Screen -->
|
<!-- Candidate Shared Screen -->
|
||||||
@ -231,7 +232,6 @@
|
|||||||
videoId="interviewer-screen-video"
|
videoId="interviewer-screen-video"
|
||||||
placeholderId="screen-video-placeholder"
|
placeholderId="screen-video-placeholder"
|
||||||
badgeText="Shared screen"
|
badgeText="Shared screen"
|
||||||
badgeId="zoom-level-badge"
|
|
||||||
focused="true"
|
focused="true"
|
||||||
showZoom="true"
|
showZoom="true"
|
||||||
showFullscreen="true"
|
showFullscreen="true"
|
||||||
@ -255,6 +255,8 @@
|
|||||||
videoId="interviewer-self-video"
|
videoId="interviewer-self-video"
|
||||||
placeholderId="self-video-placeholder"
|
placeholderId="self-video-placeholder"
|
||||||
badgeText="Panelist self"
|
badgeText="Panelist self"
|
||||||
|
micStatusId="self-mic-status"
|
||||||
|
camStatusId="self-cam-status"
|
||||||
avatarTitle="{{ Auth::user()->name }}"
|
avatarTitle="{{ Auth::user()->name }}"
|
||||||
initials="{{ $usrInitials }}">
|
initials="{{ $usrInitials }}">
|
||||||
Panelist (self)
|
Panelist (self)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user