Spaces:
Sleeping
Sleeping
| /* | |
| camera.js - Enhanced for Hugging Face Spaces deployment | |
| Features: | |
| - Robust session management and authentication handling | |
| - Enhanced error handling for network timeouts and server errors | |
| - Improved retry mechanisms for unreliable connections | |
| - Better resource cleanup and camera management | |
| - Live liveness detection preview for attendance | |
| - Fallback handling when models are unavailable | |
| - Progressive image quality for better performance on cloud platforms | |
| */ | |
| document.addEventListener('DOMContentLoaded', function () { | |
| // Configuration | |
| const CONFIG = { | |
| REQUEST_TIMEOUT: 45000, // 45 seconds for cloud deployment | |
| RETRY_ATTEMPTS: 3, | |
| RETRY_DELAY: 1000, | |
| PREVIEW_FPS: 1.5, // Reduced for cloud hosting stability | |
| MAX_CONSECUTIVE_ERRORS: 3, | |
| IMAGE_QUALITY: { | |
| preview: 0.4, | |
| capture: 0.8, | |
| registration: 0.9 | |
| } | |
| }; | |
| // Global state management | |
| let globalState = { | |
| sessionValid: true, | |
| modelsAvailable: true, | |
| networkOnline: navigator.onLine | |
| }; | |
| // Enhanced request function with retry logic and session handling | |
| async function makeRequest(url, options = {}, retries = CONFIG.RETRY_ATTEMPTS) { | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT); | |
| try { | |
| const response = await fetch(url, { | |
| ...options, | |
| signal: controller.signal, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Cache-Control': 'no-cache', | |
| ...options.headers | |
| } | |
| }); | |
| clearTimeout(timeoutId); | |
| if (!response.ok) { | |
| // Handle specific HTTP status codes | |
| if (response.status === 401 || response.status === 403) { | |
| globalState.sessionValid = false; | |
| throw new Error('Session expired. Please login again.'); | |
| } else if (response.status === 503) { | |
| throw new Error('Service temporarily unavailable. Please try again later.'); | |
| } else { | |
| throw new Error(`Server error (${response.status}). Please try again.`); | |
| } | |
| } | |
| const data = await response.json(); | |
| // Handle application-level redirects and session issues | |
| if (data.redirect || (data.message && data.message.includes('Not logged in'))) { | |
| globalState.sessionValid = false; | |
| throw new Error('Session expired. Redirecting to login...'); | |
| } | |
| // Check for model availability issues | |
| if (data.message && data.message.includes('model not available')) { | |
| globalState.modelsAvailable = false; | |
| } | |
| return data; | |
| } catch (error) { | |
| clearTimeout(timeoutId); | |
| // Handle session expiration | |
| if (!globalState.sessionValid || error.message.includes('Session expired')) { | |
| setTimeout(() => { | |
| window.location.href = '/login.html'; | |
| }, 2000); | |
| throw error; | |
| } | |
| // Handle network errors with retry logic | |
| if (retries > 0 && ( | |
| error.name === 'AbortError' || | |
| error.name === 'TypeError' || | |
| error.message.includes('fetch') | |
| )) { | |
| console.warn(`Request failed, retrying... (${CONFIG.RETRY_ATTEMPTS - retries + 1}/${CONFIG.RETRY_ATTEMPTS})`); | |
| await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY)); | |
| return makeRequest(url, options, retries - 1); | |
| } | |
| // Enhanced error messages for user | |
| if (error.name === 'AbortError') { | |
| throw new Error('Request timed out. Please check your internet connection and try again.'); | |
| } else if (error.name === 'TypeError') { | |
| throw new Error('Network error. Please check your internet connection.'); | |
| } | |
| throw error; | |
| } | |
| } | |
| // Enhanced loading indicator with progress | |
| function showLoading(element, message = 'Processing...', showSpinner = true) { | |
| if (!element) return; | |
| const spinner = showSpinner ? ` | |
| <div class="spinner-border spinner-border-sm me-2" role="status"> | |
| <span class="visually-hidden">Loading...</span> | |
| </div> | |
| ` : ''; | |
| element.innerHTML = ` | |
| <div class="d-flex align-items-center justify-content-center"> | |
| ${spinner} | |
| <span class="loading-text">${message}</span> | |
| </div> | |
| `; | |
| element.disabled = true; | |
| } | |
| function hideLoading(element, message = '', isError = false) { | |
| if (!element) return; | |
| element.innerHTML = message; | |
| element.disabled = false; | |
| if (isError) { | |
| element.classList.add('btn-outline-danger'); | |
| setTimeout(() => { | |
| element.classList.remove('btn-outline-danger'); | |
| }, 3000); | |
| } | |
| } | |
| // Enhanced camera access with progressive fallback | |
| async function getCameraStream(constraints = {}) { | |
| const configurations = [ | |
| // High quality for registration/login | |
| { | |
| video: { | |
| width: { ideal: 640, max: 1280 }, | |
| height: { ideal: 480, max: 720 }, | |
| facingMode: 'user', | |
| frameRate: { ideal: 30, max: 60 } | |
| } | |
| }, | |
| // Medium quality fallback | |
| { | |
| video: { | |
| width: { ideal: 480, max: 640 }, | |
| height: { ideal: 360, max: 480 }, | |
| facingMode: 'user', | |
| frameRate: { ideal: 15, max: 30 } | |
| } | |
| }, | |
| // Basic fallback | |
| { | |
| video: { facingMode: 'user' } | |
| }, | |
| // Last resort - any camera | |
| { | |
| video: true | |
| } | |
| ]; | |
| // Merge user constraints with defaults | |
| if (Object.keys(constraints).length > 0) { | |
| configurations.unshift(constraints); | |
| } | |
| for (let i = 0; i < configurations.length; i++) { | |
| try { | |
| console.log(`Trying camera configuration ${i + 1}/${configurations.length}`); | |
| const stream = await navigator.mediaDevices.getUserMedia(configurations[i]); | |
| console.log('Camera access successful with configuration:', configurations[i]); | |
| return stream; | |
| } catch (error) { | |
| console.warn(`Camera configuration ${i + 1} failed:`, error); | |
| if (i === configurations.length - 1) { | |
| throw new Error('Unable to access camera. Please ensure camera permissions are granted and no other application is using the camera.'); | |
| } | |
| } | |
| } | |
| } | |
| // Check system capabilities | |
| async function checkSystemCapabilities() { | |
| try { | |
| // Check camera availability | |
| const devices = await navigator.mediaDevices.enumerateDevices(); | |
| const hasCamera = devices.some(device => device.kind === 'videoinput'); | |
| if (!hasCamera) { | |
| throw new Error('No camera detected on this device.'); | |
| } | |
| // Check server health | |
| const healthData = await makeRequest('/health-check'); | |
| globalState.modelsAvailable = healthData.models && | |
| (healthData.models.yolo_loaded || healthData.models.antispoof_loaded); | |
| return { | |
| camera: hasCamera, | |
| models: globalState.modelsAvailable, | |
| database: healthData.database_connected | |
| }; | |
| } catch (error) { | |
| console.warn('System capability check failed:', error); | |
| return { camera: true, models: false, database: false }; // Optimistic defaults | |
| } | |
| } | |
| // Reusable camera section for Registration/Login with enhanced error handling | |
| function setupCameraSection(config) { | |
| const elements = { | |
| video: document.getElementById(config.videoId), | |
| canvas: document.getElementById(config.canvasId), | |
| startBtn: document.getElementById(config.startCameraBtnId), | |
| captureBtn: document.getElementById(config.captureImageBtnId), | |
| retakeBtn: document.getElementById(config.retakeImageBtnId), | |
| overlay: document.getElementById(config.cameraOverlayId), | |
| imageInput: document.getElementById(config.faceImageInputId), | |
| actionBtn: document.getElementById(config.actionBtnId) | |
| }; | |
| // Validate required elements | |
| const requiredElements = ['video', 'canvas', 'startBtn', 'captureBtn', 'retakeBtn', 'imageInput']; | |
| const missingElements = requiredElements.filter(key => !elements[key]); | |
| if (missingElements.length > 0) { | |
| console.warn(`Skipping camera section - missing elements: ${missingElements.join(', ')}`); | |
| return; | |
| } | |
| let stream = null; | |
| let isCapturing = false; | |
| // Enhanced start camera with capability check | |
| elements.startBtn.addEventListener('click', async function () { | |
| if (isCapturing) return; | |
| try { | |
| isCapturing = true; | |
| showLoading(elements.startBtn, 'Checking camera...'); | |
| // Quick capability check | |
| const capabilities = await checkSystemCapabilities(); | |
| if (!capabilities.camera) { | |
| throw new Error('No camera available on this device.'); | |
| } | |
| showLoading(elements.startBtn, 'Starting camera...'); | |
| stream = await getCameraStream({ | |
| video: { | |
| width: { ideal: 400, max: 640 }, | |
| height: { ideal: 300, max: 480 }, | |
| facingMode: 'user' | |
| } | |
| }); | |
| elements.video.srcObject = stream; | |
| await elements.video.play(); | |
| // Update UI | |
| elements.startBtn.classList.add('d-none'); | |
| elements.captureBtn.classList.remove('d-none'); | |
| elements.retakeBtn.classList.add('d-none'); | |
| if (elements.overlay) elements.overlay.classList.add('d-none'); | |
| elements.video.classList.remove('d-none'); | |
| elements.canvas.classList.add('d-none'); | |
| if (elements.actionBtn) elements.actionBtn.disabled = true; | |
| } catch (err) { | |
| console.error('Error starting camera:', err); | |
| alert(err.message || 'Could not access the camera. Please check permissions.'); | |
| hideLoading(elements.startBtn, 'Start Camera', true); | |
| } finally { | |
| isCapturing = false; | |
| if (!stream) { | |
| hideLoading(elements.startBtn, 'Start Camera'); | |
| } else { | |
| hideLoading(elements.startBtn, 'Camera Active'); | |
| } | |
| } | |
| }); | |
| // Enhanced capture with quality options | |
| elements.captureBtn.addEventListener('click', function () { | |
| try { | |
| if (!stream || elements.video.readyState < 2) { | |
| alert('Camera not ready. Please wait a moment and try again.'); | |
| return; | |
| } | |
| const context = elements.canvas.getContext('2d'); | |
| elements.canvas.width = elements.video.videoWidth || 400; | |
| elements.canvas.height = elements.video.videoHeight || 300; | |
| // Draw with better quality | |
| context.drawImage(elements.video, 0, 0, elements.canvas.width, elements.canvas.height); | |
| // Use appropriate quality based on use case | |
| const quality = config.isRegistration ? CONFIG.IMAGE_QUALITY.registration : CONFIG.IMAGE_QUALITY.capture; | |
| const imageDataURL = elements.canvas.toDataURL('image/jpeg', quality); | |
| elements.imageInput.value = imageDataURL; | |
| // Update UI | |
| if (elements.overlay) elements.overlay.classList.remove('d-none'); | |
| elements.captureBtn.classList.add('d-none'); | |
| elements.retakeBtn.classList.remove('d-none'); | |
| elements.video.classList.add('d-none'); | |
| elements.canvas.classList.remove('d-none'); | |
| if (elements.actionBtn) elements.actionBtn.disabled = false; | |
| // Stop camera stream | |
| if (stream) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| stream = null; | |
| } | |
| } catch (err) { | |
| console.error('Error capturing image:', err); | |
| alert('Failed to capture image. Please try again.'); | |
| } | |
| }); | |
| // Enhanced retake with cleanup | |
| elements.retakeBtn.addEventListener('click', async function () { | |
| try { | |
| showLoading(elements.retakeBtn, 'Restarting...'); | |
| // Clear canvas and input | |
| const context = elements.canvas.getContext('2d'); | |
| context.clearRect(0, 0, elements.canvas.width, elements.canvas.height); | |
| elements.imageInput.value = ''; | |
| // Restart camera | |
| stream = await getCameraStream({ | |
| video: { | |
| width: { ideal: 400, max: 640 }, | |
| height: { ideal: 300, max: 480 }, | |
| facingMode: 'user' | |
| } | |
| }); | |
| elements.video.srcObject = stream; | |
| await elements.video.play(); | |
| // Update UI | |
| if (elements.overlay) elements.overlay.classList.add('d-none'); | |
| elements.captureBtn.classList.remove('d-none'); | |
| elements.retakeBtn.classList.add('d-none'); | |
| elements.video.classList.remove('d-none'); | |
| elements.canvas.classList.add('d-none'); | |
| if (elements.actionBtn) elements.actionBtn.disabled = true; | |
| } catch (err) { | |
| console.error('Error restarting camera:', err); | |
| alert(err.message || 'Error restarting camera.'); | |
| } finally { | |
| hideLoading(elements.retakeBtn, 'Retake'); | |
| } | |
| }); | |
| // Cleanup on page unload | |
| window.addEventListener('beforeunload', () => { | |
| if (stream) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| } | |
| }); | |
| } | |
| // Enhanced attendance section with robust liveness preview | |
| function setupAttendanceSection(config) { | |
| const elements = { | |
| video: document.getElementById(config.videoId), | |
| canvas: document.getElementById(config.canvasId), | |
| startBtn: document.getElementById(config.startCameraBtnId), | |
| captureBtn: document.getElementById(config.captureImageBtnId), | |
| retakeBtn: document.getElementById(config.retakeImageBtnId), | |
| markBtn: document.getElementById(config.markBtnId), | |
| overlayImg: document.getElementById(config.overlayImgId), | |
| statusEl: document.getElementById(config.statusId), | |
| // Form fields | |
| programEl: document.getElementById(config.programId), | |
| semesterEl: document.getElementById(config.semesterId), | |
| courseEl: document.getElementById(config.courseId), | |
| studentIdEl: document.getElementById(config.studentIdInputId) | |
| }; | |
| // Validate core elements | |
| if (!elements.video || !elements.canvas || !elements.startBtn || !elements.captureBtn || | |
| !elements.retakeBtn || !elements.markBtn) { | |
| console.warn('Skipping attendance section - missing core elements'); | |
| return; | |
| } | |
| let stream = null; | |
| let capturedDataUrl = ''; | |
| let previewState = { | |
| active: false, | |
| busy: false, | |
| timer: null, | |
| consecutiveErrors: 0, | |
| lastSuccessTime: 0 | |
| }; | |
| // Preview canvas for efficiency | |
| const previewCanvas = document.createElement('canvas'); | |
| const previewCtx = previewCanvas.getContext('2d'); | |
| function setStatus(msg, type = 'info') { | |
| if (!elements.statusEl) return; | |
| elements.statusEl.textContent = msg || ''; | |
| elements.statusEl.classList.remove('text-success', 'text-danger', 'text-warning', 'text-info'); | |
| switch (type) { | |
| case 'error': | |
| elements.statusEl.classList.add('text-danger'); | |
| break; | |
| case 'warning': | |
| elements.statusEl.classList.add('text-warning'); | |
| break; | |
| case 'success': | |
| elements.statusEl.classList.add('text-success'); | |
| break; | |
| default: | |
| elements.statusEl.classList.add('text-info'); | |
| } | |
| } | |
| function clearOverlay() { | |
| if (elements.overlayImg) { | |
| elements.overlayImg.src = ''; | |
| elements.overlayImg.classList.add('d-none'); | |
| } | |
| } | |
| async function startCamera() { | |
| try { | |
| showLoading(elements.startBtn, 'Initializing camera...'); | |
| setStatus('Starting camera system...'); | |
| // Check system capabilities first | |
| const capabilities = await checkSystemCapabilities(); | |
| if (!capabilities.camera) { | |
| throw new Error('No camera detected. Please ensure a camera is connected.'); | |
| } | |
| if (!capabilities.models) { | |
| setStatus('Warning: Face recognition models may not be fully available', 'warning'); | |
| } | |
| showLoading(elements.startBtn, 'Accessing camera...'); | |
| stream = await getCameraStream({ | |
| video: { | |
| width: { ideal: 640, max: 1280 }, | |
| height: { ideal: 480, max: 720 }, | |
| facingMode: 'user' | |
| } | |
| }); | |
| elements.video.srcObject = stream; | |
| await elements.video.play(); | |
| // Configure preview canvas | |
| previewCanvas.width = 480; // Reduced for better network performance | |
| previewCanvas.height = Math.round(previewCanvas.width * | |
| (elements.video.videoHeight || 480) / (elements.video.videoWidth || 640)); | |
| // Update UI | |
| elements.startBtn.classList.add('d-none'); | |
| elements.captureBtn.classList.remove('d-none'); | |
| elements.retakeBtn.classList.add('d-none'); | |
| elements.markBtn.disabled = true; | |
| elements.video.classList.remove('d-none'); | |
| elements.canvas.classList.add('d-none'); | |
| clearOverlay(); | |
| setStatus('Camera active. Starting liveness detection...'); | |
| // Wait for video stabilization before starting preview | |
| setTimeout(() => { | |
| if (stream && elements.video.readyState >= 2) { | |
| startLivenessPreview(); | |
| } | |
| }, 1500); | |
| } catch (err) { | |
| console.error('Error starting camera:', err); | |
| setStatus(`Camera error: ${err.message}`, 'error'); | |
| alert(err.message || 'Could not access camera. Please check permissions.'); | |
| } finally { | |
| hideLoading(elements.startBtn, 'Start Camera'); | |
| } | |
| } | |
| function startLivenessPreview() { | |
| if (previewState.active) return; | |
| previewState.active = true; | |
| previewState.consecutiveErrors = 0; | |
| previewState.lastSuccessTime = Date.now(); | |
| // Reduced FPS for cloud hosting stability | |
| const intervalMs = Math.round(1000 / CONFIG.PREVIEW_FPS); | |
| previewState.timer = setInterval(async () => { | |
| if (!previewState.active || previewState.busy || !stream || | |
| elements.video.readyState < 2) { | |
| return; | |
| } | |
| previewState.busy = true; | |
| try { | |
| // Capture frame at lower quality for preview | |
| previewCtx.drawImage(elements.video, 0, 0, previewCanvas.width, previewCanvas.height); | |
| const frameDataUrl = previewCanvas.toDataURL('image/jpeg', CONFIG.IMAGE_QUALITY.preview); | |
| const data = await makeRequest('/liveness-preview', { | |
| method: 'POST', | |
| body: JSON.stringify({ face_image: frameDataUrl }) | |
| }); | |
| // Reset error counter on success | |
| previewState.consecutiveErrors = 0; | |
| previewState.lastSuccessTime = Date.now(); | |
| // Update overlay | |
| if (elements.overlayImg && data.overlay) { | |
| elements.overlayImg.src = data.overlay; | |
| elements.overlayImg.classList.remove('d-none'); | |
| } | |
| // Enhanced status with confidence indicators | |
| if (typeof data.live === 'boolean' && typeof data.live_prob === 'number') { | |
| const confidence = data.live_prob; | |
| let confidenceText = confidence >= 0.9 ? 'Excellent' : | |
| confidence >= 0.8 ? 'Good' : | |
| confidence >= 0.7 ? 'Fair' : 'Poor'; | |
| const statusType = data.live ? (confidence >= 0.8 ? 'success' : 'warning') : 'error'; | |
| setStatus( | |
| `${data.live ? 'LIVE' : 'SPOOF'} detected - Confidence: ${confidenceText} (${confidence.toFixed(2)})`, | |
| statusType | |
| ); | |
| } else if (data.message) { | |
| setStatus(data.message, data.success ? 'info' : 'warning'); | |
| } | |
| } catch (error) { | |
| previewState.consecutiveErrors++; | |
| const timeSinceLastSuccess = Date.now() - previewState.lastSuccessTime; | |
| console.warn(`Preview error ${previewState.consecutiveErrors}/${CONFIG.MAX_CONSECUTIVE_ERRORS}:`, error); | |
| if (previewState.consecutiveErrors >= CONFIG.MAX_CONSECUTIVE_ERRORS || | |
| timeSinceLastSuccess > 30000) { | |
| setStatus('Liveness preview temporarily unavailable. You can still capture and mark attendance.', 'warning'); | |
| stopLivenessPreview(); | |
| } else if (previewState.consecutiveErrors === 1) { | |
| setStatus('Connecting to liveness service...', 'warning'); | |
| } | |
| } finally { | |
| previewState.busy = false; | |
| } | |
| }, intervalMs); | |
| } | |
| function stopLivenessPreview() { | |
| previewState.active = false; | |
| if (previewState.timer) { | |
| clearInterval(previewState.timer); | |
| previewState.timer = null; | |
| } | |
| previewState.busy = false; | |
| previewState.consecutiveErrors = 0; | |
| } | |
| function captureAttendanceFrame() { | |
| try { | |
| if (!stream || elements.video.readyState < 2) { | |
| setStatus('Camera not ready. Please wait and try again.', 'error'); | |
| return; | |
| } | |
| // Stop preview during capture | |
| stopLivenessPreview(); | |
| const ctx = elements.canvas.getContext('2d'); | |
| elements.canvas.width = elements.video.videoWidth || 640; | |
| elements.canvas.height = elements.video.videoHeight || 480; | |
| ctx.drawImage(elements.video, 0, 0, elements.canvas.width, elements.canvas.height); | |
| capturedDataUrl = elements.canvas.toDataURL('image/jpeg', CONFIG.IMAGE_QUALITY.capture); | |
| // Update UI | |
| elements.captureBtn.classList.add('d-none'); | |
| elements.retakeBtn.classList.remove('d-none'); | |
| elements.markBtn.disabled = false; | |
| elements.video.classList.add('d-none'); | |
| elements.canvas.classList.remove('d-none'); | |
| setStatus('Image captured successfully. Ready to mark attendance.', 'success'); | |
| // Stop camera | |
| if (stream) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| stream = null; | |
| } | |
| } catch (err) { | |
| console.error('Error capturing frame:', err); | |
| setStatus('Failed to capture image. Please try again.', 'error'); | |
| } | |
| } | |
| async function retakeAttendanceFrame() { | |
| try { | |
| showLoading(elements.retakeBtn, 'Restarting camera...'); | |
| // Clear previous capture | |
| const ctx = elements.canvas.getContext('2d'); | |
| ctx.clearRect(0, 0, elements.canvas.width, elements.canvas.height); | |
| capturedDataUrl = ''; | |
| clearOverlay(); | |
| // Restart camera | |
| await startCamera(); | |
| } catch (err) { | |
| console.error('Error during retake:', err); | |
| setStatus('Error restarting camera. Please refresh the page.', 'error'); | |
| } finally { | |
| hideLoading(elements.retakeBtn, 'Retake'); | |
| } | |
| } | |
| async function markAttendance() { | |
| try { | |
| if (!capturedDataUrl) { | |
| setStatus('Please capture an image first.', 'error'); | |
| return; | |
| } | |
| // Validate form fields | |
| const payload = { | |
| student_id: (elements.studentIdEl && elements.studentIdEl.value) || null, | |
| program: (elements.programEl && elements.programEl.value) || '', | |
| semester: (elements.semesterEl && elements.semesterEl.value) || '', | |
| course: (elements.courseEl && elements.courseEl.value) || '', | |
| face_image: capturedDataUrl, | |
| }; | |
| if (!payload.program || !payload.semester || !payload.course) { | |
| setStatus('Please fill in Program, Semester, and Course fields.', 'error'); | |
| return; | |
| } | |
| showLoading(elements.markBtn, 'Processing attendance...'); | |
| setStatus('Analyzing face and marking attendance... This may take a moment.'); | |
| const data = await makeRequest('/mark-attendance', { | |
| method: 'POST', | |
| body: JSON.stringify(payload) | |
| }); | |
| // Show final overlay with detection results | |
| if (elements.overlayImg && data.overlay) { | |
| elements.overlayImg.src = data.overlay; | |
| elements.overlayImg.classList.remove('d-none'); | |
| } | |
| if (data.success) { | |
| setStatus('✅ ' + (data.message || 'Attendance marked successfully!'), 'success'); | |
| // Auto-refresh after successful attendance | |
| setTimeout(() => { | |
| setStatus('Refreshing page...', 'info'); | |
| window.location.reload(); | |
| }, 3000); | |
| } else { | |
| const errorMessage = data.message || 'Failed to mark attendance.'; | |
| setStatus('❌ ' + errorMessage, 'error'); | |
| // Provide helpful suggestions based on error type | |
| if (errorMessage.includes('already marked')) { | |
| setStatus('Attendance already marked for this course today.', 'warning'); | |
| } else if (errorMessage.includes('not recognized')) { | |
| setStatus('Face not recognized. Please ensure good lighting and try again.', 'warning'); | |
| } else if (errorMessage.includes('SPOOF')) { | |
| setStatus('Spoof detection activated. Please ensure you are physically present and try again.', 'warning'); | |
| } | |
| } | |
| } catch (err) { | |
| console.error('markAttendance error:', err); | |
| let errorMsg = 'An error occurred while marking attendance.'; | |
| if (err.message.includes('Session expired')) { | |
| errorMsg = 'Session expired. You will be redirected to login.'; | |
| } else if (err.message.includes('timed out')) { | |
| errorMsg = 'Request timed out. Please check your connection and try again.'; | |
| } else if (err.message.includes('model not available')) { | |
| errorMsg = 'Face recognition service is temporarily unavailable. Please try again later.'; | |
| } | |
| setStatus('❌ ' + errorMsg, 'error'); | |
| } finally { | |
| hideLoading(elements.markBtn, 'Mark Attendance'); | |
| } | |
| } | |
| // Event listeners | |
| elements.startBtn.addEventListener('click', startCamera); | |
| elements.captureBtn.addEventListener('click', captureAttendanceFrame); | |
| elements.retakeBtn.addEventListener('click', retakeAttendanceFrame); | |
| elements.markBtn.addEventListener('click', markAttendance); | |
| // Cleanup handlers | |
| window.addEventListener('beforeunload', () => { | |
| stopLivenessPreview(); | |
| if (stream) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| } | |
| }); | |
| // Handle page visibility changes | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden) { | |
| if (previewState.active) { | |
| stopLivenessPreview(); | |
| } | |
| } else if (!document.hidden && stream && elements.video.readyState >= 2 && !previewState.active) { | |
| setTimeout(() => { | |
| startLivenessPreview(); | |
| }, 1000); | |
| } | |
| }); | |
| } | |
| // Enhanced auto face login with progressive quality | |
| function setupAutoFaceLogin() { | |
| const autoLoginBtn = document.getElementById('autoFaceLoginBtn'); | |
| const roleSelect = document.getElementById('faceRole'); | |
| if (!autoLoginBtn) return; | |
| autoLoginBtn.addEventListener('click', async function() { | |
| try { | |
| autoLoginBtn.disabled = true; | |
| showLoading(autoLoginBtn, 'Initializing...'); | |
| const role = roleSelect ? roleSelect.value : 'student'; | |
| showLoading(autoLoginBtn, 'Accessing camera...'); | |
| const stream = await getCameraStream({ | |
| video: { | |
| width: { ideal: 640, max: 1280 }, | |
| height: { ideal: 480, max: 720 }, | |
| facingMode: 'user' | |
| } | |
| }); | |
| // Create temporary elements | |
| const tempVideo = document.createElement('video'); | |
| tempVideo.srcObject = stream; | |
| tempVideo.muted = true; | |
| showLoading(autoLoginBtn, 'Preparing camera...'); | |
| await tempVideo.play(); | |
| // Wait for stabilization | |
| await new Promise(resolve => setTimeout(resolve, 2000)); | |
| showLoading(autoLoginBtn, 'Capturing image...'); | |
| const tempCanvas = document.createElement('canvas'); | |
| const ctx = tempCanvas.getContext('2d'); | |
| tempCanvas.width = tempVideo.videoWidth || 640; | |
| tempCanvas.height = tempVideo.videoHeight || 480; | |
| ctx.drawImage(tempVideo, 0, 0, tempCanvas.width, tempCanvas.height); | |
| const imageDataURL = tempCanvas.toDataURL('image/jpeg', CONFIG.IMAGE_QUALITY.capture); | |
| // Cleanup camera | |
| stream.getTracks().forEach(track => track.stop()); | |
| showLoading(autoLoginBtn, 'Recognizing face...'); | |
| const data = await makeRequest('/auto-face-login', { | |
| method: 'POST', | |
| body: JSON.stringify({ | |
| face_image: imageDataURL, | |
| face_role: role | |
| }) | |
| }); | |
| if (data.success) { | |
| showLoading(autoLoginBtn, 'Login successful! Redirecting...', false); | |
| setTimeout(() => { | |
| window.location.href = data.redirect_url; | |
| }, 1500); | |
| } else { | |
| throw new Error(data.message || 'Face recognition failed. Please try again.'); | |
| } | |
| } catch (err) { | |
| console.error('Auto face login error:', err); | |
| alert(err.message || 'Auto face login failed. Please try manual login.'); | |
| hideLoading(autoLoginBtn, 'Auto Face Login', true); | |
| } finally { | |
| if (autoLoginBtn.innerHTML === autoLoginBtn.textContent) { | |
| autoLoginBtn.disabled = false; | |
| } | |
| } | |
| }); | |
| } | |
| // Initialize all camera sections | |
| console.log('Initializing camera systems...'); | |
| // Student Registration (enhanced quality) | |
| setupCameraSection({ | |
| videoId: 'videoStudent', | |
| canvasId: 'canvasStudent', | |
| startCameraBtnId: 'startCameraStudent', | |
| captureImageBtnId: 'captureImageStudent', | |
| retakeImageBtnId: 'retakeImageStudent', | |
| cameraOverlayId: 'cameraOverlayStudent', | |
| faceImageInputId: 'face_image_student', | |
| actionBtnId: 'registerBtnStudent', | |
| isRegistration: true | |
| }); | |
| // Teacher Registration (enhanced quality) | |
| setupCameraSection({ | |
| videoId: 'videoTeacher', | |
| canvasId: 'canvasTeacher', | |
| startCameraBtnId: 'startCameraTeacher', | |
| captureImageBtnId: 'captureImageTeacher', | |
| retakeImageBtnId: 'retakeImageTeacher', | |
| cameraOverlayId: 'cameraOverlayTeacher', | |
| faceImageInputId: 'face_image_teacher', | |
| actionBtnId: 'registerBtnTeacher', | |
| isRegistration: true | |
| }); | |
| // Face Login | |
| setupCameraSection({ | |
| videoId: 'video', | |
| canvasId: 'canvas', | |
| startCameraBtnId: 'startCamera', | |
| captureImageBtnId: 'captureImage', | |
| retakeImageBtnId: 'retakeImage', | |
| cameraOverlayId: 'cameraOverlay', | |
| faceImageInputId: 'face_image', | |
| actionBtnId: 'faceLoginBtn' | |
| }); | |
| // Attendance (with liveness preview) | |
| setupAttendanceSection({ | |
| videoId: 'videoAttendance', | |
| canvasId: 'canvasAttendance', | |
| startCameraBtnId: 'startCameraAttendance', | |
| captureImageBtnId: 'captureImageAttendance', | |
| retakeImageBtnId: 'retakeImageAttendance', | |
| markBtnId: 'markAttendanceBtn', | |
| overlayImgId: 'attendanceOverlayImg', | |
| statusId: 'attendanceStatus', | |
| programId: 'program', | |
| semesterId: 'semester', | |
| courseId: 'course', | |
| studentIdInputId: 'student_id' | |
| }); | |
| // Auto face login | |
| setupAutoFaceLogin(); | |
| // Network status monitoring | |
| window.addEventListener('online', () => { | |
| globalState.networkOnline = true; | |
| console.log('Network connection restored'); | |
| const statusElements = document.querySelectorAll('[id*="Status"]'); | |
| statusElements.forEach(el => { | |
| if (el.textContent.includes('connection')) { | |
| el.textContent = 'Connection restored'; | |
| el.className = 'text-success'; | |
| } | |
| }); | |
| }); | |
| window.addEventListener('offline', () => { | |
| globalState.networkOnline = false; | |
| console.log('Network connection lost'); | |
| const statusElements = document.querySelectorAll('[id*="Status"]'); | |
| statusElements.forEach(el => { | |
| el.textContent = 'No internet connection - Please check your network'; | |
| el.className = 'text-danger'; | |
| }); | |
| }); | |
| // System capability check on load | |
| checkSystemCapabilities().then(capabilities => { | |
| console.log('System capabilities:', capabilities); | |
| if (!capabilities.camera) { | |
| console.warn('No camera detected'); | |
| } | |
| if (!capabilities.models) { | |
| console.warn('Face recognition models may not be fully available'); | |
| } | |
| }).catch(err => { | |
| console.warn('Failed to check system capabilities:', err); | |
| }); | |
| console.log('Camera system initialization complete'); | |
| }); | |