| 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'); |
|
|
| |
| const spoofToast = document.getElementById('spoofToast'); |
| const spoofToastMessage = document.getElementById('spoofToastMessage'); |
|
|
| |
| 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; |
|
|
| |
|
|
| 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); |
| } |
|
|
| |
|
|
| 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)); |
| } |
| } |
|
|
| |
|
|
| 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'); |
| } |
| } |
| } |
|
|
| |
|
|
| 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); |
| } |
|
|
| |
|
|
| function showSuccess(data) { |
| isScanning = false; |
| statusDot.classList.add('active'); |
| |
| modalName.textContent = data.name; |
| modalTime.textContent = data.timestamp; |
| |
| modal.classList.add('show'); |
| |
| |
| 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(); |
| } |
|
|
| |
| startCamera(); |
| window.addEventListener('beforeunload', stopCamera); |
|
|