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);