File size: 6,392 Bytes
df53738 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | const video = document.getElementById('videoFeed');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const modal = document.getElementById('successModal');
const modalName = document.getElementById('modalName');
const modalTime = document.getElementById('modalTime');
const cameraContainer = document.getElementById('cameraContainer');
// Spoof toast
const spoofToast = document.getElementById('spoofToast');
const spoofToastMessage = document.getElementById('spoofToastMessage');
// Frame buffer for sequence liveness (motion + blink)
const FRAME_BUFFER_SIZE = 5;
let frameBuffer = [];
let isScanning = true;
let stream = null;
let toastDismissTimer = null;
let lastFaceAlertAt = 0;
const FACE_ALERT_COOLDOWN_MS = 4000;
// βββ Camera βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function startCamera() {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user', width: 640, height: 480 }
});
video.srcObject = stream;
startCaptureLoop();
} catch (err) {
console.error("Camera error:", err);
statusText.textContent = "Camera access denied or unavailable";
statusText.style.color = "red";
}
}
function stopCamera() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
}
function captureFrame() {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0);
return canvas.toDataURL('image/jpeg', 0.8);
}
// βββ Capture loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function startCaptureLoop() {
while (isScanning) {
if (video.readyState === video.HAVE_ENOUGH_DATA) {
const frame = captureFrame();
frameBuffer.push(frame);
if (frameBuffer.length > FRAME_BUFFER_SIZE) frameBuffer.shift();
const payload = frameBuffer.length >= 2
? { frames: frameBuffer.slice() }
: { frame: frame };
try {
const response = await fetch('/api/recognize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
handleResult(result);
} catch (e) {
console.log("Network error", e);
}
}
await new Promise(r => setTimeout(r, 800));
}
}
// βββ Result handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function handleResult(result) {
if (result.status === 'success') {
showSuccess(result);
if (typeof showSnackbar === 'function') {
showSnackbar('Checked in at ' + (result.timestamp || ''), 'success');
}
} else if (result.status === 'already_marked') {
statusText.textContent = `Already marked: ${result.name}`;
statusText.style.color = "#FFD700";
if (typeof showSnackbar === 'function') showSnackbar('Already marked today: ' + result.name, 'info');
} else if (result.status === 'spoof') {
showSpoofToast(result);
} else if (result.status === 'unknown') {
statusText.textContent = "Face not recognized";
statusText.style.color = "#A5A5A5";
if (typeof showSnackbar === 'function' && Date.now() - lastFaceAlertAt > FACE_ALERT_COOLDOWN_MS) {
lastFaceAlertAt = Date.now();
showSnackbar('Face not recognized β ensure your face is clearly visible.', 'info');
}
} else if (result.status === 'no_face') {
statusText.textContent = "Position your face in the frame";
statusText.style.color = "#A5A5A5";
if (typeof showSnackbar === 'function' && Date.now() - lastFaceAlertAt > FACE_ALERT_COOLDOWN_MS) {
lastFaceAlertAt = Date.now();
showSnackbar('Adjust position β keep your face clearly visible in the frame.', 'info');
}
}
}
// βββ Spoof toast (banner, auto-dismiss) βββββββββββββββββββββββββββββββββββββ
function showSpoofToast(data) {
const msg = data.reason || data.message || "Use a live face, not a photo or screen.";
spoofToastMessage.textContent = msg;
spoofToast.classList.add('show');
statusText.textContent = "β Spoofing detected";
statusText.style.color = "#FF3B30";
if (toastDismissTimer) clearTimeout(toastDismissTimer);
toastDismissTimer = setTimeout(() => {
spoofToast.classList.remove('show');
statusText.textContent = "Position your face in the frame";
statusText.style.color = "#A5A5A5";
toastDismissTimer = null;
}, 4500);
}
// βββ Success modal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function showSuccess(data) {
isScanning = false;
statusDot.classList.add('active');
modalName.textContent = data.name;
modalTime.textContent = data.timestamp;
modal.classList.add('show');
// Auto dismiss after 4s
setTimeout(dismissModal, 4000);
}
function dismissModal() {
modal.classList.remove('show');
statusDot.classList.remove('active');
statusText.textContent = "Position your face in the frame";
statusText.style.color = "#A5A5A5";
isScanning = true;
startCaptureLoop();
}
// βββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
startCamera();
window.addEventListener('beforeunload', stopCamera);
|