AutoAttend-AI / app /static /js /camera.js
kkt-2002's picture
Fix attendance marking issues and enhance session management
6d1dc35
/*
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');
});