Rekam wajah setelah register: screen/modal, kirim frame ke enroll-live, integrasi flow
This commit is contained in:
182
app.js
182
app.js
@@ -88,6 +88,20 @@
|
||||
var pendingQrToken = null;
|
||||
var html5QrCodeInstance = null;
|
||||
|
||||
// Enroll wajah saat daftar
|
||||
var enrollFaceModal = document.getElementById('enroll-face-modal');
|
||||
var enrollFaceVideo = document.getElementById('enroll-face-video');
|
||||
var enrollFaceStatus = document.getElementById('enroll-face-status');
|
||||
var enrollFaceError = document.getElementById('enroll-face-error');
|
||||
var enrollFaceStart = document.getElementById('enroll-face-start');
|
||||
var enrollFaceBackdrop = document.getElementById('enroll-face-backdrop');
|
||||
var enrollStream = null;
|
||||
var enrollIntervalId = null;
|
||||
var enrollCollectedImages = [];
|
||||
var enrollTargetCount = 5;
|
||||
var enrollMinCount = 3;
|
||||
var enrollSubmitting = false;
|
||||
|
||||
// Device credentials — bisa diisi di Pengaturan, default untuk development
|
||||
var DEFAULT_DEVICE_CODE = 'MOBILE_APP';
|
||||
var DEFAULT_API_KEY = 'MOBILE_APP_SECRET';
|
||||
@@ -535,8 +549,8 @@
|
||||
saveStudentProfile(profile);
|
||||
renderStudentSummary(homeStudentSummary, profile);
|
||||
|
||||
goToScreen('home');
|
||||
showToast('Registrasi berhasil. Siap presensi.', 'success');
|
||||
openEnrollFaceModal();
|
||||
showToast('Registrasi berhasil. Rekam wajah untuk verifikasi absen.', 'success');
|
||||
}).catch(function (err) {
|
||||
btnCompleteRegister.disabled = false;
|
||||
btnCompleteRegister.textContent = 'Simpan & Mulai';
|
||||
@@ -877,6 +891,162 @@
|
||||
if (cameraModal) cameraModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function captureEnrollFrameAsBase64() {
|
||||
if (!enrollFaceVideo || enrollFaceVideo.readyState < 2) return null;
|
||||
var w = enrollFaceVideo.videoWidth || 640;
|
||||
var h = enrollFaceVideo.videoHeight || 480;
|
||||
if (w === 0 || h === 0) return null;
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(enrollFaceVideo, 0, 0, w, h);
|
||||
return canvas.toDataURL('image/jpeg', 0.9);
|
||||
}
|
||||
|
||||
function closeEnrollFaceModal() {
|
||||
if (enrollIntervalId) {
|
||||
clearInterval(enrollIntervalId);
|
||||
enrollIntervalId = null;
|
||||
}
|
||||
enrollCollectedImages = [];
|
||||
if (enrollStream) {
|
||||
enrollStream.getTracks().forEach(function (t) { t.stop(); });
|
||||
enrollStream = null;
|
||||
}
|
||||
if (enrollFaceVideo) enrollFaceVideo.srcObject = null;
|
||||
if (enrollFaceModal) enrollFaceModal.classList.add('hidden');
|
||||
if (enrollFaceStatus) {
|
||||
enrollFaceStatus.classList.add('hidden');
|
||||
enrollFaceStatus.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
function openEnrollFaceModal() {
|
||||
if (!ensureConfig()) {
|
||||
goToScreen('home');
|
||||
return;
|
||||
}
|
||||
if (!enrollFaceModal || !enrollFaceVideo || !currentStudentProfile || !currentStudentProfile.student_id) {
|
||||
goToScreen('home');
|
||||
return;
|
||||
}
|
||||
enrollCollectedImages = [];
|
||||
enrollSubmitting = false;
|
||||
if (enrollFaceError) enrollFaceError.classList.add('hidden');
|
||||
if (enrollFaceStatus) {
|
||||
enrollFaceStatus.textContent = 'Menyiapkan kamera…';
|
||||
enrollFaceStatus.classList.remove('hidden');
|
||||
}
|
||||
enrollFaceModal.classList.remove('hidden');
|
||||
enrollFaceVideo.style.display = 'block';
|
||||
var constraints = { video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } }, audio: false };
|
||||
navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
|
||||
enrollStream = stream;
|
||||
enrollFaceVideo.srcObject = stream;
|
||||
enrollFaceVideo.onloadedmetadata = function () {
|
||||
enrollFaceVideo.play().then(function () {
|
||||
if (enrollFaceStatus) enrollFaceStatus.textContent = 'Arahkan wajah ke kamera lalu tekan Rekam.';
|
||||
}).catch(function () {});
|
||||
};
|
||||
}).catch(function () {
|
||||
if (enrollFaceError) {
|
||||
enrollFaceError.textContent = 'Kamera tidak tersedia atau izin ditolak. Bisa dilewati.';
|
||||
enrollFaceError.classList.remove('hidden');
|
||||
}
|
||||
if (enrollFaceStatus) enrollFaceStatus.textContent = 'Kamera gagal. Tekan Lewati untuk lanjut.';
|
||||
});
|
||||
}
|
||||
|
||||
function startEnrollCapture() {
|
||||
if (enrollSubmitting || !enrollStream || !enrollFaceVideo || typeof faceapi === 'undefined') return;
|
||||
if (!currentStudentProfile || !currentStudentProfile.student_id) return;
|
||||
enrollSubmitting = true;
|
||||
enrollCollectedImages = [];
|
||||
if (enrollFaceStart) { enrollFaceStart.disabled = true; enrollFaceStart.textContent = 'Merekam…'; }
|
||||
var weightsUrl = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/weights/';
|
||||
faceapi.nets.tinyFaceDetector.loadFromUri(weightsUrl).then(function () {
|
||||
var tick = 0;
|
||||
var lastFaceCount = 0;
|
||||
enrollIntervalId = setInterval(function () {
|
||||
if (!enrollStream || !enrollFaceVideo.srcObject || enrollFaceVideo.readyState < 2) return;
|
||||
var detect = faceapi.detectSingleFace(enrollFaceVideo, new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.5 }));
|
||||
detect.then(function (detection) {
|
||||
if (detection && enrollCollectedImages.length < enrollTargetCount) {
|
||||
var b64 = captureEnrollFrameAsBase64();
|
||||
if (b64) {
|
||||
enrollCollectedImages.push(b64);
|
||||
if (enrollFaceStatus) enrollFaceStatus.textContent = 'Rekam ' + enrollCollectedImages.length + '/' + enrollTargetCount + '…';
|
||||
}
|
||||
}
|
||||
if (enrollCollectedImages.length >= enrollMinCount) {
|
||||
clearInterval(enrollIntervalId);
|
||||
enrollIntervalId = null;
|
||||
var imgs = enrollCollectedImages.slice(0, enrollTargetCount);
|
||||
var url = backendBase() + '/api/face/enroll-live';
|
||||
if (enrollFaceStatus) enrollFaceStatus.textContent = 'Mengirim ke server…';
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ student_id: currentStudentProfile.student_id, images: imgs })
|
||||
}).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); })
|
||||
.then(function (r) {
|
||||
enrollSubmitting = false;
|
||||
if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; }
|
||||
if (r.ok && r.data && (r.data.saved || 0) > 0) {
|
||||
showToast('Wajah berhasil direkam. Siap untuk verifikasi absen.', 'success');
|
||||
} else {
|
||||
showToast('Rekam wajah gagal. Bisa dicoba nanti dari pengaturan.', 'info');
|
||||
}
|
||||
closeEnrollFaceModal();
|
||||
goToScreen('home');
|
||||
}).catch(function () {
|
||||
enrollSubmitting = false;
|
||||
if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; }
|
||||
if (enrollFaceStatus) enrollFaceStatus.textContent = 'Gagal mengirim. Coba lagi atau Lewati.';
|
||||
showToast('Gagal mengirim rekaman wajah.', 'error');
|
||||
});
|
||||
}
|
||||
}).catch(function () {});
|
||||
tick++;
|
||||
if (tick > 80) {
|
||||
clearInterval(enrollIntervalId);
|
||||
enrollIntervalId = null;
|
||||
enrollSubmitting = false;
|
||||
if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; }
|
||||
if (enrollCollectedImages.length >= 1) {
|
||||
var url = backendBase() + '/api/face/enroll-live';
|
||||
if (enrollFaceStatus) enrollFaceStatus.textContent = 'Mengirim ' + enrollCollectedImages.length + ' foto…';
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ student_id: currentStudentProfile.student_id, images: enrollCollectedImages })
|
||||
}).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); })
|
||||
.then(function (r) {
|
||||
if (r.ok && (r.data && r.data.saved > 0)) showToast('Wajah berhasil direkam.', 'success');
|
||||
else showToast('Rekam wajah gagal. Bisa dicoba nanti.', 'info');
|
||||
closeEnrollFaceModal();
|
||||
goToScreen('home');
|
||||
}).catch(function () {
|
||||
enrollSubmitting = false;
|
||||
if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; }
|
||||
if (enrollFaceStatus) enrollFaceStatus.textContent = 'Gagal mengirim. Coba lagi atau Lewati.';
|
||||
showToast('Gagal mengirim rekaman wajah.', 'error');
|
||||
});
|
||||
} else {
|
||||
enrollSubmitting = false;
|
||||
if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; }
|
||||
if (enrollFaceStatus) enrollFaceStatus.textContent = 'Wajah tidak terdeteksi. Arahkan wajah ke kamera lalu Rekam lagi.';
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}).catch(function () {
|
||||
enrollSubmitting = false;
|
||||
if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; }
|
||||
if (enrollFaceStatus) enrollFaceStatus.textContent = 'Gagal memuat deteksi wajah. Lewati atau coba lagi.';
|
||||
});
|
||||
}
|
||||
|
||||
function stopCamera() {
|
||||
if (cameraStream) {
|
||||
cameraStream.getTracks().forEach(function (t) { t.stop(); });
|
||||
@@ -1235,6 +1405,14 @@
|
||||
if (cameraBackdrop) {
|
||||
cameraBackdrop.addEventListener('click', closeCameraModal);
|
||||
}
|
||||
if (enrollFaceBackdrop) {
|
||||
enrollFaceBackdrop.addEventListener('click', function () {
|
||||
showToast('Rekam wajah wajib untuk melanjutkan.', 'info');
|
||||
});
|
||||
}
|
||||
if (enrollFaceStart) {
|
||||
enrollFaceStart.addEventListener('click', startEnrollCapture);
|
||||
}
|
||||
if (resultCloseBtn) {
|
||||
resultCloseBtn.addEventListener('click', function () {
|
||||
resultBox.classList.add('hidden');
|
||||
|
||||
21
index.html
21
index.html
@@ -315,6 +315,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Rekam Wajah (saat daftar pertama) -->
|
||||
<div id="enroll-face-modal" class="modal modal-overlay hidden">
|
||||
<div class="modal-backdrop" id="enroll-face-backdrop"></div>
|
||||
<div class="modal-panel modal-panel-camera">
|
||||
<div class="modal-panel-header">
|
||||
<h2 class="modal-panel-title">Rekam Wajah (Wajib)</h2>
|
||||
</div>
|
||||
<div class="modal-panel-body">
|
||||
<p class="modal-panel-hint">Arahkan wajah ke kamera lalu tekan <strong>Rekam</strong>. Data wajah dari HP dipakai untuk verifikasi saat absen masuk/pulang.</p>
|
||||
<p id="enroll-face-status" class="camera-face-status hidden"></p>
|
||||
<div class="camera-frame">
|
||||
<video id="enroll-face-video" class="camera-video" playsinline autoplay muted></video>
|
||||
<div id="enroll-face-error" class="camera-frame-error hidden">Kamera tidak tersedia atau izin ditolak.</div>
|
||||
</div>
|
||||
<div class="modal-panel-actions">
|
||||
<button type="button" id="enroll-face-start" class="btn-primary btn-full">Rekam (3–5 foto)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user