diff --git a/resources/js/candidate-proctor.js b/resources/js/candidate-proctor.js index 67d1aac..a418df7 100644 --- a/resources/js/candidate-proctor.js +++ b/resources/js/candidate-proctor.js @@ -343,6 +343,70 @@ export function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') { .catch(err => console.log('System screenshot fetch error:', err)); } +export function isEntireScreenShare(track) { + if (!track || !track.getSettings) return true; + const settings = track.getSettings(); + const surface = settings.displaySurface; + + if (surface) { + const allowedSurfaces = ['monitor', 'screen']; + if (!allowedSurfaces.includes(surface)) { + return false; + } + } + + if (window.screen && window.screen.width && settings.width) { + if (settings.width < (window.screen.width * 0.65) && settings.height < (window.screen.height * 0.65)) { + return false; + } + } + + return true; +} + +export function isScreenSharingActive() { + return Boolean(screenShareStream && screenShareStream.active && screenShareStream.getVideoTracks().some(t => t.readyState === 'live')); +} + +export function enforceBrowserFullscreen() { + if (!isScreenSharingActive() || document.fullscreenElement) return; + + beginNativePrompt(); + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen() + .then(() => { + endNativePrompt(); + armTrailingBuffer(1000); + }) + .catch(() => { + endNativePrompt(); + showFullscreenRequiredModal(); + }); + } else { + endNativePrompt(); + showFullscreenRequiredModal(); + } +} + +export function showFullscreenRequiredModal() { + if (!isScreenSharingActive() || document.fullscreenElement) return; + if (typeof window.Swal !== 'undefined') { + window.Swal.fire({ + icon: 'warning', + title: 'Fullscreen Mode Required', + text: 'Your assessment must be conducted in Fullscreen mode while screen sharing. Please click below to enter Fullscreen.', + confirmButtonText: 'Enter Fullscreen Mode', + confirmButtonColor: '#6366f1', + allowOutsideClick: false, + allowEscapeKey: false + }).then(() => { + if (isScreenSharingActive()) { + enforceBrowserFullscreen(); + } + }); + } +} + // --- Screen Share Enforcement & Live Overlay Detector (Category 6) --- export function startScreenShare() { if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) { @@ -361,26 +425,27 @@ export function startScreenShare() { .then(stream => { endNativePrompt(); const track = stream.getVideoTracks()[0]; - const settings = track.getSettings ? track.getSettings() : {}; - // Enforce ENTIRE SCREEN share - if (settings.displaySurface && settings.displaySurface !== 'monitor') { + // Enforce ENTIRE SCREEN share across all browsers + if (!isEntireScreenShare(track)) { + track.stop(); + screenShareStream = null; + logViolation('tab_switch', 'Candidate attempted non-entire screen share (tab or window)'); + if (typeof window.Swal !== 'undefined') { window.Swal.fire({ - icon: 'warning', + icon: 'error', title: 'Entire Screen Share Required', - text: 'You selected a single tab or window. You MUST select "Entire Screen".', - confirmButtonText: 'Reshare Entire Screen', - confirmButtonColor: '#6366f1', + html: 'You selected a single tab or window.

