fix: recursive polling, candiate video reconnecting

- made all polling to 6s
- fixed candidate video being reconnected in interval
This commit is contained in:
kushal.saha 2026-08-13 07:40:08 +00:00
parent 78ed98bc43
commit ab00516c42
4 changed files with 50 additions and 57 deletions

View File

@ -663,11 +663,11 @@ public function getPollData($id)
}
}
// Clean up stale active_peers (> 15 seconds)
// Clean up stale active_peers (> 30 seconds)
$activePeers = $interview->active_peers ?? [];
$now = now()->timestamp;
$validPeers = array_values(array_filter($activePeers, function ($p) use ($now) {
return ($now - ($p['last_seen'] ?? 0)) < 15;
return ($now - ($p['last_seen'] ?? 0)) < 30;
}));
if (count($validPeers) !== count($activePeers)) {
$interview->update(['active_peers' => $validPeers]);
@ -726,12 +726,13 @@ public function registerPeerHeartbeat(Request $request, $id)
$role = $request->input('role', $user ? ($user->is_admin ? 'admin' : 'interviewer') : 'candidate');
$userId = $user ? $user->id : 0;
$interview->refresh();
$activePeers = $interview->active_peers ?? [];
$now = now()->timestamp;
$updatedPeers = [];
foreach ($activePeers as $p) {
if (($now - ($p['last_seen'] ?? 0)) < 15) {
if (($now - ($p['last_seen'] ?? 0)) < 30) {
if ($action === 'leave' && ($p['peer_id'] ?? '') === $peerId) {
continue;
}

View File

@ -1135,7 +1135,7 @@ class CandidateProctoring {
const objectDetector = await this.withAmdLoaderDisabled(() =>
this.createModel(vision.ObjectDetector, fileset, {
modelAssetPath: OBJECT_MODEL_URL,
runningMode: 'VIDEO', maxResults: 10, scoreThreshold: 0.15,
runningMode: 'VIDEO', maxResults: 10, scoreThreshold: 0.35,
})
);
this.phoneDetector = new PhoneDetector(objectDetector);

View File

@ -271,26 +271,12 @@ export function saveCanvasDrawing(force = false) {
}
// --- Candidate Chat Polling & Sending ---
export function pollCandidateChat() {
const interviewId = document.body.dataset.interviewId;
if (!interviewId) return;
fetch(`/candidate/poll/${interviewId}`, {
headers: { 'Accept': 'application/json' }
})
.then(r => {
if (!r.ok) return null;
const contentType = r.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) return null;
return r.json();
})
.then(data => {
export function pollCandidateChat(data) {
if (!data) return;
const chatHistory = document.getElementById('candidate-chat-history');
if (!chatHistory) return;
if (data.status === 'blocked' || data.is_expired) {
window.location.reload();
return;
}
@ -318,8 +304,6 @@ export function pollCandidateChat() {
chatHistory.innerHTML = html;
chatHistory.scrollTop = chatHistory.scrollHeight;
}
})
.catch(() => {});
}
export function sendCandidateMessage() {
@ -345,7 +329,13 @@ export function sendCandidateMessage() {
})
.then(r => r.json())
.then(() => {
pollCandidateChat();
fetch(`/candidate/poll/${interviewId}`, {
headers: { 'Accept': 'application/json' }
})
.then(r => r.json())
.then(data => {
if (data) pollCandidateChat(data);
});
});
}
@ -972,10 +962,6 @@ export function initCandidateRoom() {
// Start Candidate Camera
startCandidateCameraStream();
// Chat polling
setInterval(pollCandidateChat, 600);
pollCandidateChat();
// Heartbeat & Polling
if (candidateHeartbeatIntervalTimer) clearInterval(candidateHeartbeatIntervalTimer);
sendCandidatePeerHeartbeat();
@ -985,7 +971,6 @@ export function initCandidateRoom() {
setInterval(() => {
if (!interviewId) return;
sendCandidatePeerHeartbeat('heartbeat');
fetch('/candidate/poll/' + interviewId, {
headers: { 'Accept': 'application/json' }
@ -994,6 +979,8 @@ export function initCandidateRoom() {
.then(data => {
if (!data) return;
pollCandidateChat(data);
if (data.active_peers && Array.isArray(data.active_peers)) {
const activePeerIds = new Set(data.active_peers.map(p => p.peer_id));

View File

@ -606,7 +606,7 @@ export function updateCandidateStatusIcons(isMicOn, isCamOn, isJoined = true) {
const screenWrapper = document.getElementById('screen-wrapper');
const joinedPill = document.getElementById('candidate-joined-pill');
if (!isJoined) {
if (!isJoined && !candidateStreamConnected) {
if (joinedPill) {
joinedPill.className = 'pill gray inline-flex items-center gap-1.5 font-semibold text-[11px] px-2.5 py-0.5 rounded-full border border-slate-700 text-slate-400 bg-slate-800/60';
joinedPill.innerHTML = '<i class="fa-solid fa-circle text-[8px] text-slate-500"></i> Not Joined';
@ -692,13 +692,18 @@ export function pollReviewData() {
const currentUserId = window.currentUserId || 0;
const myPeerId = 'interviewer_' + currentInterviewId + '_' + currentUserId;
const candVid = document.getElementById('interviewer-cand-video');
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))
? data.active_peers.find(p => p.role === 'candidate' || (p.peer_id && p.peer_id.startsWith('cand_')))
: null;
if (candidatePeer) {
const isMicOn = isTrueVal(candidatePeer.mic_on);
const isCamOn = isTrueVal(candidatePeer.cam_on);
const isCandidateJoined = Boolean(candidatePeer) || Boolean(candidateStreamConnected);
if (isCandidateJoined) {
const isMicOn = candidatePeer ? isTrueVal(candidatePeer.mic_on) : true;
const isCamOn = candidatePeer ? isTrueVal(candidatePeer.cam_on) : true;
updateCandidateStatusIcons(isMicOn, isCamOn, true);
} else {
updateCandidateStatusIcons(false, false, false);