improvement: tab switch and overlay detection
Improved the detection so that when candiate is selection screen share OS Native modal, or allowing permissions should not detect as a Suspecious activity
This commit is contained in:
parent
d913dded0b
commit
5e87dcfea2
@ -565,6 +565,24 @@ function submitSolution() {
|
|||||||
|
|
||||||
let isTabSwitchScreenshotEnabled = true;
|
let isTabSwitchScreenshotEnabled = true;
|
||||||
let lastTabSwitchTime = 0;
|
let lastTabSwitchTime = 0;
|
||||||
|
let pendingNativePrompts = 0; // counter for native OS prompts
|
||||||
|
let suppressFocusViolationsUntil = 0; // trailing buffer after prompt resolves
|
||||||
|
|
||||||
|
function beginNativePrompt() {
|
||||||
|
pendingNativePrompts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endNativePrompt() {
|
||||||
|
pendingNativePrompts = Math.max(0, pendingNativePrompts - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function armTrailingBuffer(ms) {
|
||||||
|
suppressFocusViolationsUntil = Math.max(suppressFocusViolationsUntil, Date.now() + ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFocusSuppressed() {
|
||||||
|
return pendingNativePrompts > 0 || Date.now() < suppressFocusViolationsUntil;
|
||||||
|
}
|
||||||
let latestCallStatus = '{{ $interview->call_status ?? "idle" }}';
|
let latestCallStatus = '{{ $interview->call_status ?? "idle" }}';
|
||||||
let screenShareVideoElem = null;
|
let screenShareVideoElem = null;
|
||||||
|
|
||||||
@ -728,6 +746,7 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
|
|||||||
|
|
||||||
// 1. Tab Switch Detection (Captures Candidate's SYSTEM DESKTOP MONITOR screenshot)
|
// 1. Tab Switch Detection (Captures Candidate's SYSTEM DESKTOP MONITOR screenshot)
|
||||||
document.addEventListener('visibilitychange', function () {
|
document.addEventListener('visibilitychange', function () {
|
||||||
|
if (isFocusSuppressed()) return;
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTabSwitchTime > 1000) {
|
if (now - lastTabSwitchTime > 1000) {
|
||||||
@ -740,6 +759,7 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
|
|||||||
|
|
||||||
// 2. Window Focus Loss / External AI Overlay Application Switch Detection
|
// 2. Window Focus Loss / External AI Overlay Application Switch Detection
|
||||||
window.addEventListener('blur', function () {
|
window.addEventListener('blur', function () {
|
||||||
|
if (isFocusSuppressed()) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTabSwitchTime > 1000) {
|
if (now - lastTabSwitchTime > 1000) {
|
||||||
lastTabSwitchTime = now;
|
lastTabSwitchTime = now;
|
||||||
@ -750,6 +770,7 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
|
|||||||
|
|
||||||
// Mouse Viewport Departure (Candidate moving cursor outside window to interact with Parakeet AI or secondary app overlay)
|
// Mouse Viewport Departure (Candidate moving cursor outside window to interact with Parakeet AI or secondary app overlay)
|
||||||
document.addEventListener('mouseleave', function () {
|
document.addEventListener('mouseleave', function () {
|
||||||
|
if (isFocusSuppressed()) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTabSwitchTime > 2500) {
|
if (now - lastTabSwitchTime > 2500) {
|
||||||
lastTabSwitchTime = now;
|
lastTabSwitchTime = now;
|
||||||
@ -1550,6 +1571,8 @@ function startCandidateCameraStream() {
|
|||||||
autoGainControl: true
|
autoGainControl: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beginNativePrompt();
|
||||||
|
|
||||||
return navigator.mediaDevices.getUserMedia({
|
return navigator.mediaDevices.getUserMedia({
|
||||||
video: {
|
video: {
|
||||||
width: { ideal: 1280 },
|
width: { ideal: 1280 },
|
||||||
@ -1563,6 +1586,7 @@ function startCandidateCameraStream() {
|
|||||||
.catch(() => navigator.mediaDevices.getUserMedia({ video: false, audio: true }))
|
.catch(() => navigator.mediaDevices.getUserMedia({ video: false, audio: true }))
|
||||||
.catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: false }))
|
.catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: false }))
|
||||||
.then(stream => {
|
.then(stream => {
|
||||||
|
endNativePrompt();
|
||||||
localMediaStream = stream;
|
localMediaStream = stream;
|
||||||
const vid = document.getElementById('webcam');
|
const vid = document.getElementById('webcam');
|
||||||
if (vid) {
|
if (vid) {
|
||||||
@ -1589,6 +1613,7 @@ function startCandidateCameraStream() {
|
|||||||
return stream;
|
return stream;
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
endNativePrompt();
|
||||||
console.log('Candidate media access error:', err);
|
console.log('Candidate media access error:', err);
|
||||||
if (avatarPlaceholder) avatarPlaceholder.style.display = 'flex';
|
if (avatarPlaceholder) avatarPlaceholder.style.display = 'flex';
|
||||||
initCandidatePeer();
|
initCandidatePeer();
|
||||||
@ -1729,6 +1754,17 @@ function toggleCam() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestFullscreenAsPromise(elem) {
|
||||||
|
if (elem.requestFullscreen) {
|
||||||
|
return elem.requestFullscreen();
|
||||||
|
}
|
||||||
|
if (elem.webkitRequestFullscreen) {
|
||||||
|
elem.webkitRequestFullscreen();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('Fullscreen not supported'));
|
||||||
|
}
|
||||||
|
|
||||||
function requestProctoringFullscreen() {
|
function requestProctoringFullscreen() {
|
||||||
try {
|
try {
|
||||||
const elem = document.documentElement;
|
const elem = document.documentElement;
|
||||||
@ -1741,6 +1777,7 @@ function requestProctoringFullscreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', function() {
|
document.addEventListener('fullscreenchange', function() {
|
||||||
|
if (isFocusSuppressed()) return;
|
||||||
if (!document.fullscreenElement) {
|
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 (possible overlay AI window interaction)');
|
||||||
captureAndUploadTabScreenshot();
|
captureAndUploadTabScreenshot();
|
||||||
@ -1826,6 +1863,8 @@ function startScreenShare() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beginNativePrompt();
|
||||||
|
|
||||||
navigator.mediaDevices.getDisplayMedia({
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
video: {
|
video: {
|
||||||
displaySurface: 'monitor'
|
displaySurface: 'monitor'
|
||||||
@ -1833,6 +1872,7 @@ function startScreenShare() {
|
|||||||
audio: false
|
audio: false
|
||||||
})
|
})
|
||||||
.then(stream => {
|
.then(stream => {
|
||||||
|
endNativePrompt();
|
||||||
const track = stream.getVideoTracks()[0];
|
const track = stream.getVideoTracks()[0];
|
||||||
const settings = track.getSettings ? track.getSettings() : {};
|
const settings = track.getSettings ? track.getSettings() : {};
|
||||||
|
|
||||||
@ -1870,7 +1910,13 @@ function startScreenShare() {
|
|||||||
btn.style.background = '#10b981';
|
btn.style.background = '#10b981';
|
||||||
}
|
}
|
||||||
|
|
||||||
requestProctoringFullscreen();
|
beginNativePrompt();
|
||||||
|
requestFullscreenAsPromise(document.documentElement)
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
endNativePrompt();
|
||||||
|
armTrailingBuffer(1000);
|
||||||
|
});
|
||||||
|
|
||||||
// Connect screen stream to interviewer screen peer
|
// Connect screen stream to interviewer screen peer
|
||||||
const screenPeer = new Peer('cand_screen_' + interviewId + '_' + Date.now());
|
const screenPeer = new Peer('cand_screen_' + interviewId + '_' + Date.now());
|
||||||
@ -1891,6 +1937,7 @@ function startScreenShare() {
|
|||||||
initLiveOverlayDetector();
|
initLiveOverlayDetector();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
endNativePrompt();
|
||||||
console.log('Screen sharing cancelled/denied:', err);
|
console.log('Screen sharing cancelled/denied:', err);
|
||||||
screenShareStream = null;
|
screenShareStream = null;
|
||||||
logViolation('tab_switch', 'Candidate denied or cancelled screen sharing permission prompt');
|
logViolation('tab_switch', 'Candidate denied or cancelled screen sharing permission prompt');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user