attendance-apps / static /js /attendance.js
devindia's picture
Upload 31 files
701628b verified
Raw
History Blame Contribute Delete
7.24 kB
const video = document.getElementById('videoFeed');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const modal = document.getElementById('successModal');
const modalTitle = document.getElementById('modalTitle');
const modalName = document.getElementById('modalName');
const modalTime = document.getElementById('modalTime');
const cameraContainer = document.getElementById('cameraContainer');
const statusHint = document.getElementById('statusHint');
// Spoof toast
const spoofToast = document.getElementById('spoofToast');
const spoofToastMessage = document.getElementById('spoofToastMessage');
// Frame buffer for sequence liveness (motion + blink); need enough frames to catch a blink
const FRAME_BUFFER_SIZE = 14;
const CAPTURE_INTERVAL_MS = 280;
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();
// Send sequence only when we have enough frames for blink detection (backend needs ~4+ frames)
const payload = frameBuffer.length >= 6
? { 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, CAPTURE_INTERVAL_MS));
}
}
// ─── Result handler ─────────────────────────────────────────────────────────
function handleResult(result) {
if (result.status === 'success') {
showSuccess(result);
var action = (result.punch_type === 'out') ? 'Punched out' : 'Punched in';
if (typeof showSnackbar === 'function') {
showSnackbar(action + ' at ' + (result.timestamp || ''), 'success');
}
} else if (result.status === 'cooldown') {
statusText.textContent = 'Please wait 1 min';
statusText.style.color = "#FFD700";
if (typeof showSnackbar === 'function') showSnackbar(result.message || 'Please wait 1 minute before punching again.', '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');
}
} else if (result.status === 'spoof' && result.reason && result.reason.toLowerCase().includes('blink')) {
statusText.textContent = "Please blink to verify";
statusText.style.color = "#FFD700";
}
}
// ─── 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');
if (statusHint) statusHint.style.visibility = 'hidden';
if (modalTitle) modalTitle.textContent = (data.punch_type === 'out') ? 'Punched out' : 'Punched in';
if (modalName) modalName.textContent = data.name;
if (modalTime) 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";
if (statusHint) statusHint.style.visibility = "";
isScanning = true;
startCaptureLoop();
}
// ─── Init ───────────────────────────────────────────────────────────────────
startCamera();
window.addEventListener('beforeunload', stopCamera);