You MUST select "Entire Screen" (or whole monitor) to continue your interview.', + confirmButtonText: 'Reshare Entire Screen Now', + confirmButtonColor: '#ef4444', allowOutsideClick: false, allowEscapeKey: false }).then(() => { - track.stop(); setTimeout(startScreenShare, 300); }); } else { alert('Entire Screen share is required. Please select Entire Screen.'); - track.stop(); setTimeout(startScreenShare, 300); } return; @@ -391,21 +456,9 @@ export function startScreenShare() { vidElem.srcObject = stream; vidElem.play().catch(() => {}); - const btn = document.getElementById('share-screen-btn'); - if (btn) { - btn.innerText = '🟢 Entire Screen Sharing Active'; - btn.style.background = '#10b981'; - } + updateScreenShareButton(true); - beginNativePrompt(); - if (document.documentElement.requestFullscreen) { - document.documentElement.requestFullscreen() - .catch(() => {}) - .finally(() => { - endNativePrompt(); - armTrailingBuffer(1000); - }); - } + enforceBrowserFullscreen(); const interviewId = document.body.dataset.interviewId; if (typeof window.Peer !== 'undefined' && interviewId) { @@ -417,10 +470,12 @@ export function startScreenShare() { track.onended = () => { screenShareStream = null; - if (btn) { - btn.innerText = '🖥️ Share Screen with Interviewer'; - btn.style.background = 'linear-gradient(135deg, #6366f1 0%, #0078d4 100%)'; + updateScreenShareButton(false); + + if (document.fullscreenElement) { + try { document.exitFullscreen().catch(() => {}); } catch(e) {} } + logViolation('tab_switch', 'Candidate stopped screen sharing'); }; @@ -430,14 +485,50 @@ export function startScreenShare() { endNativePrompt(); console.log('Screen sharing cancelled/denied:', err); screenShareStream = null; - const btn = document.getElementById('share-screen-btn'); - if (btn) { - btn.innerText = '🖥️ Share Screen with Interviewer'; - btn.style.background = 'linear-gradient(135deg, #6366f1 0%, #0078d4 100%)'; - } + updateScreenShareButton(false); }); } +export function stopScreenShare() { + if (screenShareStream) { + try { + screenShareStream.getTracks().forEach(track => track.stop()); + } catch(e) {} + screenShareStream = null; + } + updateScreenShareButton(false); + + if (document.fullscreenElement) { + try { document.exitFullscreen().catch(() => {}); } catch(e) {} + } + + logViolation('tab_switch', 'Candidate stopped screen sharing'); +} + +export function toggleScreenShare() { + if (screenShareStream && screenShareStream.active && screenShareStream.getVideoTracks().length > 0 && screenShareStream.getVideoTracks()[0].readyState === 'live') { + stopScreenShare(); + } else { + startScreenShare(); + } +} + +export function updateScreenShareButton(active) { + const btn = document.getElementById('share-screen-btn'); + if (!btn) return; + if (active) { + btn.innerHTML = '🛑 Stop Screen Sharing'; + btn.style.background = '#ef4444'; + btn.style.borderColor = '#dc2626'; + btn.onclick = stopScreenShare; + } else { + btn.innerHTML = '🖥️ Share Screen with Interviewer'; + btn.style.background = 'linear-gradient(135deg, #6366f1 0%, #0078d4 100%)'; + btn.style.borderColor = '#6366f1'; + btn.onclick = startScreenShare; + } +} + export function initLiveOverlayDetector() { if (overlayCheckInterval) clearInterval(overlayCheckInterval); @@ -624,12 +715,13 @@ export function initCandidateProctoringListeners() { } }, 6000); - // 8. Fullscreen Exit Listener + // 8. Fullscreen Exit Listener (Only enforced when screen sharing is active) document.addEventListener('fullscreenchange', function() { - if (isFocusSuppressed()) return; + if (isFocusSuppressed() || !isScreenSharingActive()) return; if (!document.fullscreenElement) { - logViolation('tab_switch', 'Candidate exited full-screen proctoring mode (possible overlay AI window interaction)'); + logViolation('tab_switch', 'Candidate exited full-screen proctoring mode while screen sharing'); captureAndUploadTabScreenshot(); + showFullscreenRequiredModal(); } }); } @@ -1201,7 +1293,11 @@ if (typeof window !== 'undefined') { window.endNativePrompt = endNativePrompt; window.armTrailingBuffer = armTrailingBuffer; window.isFocusSuppressed = isFocusSuppressed; + window.isScreenSharingActive = isScreenSharingActive; window.startScreenShare = startScreenShare; + window.stopScreenShare = stopScreenShare; + window.toggleScreenShare = toggleScreenShare; + window.updateScreenShareButton = updateScreenShareButton; window.initLiveOverlayDetector = initLiveOverlayDetector; window.initQuestionRepeatDetection = initQuestionRepeatDetection; window.initCandidateProctoringListeners = initCandidateProctoringListeners;