fix: stun server discovery
This commit is contained in:
parent
ab00516c42
commit
be8640c552
@ -6,6 +6,8 @@
|
|||||||
* All events (MediaPipe ML + Focus/AI/Speech) are transmitted to the server endpoint.
|
* All events (MediaPipe ML + Focus/AI/Speech) are transmitted to the server endpoint.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { initMeteredIceServers } from './metered.js';
|
||||||
|
|
||||||
// MediaPipe Published Browser Bundle CDN URLs
|
// MediaPipe Published Browser Bundle CDN URLs
|
||||||
const VISION_MODULE_URL = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.22-rc.20250304/vision_bundle.mjs';
|
const VISION_MODULE_URL = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.22-rc.20250304/vision_bundle.mjs';
|
||||||
const VISION_MODULE_FALLBACK_URL = 'https://unpkg.com/@mediapipe/tasks-vision@0.10.22-rc.20250304/vision_bundle.mjs';
|
const VISION_MODULE_FALLBACK_URL = 'https://unpkg.com/@mediapipe/tasks-vision@0.10.22-rc.20250304/vision_bundle.mjs';
|
||||||
@ -461,10 +463,28 @@ export function startScreenShare() {
|
|||||||
enforceBrowserFullscreen();
|
enforceBrowserFullscreen();
|
||||||
|
|
||||||
const interviewId = document.body.dataset.interviewId;
|
const interviewId = document.body.dataset.interviewId;
|
||||||
|
const submissionId = document.body.dataset.submissionId || interviewId;
|
||||||
if (typeof window.Peer !== 'undefined' && interviewId) {
|
if (typeof window.Peer !== 'undefined' && interviewId) {
|
||||||
const screenPeer = new window.Peer('cand_screen_' + interviewId + '_' + Date.now());
|
const targetInterviewerScreenPeerId = (submissionId && submissionId !== String(interviewId))
|
||||||
screenPeer.on('open', () => {
|
? ('interviewer_screen_' + interviewId + '_' + submissionId)
|
||||||
screenPeer.call('interviewer_screen_' + interviewId, stream);
|
: ('interviewer_screen_' + interviewId);
|
||||||
|
|
||||||
|
const screenPeerId = (submissionId && submissionId !== String(interviewId))
|
||||||
|
? ('cand_screen_' + interviewId + '_' + submissionId + '_' + Date.now())
|
||||||
|
: ('cand_screen_' + interviewId + '_' + Date.now());
|
||||||
|
|
||||||
|
initMeteredIceServers().then(iceServers => {
|
||||||
|
const screenPeer = new window.Peer(screenPeerId, {
|
||||||
|
config: { iceServers: iceServers }
|
||||||
|
});
|
||||||
|
screenPeer.on('open', () => {
|
||||||
|
screenPeer.call(targetInterviewerScreenPeerId, stream);
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
const screenPeer = new window.Peer(screenPeerId);
|
||||||
|
screenPeer.on('open', () => {
|
||||||
|
screenPeer.call(targetInterviewerScreenPeerId, stream);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { safePlayMediaStream, getInitials } from './interview-call.js';
|
import { safePlayMediaStream, getInitials } from './interview-call.js';
|
||||||
|
import { initMeteredIceServers } from './metered.js';
|
||||||
|
|
||||||
let editor = null;
|
let editor = null;
|
||||||
let candidateLocalStream = null;
|
let candidateLocalStream = null;
|
||||||
@ -404,17 +405,26 @@ export function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName =
|
|||||||
}
|
}
|
||||||
|
|
||||||
candidateInterviewerTiles.set(peerId, stream);
|
candidateInterviewerTiles.set(peerId, stream);
|
||||||
isCandidateCallConnected = true;
|
|
||||||
|
|
||||||
const badge = document.getElementById('call-status-badge');
|
if (stream || latestCallStatus === 'active') {
|
||||||
if (badge) {
|
latestCallStatus = 'active';
|
||||||
badge.innerText = '🟢 CALL CONNECTED (LIVE)';
|
isCandidateCallConnected = true;
|
||||||
badge.style.background = 'rgba(16,185,129,0.3)';
|
|
||||||
badge.style.color = '#34d399';
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeCandidateInterviewerTile(peerId) {
|
export function removeCandidateInterviewerTile(peerId) {
|
||||||
|
const stream = candidateInterviewerTiles.get(peerId);
|
||||||
|
if (stream && stream.active && latestCallStatus !== 'ended') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tile = document.getElementById('cand-iv-tile-' + peerId);
|
const tile = document.getElementById('cand-iv-tile-' + peerId);
|
||||||
if (tile) tile.remove();
|
if (tile) tile.remove();
|
||||||
candidateInterviewerTiles.delete(peerId);
|
candidateInterviewerTiles.delete(peerId);
|
||||||
@ -425,15 +435,22 @@ export function removeCandidateInterviewerTile(peerId) {
|
|||||||
|
|
||||||
const badge = document.getElementById('call-status-badge');
|
const badge = document.getElementById('call-status-badge');
|
||||||
if (badge) {
|
if (badge) {
|
||||||
badge.innerText = 'READY (Waiting for Interviewer)';
|
if (latestCallStatus === 'ended') {
|
||||||
badge.style.background = 'rgba(16,185,129,0.2)';
|
badge.innerText = 'CALL ENDED BY HOST';
|
||||||
badge.style.color = '#34d399';
|
badge.style.background = 'rgba(239,68,68,0.2)';
|
||||||
|
badge.style.color = '#fca5a5';
|
||||||
|
} else {
|
||||||
|
badge.innerText = 'READY (Waiting for Interviewer)';
|
||||||
|
badge.style.background = 'rgba(16,185,129,0.2)';
|
||||||
|
badge.style.color = '#34d399';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acceptCandidateCall() {
|
export async function acceptCandidateCall() {
|
||||||
const interviewId = document.body.dataset.interviewId;
|
const interviewId = document.body.dataset.interviewId;
|
||||||
|
if (latestCallStatus === 'ended') return;
|
||||||
candidateDeclinedOrLeftCall = false;
|
candidateDeclinedOrLeftCall = false;
|
||||||
|
|
||||||
if (!candidateLocalStream || candidateLocalStream.getVideoTracks().length === 0) {
|
if (!candidateLocalStream || candidateLocalStream.getVideoTracks().length === 0) {
|
||||||
@ -537,8 +554,8 @@ export function candidateLeaveCall(isEndedByHost = false) {
|
|||||||
async function createCandidatePeerConnection() {
|
async function createCandidatePeerConnection() {
|
||||||
if (candidatePeerConnection) return candidatePeerConnection;
|
if (candidatePeerConnection) return candidatePeerConnection;
|
||||||
|
|
||||||
const globalIceServers = window.globalIceServers || [{ urls: 'stun:stun.l.google.com:19302' }];
|
const iceServers = await initMeteredIceServers();
|
||||||
candidatePeerConnection = new RTCPeerConnection({ iceServers: globalIceServers });
|
candidatePeerConnection = new RTCPeerConnection({ iceServers: iceServers });
|
||||||
|
|
||||||
if (candidateLocalStream) {
|
if (candidateLocalStream) {
|
||||||
candidateLocalStream.getTracks().forEach(track => {
|
candidateLocalStream.getTracks().forEach(track => {
|
||||||
@ -571,23 +588,38 @@ async function handleInterviewerOffer(offer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCandidateSubmissionId() {
|
||||||
|
return document.body?.dataset?.submissionId || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCandidatePeerId() {
|
||||||
|
const interviewId = document.body?.dataset?.interviewId || '';
|
||||||
|
const subId = getCandidateSubmissionId();
|
||||||
|
if (!interviewId) return '';
|
||||||
|
|
||||||
|
return (subId && subId !== String(interviewId))
|
||||||
|
? ('cand_' + interviewId + '_' + subId)
|
||||||
|
: ('cand_' + interviewId);
|
||||||
|
}
|
||||||
|
|
||||||
export async function initCandidatePeer() {
|
export async function initCandidatePeer() {
|
||||||
const interviewId = document.body.dataset.interviewId;
|
const interviewId = document.body.dataset.interviewId;
|
||||||
const candName = document.body.dataset.candidateName || 'Candidate';
|
const candName = document.body.dataset.candidateName || 'Candidate';
|
||||||
if (!interviewId || typeof window.Peer === 'undefined') return;
|
if (!interviewId || typeof window.Peer === 'undefined') return;
|
||||||
|
|
||||||
const globalIceServers = window.globalIceServers || [{ urls: 'stun:stun.l.google.com:19302' }];
|
const iceServers = await initMeteredIceServers();
|
||||||
const peerId = 'cand_' + interviewId;
|
const peerId = getCandidatePeerId();
|
||||||
if (candidatePeerInstance) {
|
if (candidatePeerInstance) {
|
||||||
try { candidatePeerInstance.destroy(); } catch(e) {}
|
try { candidatePeerInstance.destroy(); } catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
candidatePeerInstance = new window.Peer(peerId, {
|
candidatePeerInstance = new window.Peer(peerId, {
|
||||||
config: { iceServers: globalIceServers }
|
config: { iceServers: iceServers }
|
||||||
});
|
});
|
||||||
|
|
||||||
candidatePeerInstance.on('open', id => {
|
candidatePeerInstance.on('open', id => {
|
||||||
console.log('Candidate WebRTC Peer Registered:', id);
|
console.log('[WebRTC Candidate] Registered PeerJS ID:', id);
|
||||||
|
sendCandidatePeerHeartbeat();
|
||||||
if (callSigChannel) {
|
if (callSigChannel) {
|
||||||
callSigChannel.postMessage({
|
callSigChannel.postMessage({
|
||||||
type: 'peer_joined',
|
type: 'peer_joined',
|
||||||
@ -598,30 +630,40 @@ export async function initCandidatePeer() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
candidatePeerInstance.on('call', call => {
|
candidatePeerInstance.on('call', async call => {
|
||||||
|
console.log('[WebRTC Candidate] Incoming PeerJS call from:', call.peer);
|
||||||
activeCandidateCallInstance = call;
|
activeCandidateCallInstance = call;
|
||||||
latestCallStatus = 'active';
|
if (latestCallStatus !== 'ended') latestCallStatus = 'active';
|
||||||
|
|
||||||
|
if (!candidateLocalStream || candidateLocalStream.getTracks().length === 0) {
|
||||||
|
await startCandidateCameraStream();
|
||||||
|
}
|
||||||
|
|
||||||
const sendStream = candidateLocalStream || new MediaStream();
|
const sendStream = candidateLocalStream || new MediaStream();
|
||||||
try { call.answer(sendStream); } catch(e) {}
|
try { call.answer(sendStream); } catch(e) {
|
||||||
|
console.error('[WebRTC Candidate] Error answering call:', e);
|
||||||
|
}
|
||||||
|
|
||||||
call.on('stream', remoteStream => {
|
call.on('stream', remoteStream => {
|
||||||
|
console.log('[WebRTC Candidate] Received remote stream from interviewer:', call.peer);
|
||||||
addOrUpdateCandidateInterviewerTile(call.peer, remoteStream, 'Interviewer Panelist');
|
addOrUpdateCandidateInterviewerTile(call.peer, remoteStream, 'Interviewer Panelist');
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on('close', () => {
|
call.on('close', () => {
|
||||||
|
console.log('[WebRTC Candidate] Call closed by peer:', call.peer);
|
||||||
removeCandidateInterviewerTile(call.peer);
|
removeCandidateInterviewerTile(call.peer);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
candidatePeerInstance.on('error', async err => {
|
candidatePeerInstance.on('error', async err => {
|
||||||
console.warn('Candidate PeerJS error:', err);
|
console.warn('[WebRTC Candidate] PeerJS error:', err);
|
||||||
if (err.type === 'peer-unavailable' || err.type === 'network' || err.type === 'webrtc') {
|
if (err.type === 'peer-unavailable' || err.type === 'network' || err.type === 'webrtc') {
|
||||||
if (candidatePeerInstance) {
|
if (candidatePeerInstance) {
|
||||||
try { candidatePeerInstance.destroy(); } catch(e) {}
|
try { candidatePeerInstance.destroy(); } catch(e) {}
|
||||||
}
|
}
|
||||||
|
const iceServers = await initMeteredIceServers();
|
||||||
candidatePeerInstance = new window.Peer(peerId, {
|
candidatePeerInstance = new window.Peer(peerId, {
|
||||||
config: { iceServers: globalIceServers }
|
config: { iceServers: iceServers }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -679,7 +721,7 @@ export function sendCandidatePeerHeartbeat(action = 'heartbeat') {
|
|||||||
const interviewId = document.body.dataset.interviewId;
|
const interviewId = document.body.dataset.interviewId;
|
||||||
const candName = document.body.dataset.candidateName || 'Candidate';
|
const candName = document.body.dataset.candidateName || 'Candidate';
|
||||||
if (!interviewId) return;
|
if (!interviewId) return;
|
||||||
const peerId = 'cand_' + interviewId;
|
const peerId = getCandidatePeerId();
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || window.csrfToken;
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || window.csrfToken;
|
||||||
|
|
||||||
fetch(`/candidate/peer-heartbeat/${interviewId}`, {
|
fetch(`/candidate/peer-heartbeat/${interviewId}`, {
|
||||||
@ -699,6 +741,7 @@ export function sendCandidatePeerHeartbeat(action = 'heartbeat') {
|
|||||||
})
|
})
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
if (latestCallStatus !== 'active') return;
|
||||||
if (data && data.active_peers && Array.isArray(data.active_peers)) {
|
if (data && data.active_peers && Array.isArray(data.active_peers)) {
|
||||||
data.active_peers.forEach(p => {
|
data.active_peers.forEach(p => {
|
||||||
if (p.peer_id && (p.peer_id.startsWith('interviewer_') || p.role === 'admin' || p.role === 'interviewer')) {
|
if (p.peer_id && (p.peer_id.startsWith('interviewer_') || p.role === 'admin' || p.role === 'interviewer')) {
|
||||||
@ -877,20 +920,26 @@ export function initCandidateRoom() {
|
|||||||
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' || msg.type === 'peer_state_changed') {
|
if (msg.type === 'interviewer_joined' || msg.type === 'offer') {
|
||||||
latestCallStatus = 'active';
|
latestCallStatus = 'active';
|
||||||
if (msg.peer_id && msg.peer_id.startsWith('interviewer_')) {
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'interviewer_joined' || msg.type === 'peer_joined' || msg.type === 'peer_presence_ack' || msg.type === 'offer' || msg.type === 'peer_state_changed') {
|
||||||
|
const peerIdToCall = msg.peer_id || msg.sender_peer_id;
|
||||||
|
if (peerIdToCall && peerIdToCall.startsWith('interviewer_')) {
|
||||||
const isMicOn = (msg.mic_on !== undefined) ? Boolean(msg.mic_on) : true;
|
const isMicOn = (msg.mic_on !== undefined) ? Boolean(msg.mic_on) : true;
|
||||||
const isCamOn = (msg.cam_on !== undefined) ? Boolean(msg.cam_on) : true;
|
const isCamOn = (msg.cam_on !== undefined) ? Boolean(msg.cam_on) : true;
|
||||||
const existingStream = candidateInterviewerTiles.get(msg.peer_id) || null;
|
const existingStream = candidateInterviewerTiles.get(peerIdToCall) || null;
|
||||||
addOrUpdateCandidateInterviewerTile(msg.peer_id, existingStream, msg.user_name || msg.name || 'Interviewer Panelist', isMicOn, isCamOn);
|
addOrUpdateCandidateInterviewerTile(peerIdToCall, existingStream, msg.user_name || msg.name || 'Interviewer Panelist', isMicOn, isCamOn);
|
||||||
|
|
||||||
if (candidateLocalStream && candidatePeerInstance && !existingStream) {
|
if (candidateLocalStream && candidatePeerInstance && candidatePeerInstance.open && !existingStream) {
|
||||||
try {
|
try {
|
||||||
const outCall = candidatePeerInstance.call(msg.peer_id, candidateLocalStream);
|
console.log('[WebRTC Candidate] Signal received, calling interviewer:', peerIdToCall);
|
||||||
|
const outCall = candidatePeerInstance.call(peerIdToCall, candidateLocalStream);
|
||||||
if (outCall) {
|
if (outCall) {
|
||||||
outCall.on('stream', remoteStream => {
|
outCall.on('stream', remoteStream => {
|
||||||
addOrUpdateCandidateInterviewerTile(msg.peer_id, remoteStream, msg.user_name || msg.name || 'Interviewer Panelist', isMicOn, isCamOn);
|
console.log('[WebRTC Candidate] Stream received via signal call:', peerIdToCall);
|
||||||
|
addOrUpdateCandidateInterviewerTile(peerIdToCall, remoteStream, msg.user_name || msg.name || 'Interviewer Panelist', isMicOn, isCamOn);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
@ -981,35 +1030,7 @@ export function initCandidateRoom() {
|
|||||||
|
|
||||||
pollCandidateChat(data);
|
pollCandidateChat(data);
|
||||||
|
|
||||||
if (data.active_peers && Array.isArray(data.active_peers)) {
|
if (data.call_status === 'active' && latestCallStatus !== 'ended') {
|
||||||
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 (candidateLocalStream && candidatePeerInstance && candidatePeerInstance.open) {
|
|
||||||
if (!candidateInterviewerTiles.has(p.peer_id)) {
|
|
||||||
try {
|
|
||||||
const outCall = candidatePeerInstance.call(p.peer_id, candidateLocalStream);
|
|
||||||
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.call_status === 'active') {
|
|
||||||
latestCallStatus = 'active';
|
latestCallStatus = 'active';
|
||||||
if (data.call_started_at) latestCallStartedAt = data.call_started_at;
|
if (data.call_started_at) latestCallStartedAt = data.call_started_at;
|
||||||
|
|
||||||
@ -1020,22 +1041,60 @@ export function initCandidateRoom() {
|
|||||||
}, 1200);
|
}, 1200);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isCandidateCallConnected && candidateLocalStream) {
|
if (!isCandidateCallConnected && candidateLocalStream && !candidateDeclinedOrLeftCall) {
|
||||||
acceptCandidateCall();
|
acceptCandidateCall();
|
||||||
}
|
}
|
||||||
} else if (data.call_status === 'ended' || data.call_status === 'idle') {
|
} else if (data.call_status === 'ended') {
|
||||||
latestCallStatus = data.call_status;
|
latestCallStatus = 'ended';
|
||||||
|
hasCapturedCallStartScreenshot = false;
|
||||||
|
isCandidateCallConnected = false;
|
||||||
|
candidateLeaveCall(true);
|
||||||
|
} else if (data.call_status === 'idle') {
|
||||||
|
latestCallStatus = 'idle';
|
||||||
hasCapturedCallStartScreenshot = false;
|
hasCapturedCallStartScreenshot = false;
|
||||||
candidateDeclinedOrLeftCall = false;
|
|
||||||
isCandidateCallConnected = false;
|
isCandidateCallConnected = false;
|
||||||
|
|
||||||
const badge = document.getElementById('call-status-badge');
|
const badge = document.getElementById('call-status-badge');
|
||||||
if (badge) {
|
if (badge && candidateInterviewerTiles.size === 0) {
|
||||||
badge.innerText = (data.call_status === 'ended') ? 'CALL ENDED BY HOST' : 'READY';
|
badge.innerText = 'READY (Waiting for Interviewer)';
|
||||||
badge.style.background = (data.call_status === 'ended') ? 'rgba(239,68,68,0.2)' : 'rgba(16,185,129,0.2)';
|
badge.style.background = 'rgba(16,185,129,0.2)';
|
||||||
badge.style.color = (data.call_status === 'ended') ? '#fca5a5' : '#34d399';
|
badge.style.color = '#34d399';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.active_peers && Array.isArray(data.active_peers) && latestCallStatus !== 'ended') {
|
||||||
|
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 || ''));
|
||||||
|
const isMicOn = isTrueVal(p.mic_on);
|
||||||
|
const isCamOn = isTrueVal(p.cam_on);
|
||||||
|
|
||||||
|
if (candidateLocalStream && candidatePeerInstance && candidatePeerInstance.open) {
|
||||||
|
if (!candidateInterviewerTiles.has(p.peer_id)) {
|
||||||
|
try {
|
||||||
|
const outCall = candidatePeerInstance.call(p.peer_id, candidateLocalStream);
|
||||||
|
if (outCall) {
|
||||||
|
outCall.on('stream', remoteStream => {
|
||||||
|
addOrUpdateCandidateInterviewerTile(p.peer_id, remoteStream, roleLabel, isMicOn, isCamOn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
} else {
|
||||||
|
const existingStream = candidateInterviewerTiles.get(p.peer_id);
|
||||||
|
addOrUpdateCandidateInterviewerTile(p.peer_id, existingStream, roleLabel, isMicOn, isCamOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
candidateInterviewerTiles.forEach((_, pid) => {
|
||||||
|
if (!activePeerIds.has(pid)) {
|
||||||
|
removeCandidateInterviewerTile(pid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, 6000);
|
}, 6000);
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
* Proctoring Alerts, SOS Audio Ringtone, Tab Switching, and Silent Call Recording.
|
* Proctoring Alerts, SOS Audio Ringtone, Tab Switching, and Silent Call Recording.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { initMeteredIceServers } from './metered.js';
|
||||||
|
|
||||||
// --- Global Audio Ringtone Synthesizer Engine ---
|
// --- Global Audio Ringtone Synthesizer Engine ---
|
||||||
export class CallRingtoneEngine {
|
export class CallRingtoneEngine {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -27,6 +29,8 @@ export class CallRingtoneEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
if (typeof document !== 'undefined' && document.body && document.body.dataset.interviewId) return;
|
||||||
|
if (typeof window !== 'undefined' && window.location.pathname.includes('/candidate/')) return;
|
||||||
this.init();
|
this.init();
|
||||||
if (this.isPlaying) return;
|
if (this.isPlaying) return;
|
||||||
this.isPlaying = true;
|
this.isPlaying = true;
|
||||||
@ -83,8 +87,62 @@ export const callRingtone = new CallRingtoneEngine();
|
|||||||
|
|
||||||
// State variables
|
// State variables
|
||||||
let currentInterviewId = null;
|
let currentInterviewId = null;
|
||||||
|
let currentSubmissionUniqueId = null;
|
||||||
let interviewerPeer = null;
|
let interviewerPeer = null;
|
||||||
let screenReceiverPeer = null;
|
let screenReceiverPeer = null;
|
||||||
|
|
||||||
|
export function getCandidatePeerId() {
|
||||||
|
if (!currentInterviewId && typeof document !== 'undefined' && document.body?.dataset?.interviewId) {
|
||||||
|
currentInterviewId = document.body.dataset.interviewId;
|
||||||
|
}
|
||||||
|
if (!currentSubmissionUniqueId && typeof document !== 'undefined' && document.body?.dataset?.submissionId) {
|
||||||
|
currentSubmissionUniqueId = document.body.dataset.submissionId;
|
||||||
|
}
|
||||||
|
if (!currentInterviewId) return '';
|
||||||
|
|
||||||
|
const subId = (currentSubmissionUniqueId && currentSubmissionUniqueId !== String(currentInterviewId))
|
||||||
|
? currentSubmissionUniqueId
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return subId ? ('cand_' + currentInterviewId + '_' + subId) : ('cand_' + currentInterviewId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMyInterviewerPeerId() {
|
||||||
|
const currentUserId = window.currentUserId || 0;
|
||||||
|
if (!currentInterviewId && typeof document !== 'undefined' && document.body?.dataset?.interviewId) {
|
||||||
|
currentInterviewId = document.body.dataset.interviewId;
|
||||||
|
}
|
||||||
|
if (!currentSubmissionUniqueId && typeof document !== 'undefined' && document.body?.dataset?.submissionId) {
|
||||||
|
currentSubmissionUniqueId = document.body.dataset.submissionId;
|
||||||
|
}
|
||||||
|
if (!currentInterviewId) return '';
|
||||||
|
|
||||||
|
const subId = (currentSubmissionUniqueId && currentSubmissionUniqueId !== String(currentInterviewId))
|
||||||
|
? currentSubmissionUniqueId
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return subId
|
||||||
|
? ('interviewer_' + currentInterviewId + '_' + subId + '_' + currentUserId)
|
||||||
|
: ('interviewer_' + currentInterviewId + '_' + currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInterviewerScreenPeerId() {
|
||||||
|
if (!currentInterviewId && typeof document !== 'undefined' && document.body?.dataset?.interviewId) {
|
||||||
|
currentInterviewId = document.body.dataset.interviewId;
|
||||||
|
}
|
||||||
|
if (!currentSubmissionUniqueId && typeof document !== 'undefined' && document.body?.dataset?.submissionId) {
|
||||||
|
currentSubmissionUniqueId = document.body.dataset.submissionId;
|
||||||
|
}
|
||||||
|
if (!currentInterviewId) return '';
|
||||||
|
|
||||||
|
const subId = (currentSubmissionUniqueId && currentSubmissionUniqueId !== String(currentInterviewId))
|
||||||
|
? currentSubmissionUniqueId
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return subId
|
||||||
|
? ('interviewer_screen_' + currentInterviewId + '_' + subId)
|
||||||
|
: ('interviewer_screen_' + currentInterviewId);
|
||||||
|
}
|
||||||
let interviewerLocalStream = null;
|
let interviewerLocalStream = null;
|
||||||
let activeCall = null;
|
let activeCall = null;
|
||||||
let isInterviewerMicOn = true;
|
let isInterviewerMicOn = true;
|
||||||
@ -177,7 +235,8 @@ let globalCallChannel = null;
|
|||||||
const adminPageLoadTimestamp = Date.now();
|
const adminPageLoadTimestamp = Date.now();
|
||||||
|
|
||||||
let globalIceServers = window.globalIceServers || [
|
let globalIceServers = window.globalIceServers || [
|
||||||
{ urls: 'stun:stun.l.google.com:19302' }
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun.cloudflare.com:3478' }
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getInitials(name) {
|
export function getInitials(name) {
|
||||||
@ -322,6 +381,9 @@ export function switchIdeTab(tab) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function triggerIncomingCallRing(callData, isRealtimeBroadcast = false) {
|
export function triggerIncomingCallRing(callData, isRealtimeBroadcast = false) {
|
||||||
|
if (!callData || !callData.id) return;
|
||||||
|
if (typeof document !== 'undefined' && document.body && document.body.dataset.interviewId) return;
|
||||||
|
if (typeof window !== 'undefined' && window.location.pathname.includes('/candidate/')) return;
|
||||||
if (endedCallIds.has(callData.id) || dismissedCallIds.has(callData.id)) return;
|
if (endedCallIds.has(callData.id) || dismissedCallIds.has(callData.id)) return;
|
||||||
if (iAmTheCaller && currentInterviewId == callData.id) return;
|
if (iAmTheCaller && currentInterviewId == callData.id) return;
|
||||||
if (interviewerLocalStream && currentInterviewId == callData.id) return;
|
if (interviewerLocalStream && currentInterviewId == callData.id) return;
|
||||||
@ -361,6 +423,9 @@ export function triggerIncomingCallRing(callData, isRealtimeBroadcast = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function pollGlobalActiveCalls() {
|
export function pollGlobalActiveCalls() {
|
||||||
|
if (typeof document !== 'undefined' && document.body && document.body.dataset.interviewId) return;
|
||||||
|
if (typeof window !== 'undefined' && window.location.pathname.includes('/candidate/')) return;
|
||||||
|
|
||||||
fetch('/interviews/active-calls', {
|
fetch('/interviews/active-calls', {
|
||||||
headers: { 'Accept': 'application/json' }
|
headers: { 'Accept': 'application/json' }
|
||||||
})
|
})
|
||||||
@ -505,15 +570,16 @@ export function subscribeLiveSync(interviewId) {
|
|||||||
const isCamOn = (data.cam_on !== undefined) ? Boolean(data.cam_on) : true;
|
const isCamOn = (data.cam_on !== undefined) ? Boolean(data.cam_on) : true;
|
||||||
updateCandidateStatusIcons(isMicOn, isCamOn);
|
updateCandidateStatusIcons(isMicOn, isCamOn);
|
||||||
}
|
}
|
||||||
setTimeout(pollReviewData, 100);
|
setTimeout(pollReviewData, 500);
|
||||||
} else if (data.type === 'violation_occurred' || data.type === 'violation_logged' || data.type === 'candidate_joined' || data.type === 'tab_switch') {
|
} else if (data.type === 'violation_occurred' || data.type === 'violation_logged' || data.type === 'candidate_joined' || data.type === 'tab_switch') {
|
||||||
setTimeout(pollReviewData, 200);
|
setTimeout(pollReviewData, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openReviewModal(id, name, uid, autoJoinCall = false) {
|
export function openReviewModal(id, name, uid, autoJoinCall = false) {
|
||||||
currentInterviewId = id;
|
currentInterviewId = id;
|
||||||
|
currentSubmissionUniqueId = uid;
|
||||||
acknowledgedViolationCount = 0;
|
acknowledgedViolationCount = 0;
|
||||||
sosDismissed = false;
|
sosDismissed = false;
|
||||||
|
|
||||||
@ -546,7 +612,7 @@ export function openReviewModal(id, name, uid, autoJoinCall = false) {
|
|||||||
subscribeLiveSync(id);
|
subscribeLiveSync(id);
|
||||||
pollReviewData();
|
pollReviewData();
|
||||||
if (sosPollInterval) clearInterval(sosPollInterval);
|
if (sosPollInterval) clearInterval(sosPollInterval);
|
||||||
sosPollInterval = setInterval(pollReviewData, 2500);
|
sosPollInterval = setInterval(pollReviewData, 6000);
|
||||||
|
|
||||||
if (peerHeartbeatTimer) clearInterval(peerHeartbeatTimer);
|
if (peerHeartbeatTimer) clearInterval(peerHeartbeatTimer);
|
||||||
peerHeartbeatTimer = setInterval(() => {
|
peerHeartbeatTimer = setInterval(() => {
|
||||||
@ -565,7 +631,7 @@ export function sendInterviewerPeerHeartbeat(action = 'heartbeat') {
|
|||||||
const currentUserId = window.currentUserId || 0;
|
const currentUserId = window.currentUserId || 0;
|
||||||
const currentUserName = window.currentUserName || 'Interviewer';
|
const currentUserName = window.currentUserName || 'Interviewer';
|
||||||
const currentUserRole = window.currentUserRole || 'interviewer';
|
const currentUserRole = window.currentUserRole || 'interviewer';
|
||||||
const myPeerId = 'interviewer_' + currentInterviewId + '_' + currentUserId;
|
const myPeerId = getMyInterviewerPeerId();
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || window.csrfToken;
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || window.csrfToken;
|
||||||
|
|
||||||
fetch(`/interviews/${currentInterviewId}/peer-heartbeat`, {
|
fetch(`/interviews/${currentInterviewId}/peer-heartbeat`, {
|
||||||
@ -615,10 +681,6 @@ export function updateCandidateStatusIcons(isMicOn, isCamOn, isJoined = true) {
|
|||||||
candMicIcon.className = 'fa-solid fa-microphone-slash text-slate-500 text-[10px]';
|
candMicIcon.className = 'fa-solid fa-microphone-slash text-slate-500 text-[10px]';
|
||||||
candMicIcon.title = 'Candidate Not Joined';
|
candMicIcon.title = 'Candidate Not Joined';
|
||||||
}
|
}
|
||||||
if (candCamIcon) {
|
|
||||||
candCamIcon.className = 'fa-solid fa-video-slash text-slate-500 text-[10px]';
|
|
||||||
candCamIcon.title = 'Candidate Not Joined';
|
|
||||||
}
|
|
||||||
if (candVidWrapper) {
|
if (candVidWrapper) {
|
||||||
candVidWrapper.style.opacity = '0.4';
|
candVidWrapper.style.opacity = '0.4';
|
||||||
candVidWrapper.style.filter = 'grayscale(60%)';
|
candVidWrapper.style.filter = 'grayscale(60%)';
|
||||||
@ -690,13 +752,14 @@ export function pollReviewData() {
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const currentUserId = window.currentUserId || 0;
|
const currentUserId = window.currentUserId || 0;
|
||||||
const myPeerId = 'interviewer_' + currentInterviewId + '_' + currentUserId;
|
const myPeerId = getMyInterviewerPeerId();
|
||||||
|
const candPeerId = getCandidatePeerId();
|
||||||
|
|
||||||
const candVid = document.getElementById('interviewer-cand-video');
|
const candVid = document.getElementById('interviewer-cand-video');
|
||||||
const candidateStreamConnected = candVid && candVid.srcObject && (candVid.srcObject.active || (candVid.srcObject.getTracks && candVid.srcObject.getTracks().length > 0));
|
const candidateStreamConnected = candVid && candVid.srcObject && (candVid.srcObject.active || (candVid.srcObject.getTracks && candVid.srcObject.getTracks().length > 0));
|
||||||
|
|
||||||
const candidatePeer = (data.active_peers && Array.isArray(data.active_peers))
|
const candidatePeer = (data.active_peers && Array.isArray(data.active_peers))
|
||||||
? data.active_peers.find(p => p.role === 'candidate' || (p.peer_id && p.peer_id.startsWith('cand_')))
|
? data.active_peers.find(p => p.role === 'candidate' || (p.peer_id && (p.peer_id === candPeerId || (p.peer_id.startsWith('cand_') && !p.peer_id.startsWith('cand_screen_')))))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const isCandidateJoined = Boolean(candidatePeer) || Boolean(candidateStreamConnected);
|
const isCandidateJoined = Boolean(candidatePeer) || Boolean(candidateStreamConnected);
|
||||||
@ -1243,17 +1306,7 @@ export function blockCandidateAction() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initMeteredIceServers() {
|
export { initMeteredIceServers };
|
||||||
if (typeof window.getMeteredIceServers === 'function') {
|
|
||||||
return window.getMeteredIceServers().then(servers => {
|
|
||||||
if (Array.isArray(servers) && servers.length > 0) {
|
|
||||||
globalIceServers = servers;
|
|
||||||
}
|
|
||||||
return globalIceServers;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve(globalIceServers);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function safePlayMediaStream(videoElem, stream, isSelf = false) {
|
export function safePlayMediaStream(videoElem, stream, isSelf = false) {
|
||||||
if (!videoElem || !stream) return;
|
if (!videoElem || !stream) return;
|
||||||
@ -1275,7 +1328,8 @@ export function safePlayMediaStream(videoElem, stream, isSelf = false) {
|
|||||||
|
|
||||||
export function addOrUpdatePanelistTile(peerId, stream, name = 'Panelist', micOn = true, camOn = true) {
|
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;
|
const myPeerId = getMyInterviewerPeerId();
|
||||||
|
if (!peerId || peerId === myPeerId) return;
|
||||||
const grid = document.getElementById('panelist-mesh-grid');
|
const grid = document.getElementById('panelist-mesh-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
@ -1341,21 +1395,23 @@ export function removePanelistTile(peerId) {
|
|||||||
|
|
||||||
export async function initInterviewerPeer() {
|
export async function initInterviewerPeer() {
|
||||||
if (!currentInterviewId || typeof window.Peer === 'undefined') return;
|
if (!currentInterviewId || typeof window.Peer === 'undefined') return;
|
||||||
await initMeteredIceServers();
|
const iceServers = await initMeteredIceServers();
|
||||||
|
|
||||||
const currentUserId = window.currentUserId || 0;
|
const currentUserId = window.currentUserId || 0;
|
||||||
const currentUserName = window.currentUserName || 'Interviewer';
|
const currentUserName = window.currentUserName || 'Interviewer';
|
||||||
const myPeerId = 'interviewer_' + currentInterviewId + '_' + currentUserId;
|
const myPeerId = getMyInterviewerPeerId();
|
||||||
|
const candPeerId = getCandidatePeerId();
|
||||||
|
|
||||||
if (interviewerPeer && !interviewerPeer.destroyed) {
|
if (interviewerPeer && !interviewerPeer.destroyed) {
|
||||||
try { interviewerPeer.destroy(); } catch(e) {}
|
try { interviewerPeer.destroy(); } catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
interviewerPeer = new window.Peer(myPeerId, {
|
interviewerPeer = new window.Peer(myPeerId, {
|
||||||
config: { iceServers: globalIceServers }
|
config: { iceServers: iceServers }
|
||||||
});
|
});
|
||||||
|
|
||||||
interviewerPeer.on('open', id => {
|
interviewerPeer.on('open', id => {
|
||||||
|
console.log('[WebRTC Interviewer] Registered PeerJS ID:', id);
|
||||||
if (interviewerSigChannel) {
|
if (interviewerSigChannel) {
|
||||||
interviewerSigChannel.postMessage({
|
interviewerSigChannel.postMessage({
|
||||||
type: 'peer_joined',
|
type: 'peer_joined',
|
||||||
@ -1367,17 +1423,22 @@ export async function initInterviewerPeer() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
interviewerPeer.on('call', call => {
|
interviewerPeer.on('call', async call => {
|
||||||
|
console.log('[WebRTC Interviewer] Incoming PeerJS call from:', call.peer);
|
||||||
const sendStream = interviewerLocalStream || new MediaStream();
|
const sendStream = interviewerLocalStream || new MediaStream();
|
||||||
try { call.answer(sendStream); } catch(e) {}
|
try { call.answer(sendStream); } catch(e) {
|
||||||
|
console.error('[WebRTC Interviewer] Error answering call:', e);
|
||||||
|
}
|
||||||
|
|
||||||
call.on('stream', remoteStream => {
|
call.on('stream', remoteStream => {
|
||||||
if (call.peer === 'cand_' + currentInterviewId) {
|
console.log('[WebRTC Interviewer] Stream received from peer:', call.peer);
|
||||||
|
if (call.peer === candPeerId || (call.peer && call.peer.startsWith('cand_') && !call.peer.startsWith('cand_screen_'))) {
|
||||||
|
console.log('[WebRTC Interviewer] Candidate video stream connected! Candidate ID:', call.peer);
|
||||||
const candVid = document.getElementById('interviewer-cand-video');
|
const candVid = document.getElementById('interviewer-cand-video');
|
||||||
if (candVid) safePlayMediaStream(candVid, remoteStream);
|
if (candVid) safePlayMediaStream(candVid, remoteStream);
|
||||||
const candCamIcon = document.getElementById('cand-cam-status');
|
const candCamIcon = document.getElementById('cand-cam-status');
|
||||||
const isCamOn = candCamIcon ? candCamIcon.classList.contains('fa-video') : true;
|
const isCamOn = candCamIcon ? candCamIcon.classList.contains('fa-video') : true;
|
||||||
updateCandidateStatusIcons(undefined, isCamOn);
|
updateCandidateStatusIcons(true, isCamOn, true);
|
||||||
} else {
|
} else {
|
||||||
addOrUpdatePanelistTile(call.peer, remoteStream, 'Panelist');
|
addOrUpdatePanelistTile(call.peer, remoteStream, 'Panelist');
|
||||||
}
|
}
|
||||||
@ -1388,14 +1449,18 @@ export async function initInterviewerPeer() {
|
|||||||
badge.className = 'px-2.5 py-1 text-xs font-semibold rounded-full bg-emerald-500/30 text-emerald-300 border border-emerald-500';
|
badge.className = 'px-2.5 py-1 text-xs font-semibold rounded-full bg-emerald-500/30 text-emerald-300 border border-emerald-500';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
call.on('error', err => {
|
||||||
|
console.error('[WebRTC Interviewer] Call error from peer:', call.peer, err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const screenPeerId = 'interviewer_screen_' + currentInterviewId;
|
const screenPeerId = getInterviewerScreenPeerId();
|
||||||
if (screenReceiverPeer && !screenReceiverPeer.destroyed) {
|
if (screenReceiverPeer && !screenReceiverPeer.destroyed) {
|
||||||
try { screenReceiverPeer.destroy(); } catch(e) {}
|
try { screenReceiverPeer.destroy(); } catch(e) {}
|
||||||
}
|
}
|
||||||
screenReceiverPeer = new window.Peer(screenPeerId, {
|
screenReceiverPeer = new window.Peer(screenPeerId, {
|
||||||
config: { iceServers: globalIceServers }
|
config: { iceServers: iceServers }
|
||||||
});
|
});
|
||||||
screenReceiverPeer.on('call', call => {
|
screenReceiverPeer.on('call', call => {
|
||||||
call.answer();
|
call.answer();
|
||||||
@ -1415,7 +1480,8 @@ export function initInterviewerSigChannel() {
|
|||||||
interviewerSigChannel = new BroadcastChannel('webrtc_call_' + currentInterviewId);
|
interviewerSigChannel = new BroadcastChannel('webrtc_call_' + currentInterviewId);
|
||||||
const currentUserId = window.currentUserId || 0;
|
const currentUserId = window.currentUserId || 0;
|
||||||
const currentUserName = window.currentUserName || 'Interviewer';
|
const currentUserName = window.currentUserName || 'Interviewer';
|
||||||
const myPeerId = 'interviewer_' + currentInterviewId + '_' + currentUserId;
|
const myPeerId = getMyInterviewerPeerId();
|
||||||
|
const candPeerId = getCandidatePeerId();
|
||||||
|
|
||||||
interviewerSigChannel.onmessage = async (event) => {
|
interviewerSigChannel.onmessage = async (event) => {
|
||||||
const msg = event.data;
|
const msg = event.data;
|
||||||
@ -1442,7 +1508,7 @@ export function initInterviewerSigChannel() {
|
|||||||
const outCall = interviewerPeer.call(msg.peer_id, interviewerLocalStream);
|
const outCall = interviewerPeer.call(msg.peer_id, interviewerLocalStream);
|
||||||
if (outCall) {
|
if (outCall) {
|
||||||
outCall.on('stream', remoteStream => {
|
outCall.on('stream', remoteStream => {
|
||||||
if (msg.peer_id === 'cand_' + currentInterviewId) {
|
if (msg.peer_id === candPeerId || (msg.peer_id && msg.peer_id.startsWith('cand_') && !msg.peer_id.startsWith('cand_screen_'))) {
|
||||||
const candVid = document.getElementById('interviewer-cand-video');
|
const candVid = document.getElementById('interviewer-cand-video');
|
||||||
if (candVid) safePlayMediaStream(candVid, remoteStream);
|
if (candVid) safePlayMediaStream(candVid, remoteStream);
|
||||||
const candCamIcon = document.getElementById('cand-cam-status');
|
const candCamIcon = document.getElementById('cand-cam-status');
|
||||||
@ -1463,7 +1529,7 @@ export function initInterviewerSigChannel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'peer_left') {
|
} else if (msg.type === 'peer_left') {
|
||||||
if (msg.peer_id === 'cand_' + currentInterviewId) {
|
if (msg.peer_id === candPeerId || (msg.peer_id && msg.peer_id.startsWith('cand_') && !msg.peer_id.startsWith('cand_screen_'))) {
|
||||||
const placeholder = document.getElementById('cand-video-placeholder');
|
const placeholder = document.getElementById('cand-video-placeholder');
|
||||||
if (placeholder) placeholder.style.display = 'flex';
|
if (placeholder) placeholder.style.display = 'flex';
|
||||||
} else if (msg.peer_id) {
|
} else if (msg.peer_id) {
|
||||||
@ -1545,7 +1611,7 @@ export function startInterviewerCall(isRejoin = false) {
|
|||||||
|
|
||||||
const makePeerJSCall = () => {
|
const makePeerJSCall = () => {
|
||||||
if (!interviewerPeer || interviewerPeer.destroyed) return;
|
if (!interviewerPeer || interviewerPeer.destroyed) return;
|
||||||
const candPeerId = 'cand_' + currentInterviewId;
|
const candPeerId = getCandidatePeerId();
|
||||||
activeCall = interviewerPeer.call(candPeerId, stream);
|
activeCall = interviewerPeer.call(candPeerId, stream);
|
||||||
|
|
||||||
if (activeCall) {
|
if (activeCall) {
|
||||||
@ -2087,6 +2153,8 @@ if (typeof window !== 'undefined') {
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
if (data.type === 'incoming_call') {
|
if (data.type === 'incoming_call') {
|
||||||
|
if (typeof document !== 'undefined' && document.body && document.body.dataset.interviewId) return;
|
||||||
|
if (typeof window !== 'undefined' && window.location.pathname.includes('/candidate/')) return;
|
||||||
triggerIncomingCallRing({
|
triggerIncomingCallRing({
|
||||||
id: data.interview_id,
|
id: data.interview_id,
|
||||||
candidate_name: data.candidate_name,
|
candidate_name: data.candidate_name,
|
||||||
@ -2110,5 +2178,5 @@ if (typeof window !== 'undefined') {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setInterval(pollGlobalActiveCalls, 1500);
|
setInterval(pollGlobalActiveCalls, 6000);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,14 +3,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
let globalIceServers = [
|
let globalIceServers = [
|
||||||
{ urls: 'stun:stun.l.google.com:19302' }
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun.cloudflare.com:3478' },
|
||||||
];
|
];
|
||||||
let meteredIceFetchPromise = null;
|
let meteredIceFetchPromise = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch ICE servers from /ice-servers endpoint and merge with default STUN server.
|
* Fetch ICE servers from /ice-servers endpoint and merge with default STUN server.
|
||||||
* Handles network errors gracefully with fallback.
|
* Handles network errors gracefully with fallback.
|
||||||
*
|
*
|
||||||
* @returns {Promise<Array>} Resolves with the array of ICE server objects.
|
* @returns {Promise<Array>} Resolves with the array of ICE server objects.
|
||||||
*/
|
*/
|
||||||
export function getMeteredIceServers() {
|
export function getMeteredIceServers() {
|
||||||
@ -32,15 +33,20 @@ export function getMeteredIceServers() {
|
|||||||
if (Array.isArray(meteredIce) && meteredIce.length > 0) {
|
if (Array.isArray(meteredIce) && meteredIce.length > 0) {
|
||||||
globalIceServers = [
|
globalIceServers = [
|
||||||
{ urls: 'stun:stun.l.google.com:19302' },
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
...meteredIce.slice(0, 3)
|
{ urls: 'stun:stun.cloudflare.com:3478' },
|
||||||
|
...meteredIce
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
window.globalIceServers = globalIceServers;
|
if (typeof window !== 'undefined') {
|
||||||
|
window.globalIceServers = globalIceServers;
|
||||||
|
}
|
||||||
return globalIceServers;
|
return globalIceServers;
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.warn('Metered TURN fetch warning:', e);
|
console.warn('Metered TURN fetch warning:', e);
|
||||||
window.globalIceServers = globalIceServers;
|
if (typeof window !== 'undefined') {
|
||||||
|
window.globalIceServers = globalIceServers;
|
||||||
|
}
|
||||||
return globalIceServers;
|
return globalIceServers;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@
|
|||||||
details.meta[open] summary i.chev { transform: rotate(180deg); }
|
details.meta[open] summary i.chev { transform: rotate(180deg); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen flex flex-col antialiased">
|
<body class="min-h-screen flex flex-col antialiased" data-interview-id="{{ $interview->id }}" data-submission-id="{{ $interview->submission_unique_id }}">
|
||||||
|
|
||||||
<div class="w-full mx-auto px-7 py-5 pb-16 flex-1">
|
<div class="w-full mx-auto px-7 py-5 pb-16 flex-1">
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user