import 'dart:io'; import 'dart:math' as Math; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import 'package:google_mlkit_face_mesh_detection/google_mlkit_face_mesh_detection.dart'; import 'package:sigap/src/features/auth/data/models/face_model.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart'; import 'package:uuid/uuid.dart'; enum LivenessStatus { preparing, detectingFace, checkLeftRotation, checkRightRotation, checkSmile, checkEyesOpen, readyForPhoto, photoTaken, completed, failed, } class FaceLivenessController extends GetxController with WidgetsBindingObserver { // Camera CameraController? _cameraController; late FaceDetector _faceDetector; var frontCamera; // Face Detection States final _isFaceInFrame = false.obs; final _isFaceLeft = false.obs; final _isFaceRight = false.obs; final _isEyeOpen = false.obs; final _isNoFace = false.obs; final _isMultiFace = false.obs; final _isCaptured = false.obs; final _isSmiled = false.obs; final _isFaceReadyForPhoto = false.obs; final _isDifferentPerson = false.obs; // Status tracking final Rx status = Rx( LivenessStatus.preparing, ); final RxString currentInstruction = RxString('Initializing camera...'); // Getters bool get isFaceInFrame => _isFaceInFrame.value; bool get isFaceLeft => _isFaceLeft.value; bool get isFaceRight => _isFaceRight.value; bool get isEyeOpen => _isEyeOpen.value; bool get isNoFace => _isNoFace.value; bool get isMultiFace => _isMultiFace.value; bool get isCaptured => _isCaptured.value; bool get isSmiled => _isSmiled.value; bool get isFaceReadyForPhoto => _isFaceReadyForPhoto.value; bool get isDifferentPerson => _isDifferentPerson.value; CameraController? get cameraController => _cameraController; // Face Mesh Detector final FaceMeshDetector _faceMeshDetector = FaceMeshDetector( option: FaceMeshDetectorOptions.faceMesh, ); // Face Comparison List? _firstPersonEmbedding; // Captured Image final _capturedImage = Rxn(); XFile? get capturedImage => _capturedImage.value; // Successful Steps final _successfulSteps = [].obs; List get successfulSteps => _successfulSteps; // Face Detector Options final FaceDetectorOptions options = FaceDetectorOptions( performanceMode: Platform.isAndroid ? FaceDetectorMode.fast : FaceDetectorMode.accurate, enableClassification: true, enableLandmarks: true, enableTracking: true, ); // Device Orientations final orientations = { DeviceOrientation.portraitUp: 0, DeviceOrientation.landscapeLeft: 90, DeviceOrientation.portraitDown: 180, DeviceOrientation.landscapeRight: 270, }; @override void onInit() { super.onInit(); WidgetsBinding.instance.addObserver(this); // Lock orientation to portrait mode _lockDeviceOrientation(); // Cek capabilities dulu _checkCameraCapabilities(); // Coba inisialisasi dengan fallback _initializeCameraWithFallback().catchError((e) { print("❌ All camera initialization attempts failed: $e"); status.value = LivenessStatus.failed; currentInstruction.value = 'Camera initialization failed. Please restart the app.'; }); _faceDetector = FaceDetector(options: options); } // 1. UBAH KONFIGURASI CAMERA CONTROLLER Future _initializeCamera() async { try { status.value = LivenessStatus.preparing; currentInstruction.value = 'Initializing camera...'; final cameras = await availableCameras(); final frontCameras = cameras.firstWhere( (camera) => camera.lensDirection == CameraLensDirection.front, ); frontCamera = frontCameras; // ✅ PERBAIKAN UTAMA: Gunakan format yang didukung ML Kit _cameraController = CameraController( frontCamera, ResolutionPreset.medium, // Naikkan dari low ke medium imageFormatGroup: Platform.isAndroid ? ImageFormatGroup .nv21 // ✅ PENTING: Gunakan NV21 untuk Android : ImageFormatGroup.bgra8888, // iOS tetap BGRA8888 enableAudio: false, ); await _cameraController!.initialize(); // Tambahkan delay untuk stabilisasi await Future.delayed(Duration(milliseconds: 1000)); await _cameraController!.setFlashMode(FlashMode.off); await _cameraController!.setFocusMode(FocusMode.auto); await _cameraController!.setExposureMode(ExposureMode.auto); print( "Camera initialized with resolution: ${_cameraController!.value.previewSize}", ); _cameraController!.startImageStream((CameraImage img) { _processCameraImage(img); }); status.value = LivenessStatus.detectingFace; currentInstruction.value = 'Position your face in the frame'; update(); } catch (e) { print('Error initializing camera: $e'); status.value = LivenessStatus.failed; currentInstruction.value = 'Failed to initialize camera: $e'; } } // Throttling flag to control processing rate bool _throttled = false; // Hapus batasan frame untuk deteksi wajah - selalu anggap wajah dalam frame void _handleFaceDetection(Face face) { if (!_isCaptured.value) { final double? rotY = face.headEulerAngleY; final double leftEyeOpen = face.leftEyeOpenProbability ?? -1.0; final double rightEyeOpen = face.rightEyeOpenProbability ?? -1.0; final double smileProb = face.smilingProbability ?? -1.0; print("HEAD DETECTION - Head angle: $rotY"); print( "HEAD DETECTION - Eyes: L=$leftEyeOpen, R=$rightEyeOpen, Smile=$smileProb", ); // Selalu anggap wajah sudah dalam frame jika wajah terdeteksi _updateFaceInFrameStatus(); // Print detection state print("Detection state - Face in frame: ${_isFaceInFrame.value}"); print("Detection state - Face left: ${_isFaceLeft.value}"); print("Detection state - Face right: ${_isFaceRight.value}"); print("Detection state - Smiled: ${_isSmiled.value}"); print("Detection state - Eyes open: ${_isEyeOpen.value}"); print("Detection state - Ready for photo: ${_isFaceReadyForPhoto.value}"); _updateHeadRotationStatus(rotY); _updateSmilingStatus(smileProb); _updateEyeOpenStatus(leftEyeOpen, rightEyeOpen); _updateFaceInFrameForPhotoStatus(rotY, smileProb); // Log status updates print("Updated status: ${status.value}"); print("Current instruction: ${currentInstruction.value}"); if (_isFaceInFrame.value && _isFaceLeft.value && _isFaceRight.value && _isSmiled.value && _isFaceReadyForPhoto.value && _isEyeOpen.value) { if (!_isCaptured.value) { _captureImage(); } } } } // 2. PERBAIKAN INPUT IMAGE PROCESSING InputImage? _getInputImageFromCameraImage(CameraImage image) { try { final sensorOrientation = frontCamera.sensorOrientation; InputImageRotation? rotation; // Log format untuk debugging // print( // "📸 Camera image format: ${image.format.raw}, planes: ${image.planes.length}", // ); // Handle rotasi sesuai platform if (Platform.isIOS) { rotation = InputImageRotationValue.fromRawValue(sensorOrientation) ?? InputImageRotation.rotation0deg; } else if (Platform.isAndroid) { var rotationCompensation = orientations[_cameraController!.value.deviceOrientation] ?? 0; if (frontCamera.lensDirection == CameraLensDirection.front) { rotationCompensation = (sensorOrientation + rotationCompensation) % 360; } else { rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360; } rotation = InputImageRotationValue.fromRawValue(rotationCompensation) ?? InputImageRotation.rotation0deg; } // Deteksi format gambar final format = InputImageFormatValue.fromRawValue(image.format.raw); // Ukuran gambar final Size imageSize = Size( image.width.toDouble(), image.height.toDouble(), ); // Tangani format YUV_420_888 yang terdeteksi di log debug if (image.format.raw == 35) { // YUV_420_888 memiliki raw value 35 // print("🔄 Processing YUV_420_888 format (raw value: 35)"); if (image.planes.length != 3) { // print( // "❌ YUV_420_888 requires 3 planes but got: ${image.planes.length}", // ); return null; } // Konversi YUV_420_888 ke format yang dimengerti ML Kit // Dengan mendapatkan hanya Y plane (luminance) yang bekerja untuk deteksi wajah final plane = image.planes[0]; // Y plane adalah yang penting untuk deteksi wajah return InputImage.fromBytes( bytes: plane.bytes, metadata: InputImageMetadata( size: imageSize, rotation: rotation ?? InputImageRotation.rotation0deg, format: InputImageFormat.nv21, // Format yang diharapkan ML Kit bytesPerRow: plane.bytesPerRow, ), ); } // Format lain (NV21 atau BGRA8888) else if (format == InputImageFormat.nv21 || format == InputImageFormat.bgra8888) { if (image.planes.isEmpty) { // print("❌ Not enough planes for ${format.toString()}"); return null; } final plane = image.planes.first; return InputImage.fromBytes( bytes: plane.bytes, metadata: InputImageMetadata( size: imageSize, rotation: rotation ?? InputImageRotation.rotation0deg, format: InputImageFormat.nv21, // Format yang diharapkan ML Kit bytesPerRow: plane.bytesPerRow, ), ); } // Format tidak dikenali else { // Mencoba konversi alternatif untuk format yang tidak didukung langsung return _processUnsupportedFormat(image, imageSize, rotation!); } } catch (e) { print('❌ Error creating InputImage: $e'); return null; } } // Metode baru untuk menangani format yang tidak didukung langsung InputImage? _processUnsupportedFormat( CameraImage image, Size imageSize, InputImageRotation rotation, ) { try { print( "⚠️ Using fallback conversion for unsupported format: ${image.format.raw}", ); // Untuk format apapun, coba gunakan plane pertama if (image.planes.isNotEmpty) { final plane = image.planes.first; // Force format sebagai NV21 yang paling umum didukung oleh ML Kit return InputImage.fromBytes( bytes: plane.bytes, metadata: InputImageMetadata( size: imageSize, rotation: rotation, format: InputImageFormat.nv21, bytesPerRow: plane.bytesPerRow, ), ); } print("❌ No planes available in image"); return null; } catch (e) { print("❌ Fallback format processing failed: $e"); return null; } } // 3. TAMBAHKAN METHOD UNTUK CEK CAMERA CAPABILITIES Future _checkCameraCapabilities() async { try { final cameras = await availableCameras(); print("=== CAMERA CAPABILITIES ==="); for (var camera in cameras) { print("Camera: ${camera.name}"); print(" Direction: ${camera.lensDirection}"); print(" Sensor Orientation: ${camera.sensorOrientation}"); } if (_cameraController?.value.isInitialized == true) { print("=== CURRENT CAMERA CONFIG ==="); print("Preview Size: ${_cameraController!.value.previewSize}"); print("Aspect Ratio: ${_cameraController!.value.aspectRatio}"); print("Is Streaming: ${_cameraController!.value.isStreamingImages}"); // device orientation final deviceOrientation = _cameraController!.value.deviceOrientation.toString(); print("Device Orientation: $deviceOrientation"); } print("============================"); } catch (e) { print("Error checking camera capabilities: $e"); } } // 4. FALLBACK CAMERA INITIALIZATION Future _initializeCameraWithFallback() async { // Ubah urutan preferensi format - YUV_420 di urutan pertama karena sepertinya itu yang digunakan final formatOptions = [ ImageFormatGroup.yuv420, // Prioritaskan YUV_420 yang terdeteksi dalam log ImageFormatGroup.nv21, // ImageFormatGroup.jpeg - tidak untuk streaming ]; final resolutionOptions = [ ResolutionPreset.medium, ResolutionPreset.low, ResolutionPreset.high, ]; for (var format in formatOptions) { for (var resolution in resolutionOptions) { try { print("🔄 Trying format: $format with resolution: $resolution"); final cameras = await availableCameras(); final frontCamera = cameras.firstWhere( (camera) => camera.lensDirection == CameraLensDirection.front, ); final controller = CameraController( frontCamera, resolution, imageFormatGroup: format, enableAudio: false, ); await controller.initialize(); // Test dengan sample image bool formatWorking = await _testCameraFormat(controller); if (formatWorking) { print( "✅ SUCCESS: Format $format with resolution $resolution works!", ); _cameraController = controller; this.frontCamera = frontCamera; return; } else { print("❌ Format $format with resolution $resolution failed test"); await controller.dispose(); } } catch (e) { print("❌ Failed format $format with resolution $resolution: $e"); } } } throw Exception("No compatible camera format found!"); } // 5. TEST METHOD UNTUK VALIDASI FORMAT Future _testCameraFormat(CameraController controller) async { try { bool testPassed = false; controller.startImageStream((CameraImage img) async { try { final inputImage = _getInputImageFromCameraImage(img); if (inputImage != null) { print("✅ InputImage created successfully"); testPassed = true; } } catch (e) { print("❌ Test failed: $e"); } // Stop stream after first test controller.stopImageStream(); }); // Wait for test await Future.delayed(Duration(milliseconds: 2000)); return testPassed; } catch (e) { print("❌ Camera format test error: $e"); return false; } } void _updateFaceInFrameStatus() { if (!_isFaceInFrame.value) { _isFaceInFrame.value = true; _addSuccessfulStep('Face in frame'); if (status.value == LivenessStatus.detectingFace) { status.value = LivenessStatus.checkLeftRotation; currentInstruction.value = 'Great! Now rotate your face to the left'; print("Face now in frame! Moving to LEFT rotation stage"); } } } // Longgarkan batasan rotasi kepala ke kiri void _updateHeadRotationStatus(double? rotY) { // Longgarkan deteksi rotasi ke kiri, nilai -3 lebih kecil dari -5 sebelumnya if (_isFaceInFrame.value && !_isFaceLeft.value && rotY != null && rotY < -3) { _isFaceLeft.value = true; _addSuccessfulStep('Face rotated left'); if (status.value == LivenessStatus.checkLeftRotation) { status.value = LivenessStatus.checkRightRotation; currentInstruction.value = 'Good! Now rotate your face to the right'; print("Left rotation detected! Moving to RIGHT rotation stage"); } } // Longgarkan deteksi rotasi ke kanan, nilai 3 lebih kecil dari 5 sebelumnya if (_isFaceLeft.value && !_isFaceRight.value && rotY != null && rotY > 3) { _isFaceRight.value = true; _addSuccessfulStep('Face rotated right'); if (status.value == LivenessStatus.checkRightRotation) { status.value = LivenessStatus.checkSmile; currentInstruction.value = 'Great! Now smile for the camera'; print("Right rotation detected! Moving to SMILE stage"); } } } // Longgarkan kondisi senyum void _updateSmilingStatus(double smileProb) { if (_isFaceInFrame.value && _isFaceLeft.value && _isFaceRight.value && !_isSmiled.value && smileProb > 0.1) { // Nilai diturunkan dari 0.2 menjadi 0.1 _isSmiled.value = true; _addSuccessfulStep('Smiling'); if (status.value == LivenessStatus.checkSmile) { status.value = LivenessStatus.checkEyesOpen; currentInstruction.value = 'Excellent! Now open your eyes wide'; print("Smile detected! Moving to EYES OPEN stage"); } } } // Longgarkan kondisi mata terbuka void _updateEyeOpenStatus(double leftEyeOpen, double rightEyeOpen) { if (_isFaceInFrame.value && _isFaceLeft.value && _isFaceRight.value && _isSmiled.value && !_isEyeOpen.value) { if (leftEyeOpen > 0.1 && rightEyeOpen > 0.1) { // Nilai diturunkan dari 0.2 menjadi 0.1 _isEyeOpen.value = true; _addSuccessfulStep('Eyes Open'); if (status.value == LivenessStatus.checkEyesOpen) { status.value = LivenessStatus.readyForPhoto; currentInstruction.value = 'Perfect! Hold still for photo capture'; print("Eyes open detected! Moving to READY FOR PHOTO stage"); } } } } // Longgarkan kondisi untuk siap foto void _updateFaceInFrameForPhotoStatus(double? rotY, double? smileProb) { // Longgarkan kondisi rotasi dan senyum if (_isFaceRight.value && _isFaceLeft.value && rotY != null && rotY > -5 && // Nilai lebih longgar dari -3 rotY < 5 && // Nilai lebih longgar dari 3 smileProb != null) // Hapus batasan senyum { _isFaceReadyForPhoto.value = true; _addSuccessfulStep('Face Ready For Photo'); if (status.value == LivenessStatus.checkEyesOpen) { status.value = LivenessStatus.readyForPhoto; currentInstruction.value = 'Perfect! Hold still for photo capture'; print("Face ready for photo! Moving to READY FOR PHOTO stage"); } } else { _isFaceReadyForPhoto.value = false; } } // Ganti metode _isFaceInsideFrame untuk selalu mengembalikan true bool _isFaceInsideFrame(Rect boundingBox) { // Selalu kembalikan true tanpa memeriksa batas-batas return true; } void _addSuccessfulStep(String step) { if (!_successfulSteps.contains(step)) { _successfulSteps.add(step); } } // Tambahkan metode untuk memangkas frame dan otomatis menyelesaikan proses verifikasi void autoCompleteVerification() { print("Auto-completing verification process"); // Set semua status _isFaceInFrame.value = true; _addSuccessfulStep('Face in frame'); _isFaceLeft.value = true; _addSuccessfulStep('Face rotated left'); _isFaceRight.value = true; _addSuccessfulStep('Face rotated right'); _isSmiled.value = true; _addSuccessfulStep('Smiling'); _isEyeOpen.value = true; _addSuccessfulStep('Eyes Open'); _isFaceReadyForPhoto.value = true; _addSuccessfulStep('Face Ready For Photo'); // Tangkap gambar _captureImage(); } // Tambahkan metode ini agar di debug panel bisa dipanggil void skipAllVerificationSteps() { autoCompleteVerification(); } // Modifikasi metode _processCameraImage untuk lebih relaks dalam deteksi wajah Future _processCameraImage(CameraImage img) async { // Avoid processing if already captured or currently processing if (_isCaptured.value || _processingImage) return; _processingImage = true; try { // Tambah delay untuk memberikan waktu lebih untuk pemrosesan await Future.delayed(Duration(milliseconds: 50)); // Use microtask untuk memberikan waktu lebih pada face detection await Future.microtask(() async { final inputImage = _getInputImageFromCameraImage(img); if (inputImage == null) { _processingImage = false; return; } // Log ukuran gambar untuk membantu debug zoom/resolusi // print("Processing image of size: ${img.width}x${img.height}"); // Berikan timeout yang lebih panjang untuk deteksi wajah (5 detik) List faces = []; try { // Gunakan timeout untuk mencegah pemrosesan yang terlalu lama faces = await _faceDetector .processImage(inputImage) .timeout( Duration(seconds: 3), onTimeout: () { print("⚠️ Face detection timed out after 3 seconds"); return []; }, ); print("Detected ${faces.length} faces"); } catch (e) { print("Face detection error: $e"); } // Process face detection results if (faces.isNotEmpty) { await _processFaces(faces); } else { _handleNoFacesDetected(); } }); } catch (e) { print('Error in image processing: $e'); } finally { // Ensure _processingImage is reset even if an error occurs _processingImage = false; } } // New method to process detected faces Future _processFaces(List faces) async { // Pilih wajah terbaik berdasarkan ukuran (prioritaskan wajah yang lebih besar) Face? bestFace; double largestArea = 0; for (var face in faces) { final area = face.boundingBox.width * face.boundingBox.height; if (area > largestArea) { largestArea = area; bestFace = face; } } // Periksa apakah wajah terbaik memenuhi ukuran minimum yang direkomendasikan if (bestFace != null) { final faceWidth = bestFace.boundingBox.width; final faceHeight = bestFace.boundingBox.height; print("Best face size: ${faceWidth}x$faceHeight"); // Pastikan wajah cukup besar untuk deteksi (minimal 80x80 pixel) // Reduced from 100x100 to improve detection with varied camera resolutions if (faceWidth >= 80 && faceHeight >= 80) { _isNoFace.value = false; _isMultiFace.value = false; await _compareFaces(bestFace); if (!_isDifferentPerson.value) { _handleFaceDetection(bestFace); } else { _duplicatePersonFaceDetect(); } } else { print( "Face too small: ${faceWidth}x$faceHeight, minimum 80x80 required", ); _isNoFace.value = true; currentInstruction.value = 'Please move closer to the camera'; } } else if (faces.isNotEmpty) { // If we have faces but none meet our "best face" criteria, use the first one anyway _isNoFace.value = false; _isMultiFace.value = faces.length > 1; final face = faces.first; await _compareFaces(face); _handleFaceDetection(face); } } // New method to handle when no faces are detected void _handleNoFacesDetected() { _isNoFace.value = true; // Don't reset progress if already started if (!_isFaceInFrame.value) { status.value = LivenessStatus.detectingFace; currentInstruction.value = 'No face detected. Please position your face in the frame and ensure good lighting.'; } } // Tambahkan variabel untuk mencegah pemrosesan berlebihan bool _processingImage = false; // Tambahkan metode retryDetection untuk mencoba ulang jika terjadi kegagalan deteksi void retryDetection() { if (_cameraController != null && _cameraController!.value.isInitialized) { // Reset flag proses tapi pertahankan kemajuan _processingImage = false; // Update pesan instruksi currentInstruction.value = 'Retrying face detection...'; // Tampilkan debug info print('Retrying face detection...'); } } // // Improve camera input image processing // InputImage? _getInputImageFromCameraImage(CameraImage image) { // final sensorOrientation = frontCamera.sensorOrientation; // InputImageRotation? rotation; // if (Platform.isIOS) { // rotation = InputImageRotationValue.fromRawValue(sensorOrientation); // } else if (Platform.isAndroid) { // var rotationCompensation = // orientations[_cameraController!.value.deviceOrientation]; // if (rotationCompensation == null) { // print("Warning: null rotation compensation"); // rotationCompensation = 0; // Provide default value // } // if (frontCamera.lensDirection == CameraLensDirection.front) { // rotationCompensation = (sensorOrientation + rotationCompensation) % 360; // } else { // rotationCompensation = // (sensorOrientation - rotationCompensation + 360) % 360; // } // rotation = InputImageRotationValue.fromRawValue(rotationCompensation!); // } // if (rotation == null) { // print("Warning: null input image rotation"); // return null; // } // final format = InputImageFormatValue.fromRawValue(image.format.raw); // if (format == null) { // print( // "Warning: null input image format from raw value: ${image.format.raw}", // ); // return null; // } // if ((Platform.isAndroid && format != InputImageFormat.nv21) || // (Platform.isIOS && format != InputImageFormat.bgra8888)) { // print("Warning: unexpected format for platform: $format"); // return null; // } // if (image.planes.length != 1) { // print("Warning: expected 1 plane, got ${image.planes.length}"); // return null; // } // final plane = image.planes.first; // return InputImage.fromBytes( // bytes: plane.bytes, // metadata: InputImageMetadata( // size: Size(image.width.toDouble(), image.height.toDouble()), // rotation: rotation, // format: format, // bytesPerRow: plane.bytesPerRow, // ), // ); // } // Metode untuk ambil foto dengan ukuran penuh Future _captureImage() async { if (_cameraController!.value.isTakingPicture) return; try { status.value = LivenessStatus.photoTaken; currentInstruction.value = 'Capturing photo...'; // Hentikan stream untuk foto berkualitas tinggi await _cameraController!.stopImageStream(); // Beri jeda singkat agar kamera stabil await Future.delayed(Duration(milliseconds: 500)); // Ambil foto dengan resolusi penuh final XFile file = await _cameraController!.takePicture(); print("Image captured: ${file.path}"); // Set status captured _isCaptured.value = true; _capturedImage.value = file; // Verifikasi bahwa gambar memenuhi kriteria minimum (min 480x360) final imageFile = File(file.path); if (await imageFile.exists()) { final fileSize = await imageFile.length(); print("Captured image size: $fileSize bytes"); if (fileSize < 5000) { // Gambar terlalu kecil mungkin rusak print("Warning: Captured image is too small ($fileSize bytes)"); // Bisa retry atau handle error di sini } } status.value = LivenessStatus.completed; currentInstruction.value = 'Liveness check successful!'; // Tutup detektor wajah untuk hemat resources _faceDetector.close(); } catch (e) { print('Error capturing image: $e'); status.value = LivenessStatus.failed; currentInstruction.value = 'Failed to capture image: $e'; // Restart preview jika gagal try { _cameraController!.startImageStream((CameraImage img) { _processCameraImage(img); }); } catch (e) { print('Error restarting preview: $e'); } } } // Handle detection of a different person (duplicate face) void _duplicatePersonFaceDetect() { print( "Different person detected! Please ensure only one person is in front of the camera.", ); _isDifferentPerson.value = true; currentInstruction.value = 'Different person detected. Please ensure only one person is in front of the camera.'; status.value = LivenessStatus.failed; } // Face comparison methods Future> _extractFaceEmbeddings(Face face) async { return [ face.boundingBox.left, face.boundingBox.top, face.boundingBox.right, face.boundingBox.bottom, ]; } Future _compareFaces(Face currentFace) async { final currentEmbedding = await _extractFaceEmbeddings(currentFace); if (_firstPersonEmbedding == null) { _firstPersonEmbedding = currentEmbedding; } else { final double similarity = _calculateSimilarity( _firstPersonEmbedding!, currentEmbedding, ); _isDifferentPerson.value = similarity < 0.8; } } double _calculateSimilarity( List embedding1, List embedding2, ) { double dotProduct = 0.0; double norm1 = 0.0; double norm2 = 0.0; for (int i = 0; i < embedding1.length; i++) { dotProduct += embedding1[i] * embedding2[i]; norm1 += embedding1[i] * embedding1[i]; norm2 += embedding2[i] * embedding2[i]; } return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); } String getCurrentDirection() { // Use the currentInstruction instead return currentInstruction.value; } @override void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? cameraController = _cameraController; if (cameraController == null || !cameraController.value.isInitialized) { return; } if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { _initializeCamera(); } } // Track if user left the screen final RxBool wasDetectionCancelled = RxBool(false); // Add a method to handle cleanup when users cancel void handleCancellation() { wasDetectionCancelled.value = true; // Make sure to update the selfie controller if (Get.isRegistered()) { final selfieController = Get.find(); selfieController.cancelLivenessDetection(); } } @override void onClose() { // If detection was not complete and not cancelled already, mark as cancelled if (!isCaptured && !wasDetectionCancelled.value) { handleCancellation(); } _faceDetector.close(); if (_cameraController != null) _cameraController!.dispose(); WidgetsBinding.instance.removeObserver(this); _faceMeshDetector.close(); super.onClose(); } /// Generate a FaceModel from the captured image FaceModel generateFaceModel() { if (_capturedImage.value == null) { return FaceModel.empty(); } final uuid = Uuid(); return FaceModel( imagePath: _capturedImage.value!.path, faceId: uuid.v4(), confidence: 0.95, boundingBox: {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8}, ).withLiveness( isLive: true, confidence: 0.92, message: 'Liveness check passed successfully', ); } // Add a helper method to manually advance for testing purposes void forceAdvanceToNextStep() { switch (status.value) { case LivenessStatus.detectingFace: _isFaceInFrame.value = true; _addSuccessfulStep('Face in frame'); status.value = LivenessStatus.checkLeftRotation; currentInstruction.value = 'Great! Now rotate your face to the left'; print("Forced: Face in frame step completed"); break; case LivenessStatus.checkLeftRotation: _isFaceLeft.value = true; _addSuccessfulStep('Face rotated left'); status.value = LivenessStatus.checkRightRotation; currentInstruction.value = 'Good! Now rotate your face to the right'; print("Forced: Face left rotation step completed"); break; case LivenessStatus.checkRightRotation: _isFaceRight.value = true; _addSuccessfulStep('Face rotated right'); status.value = LivenessStatus.checkSmile; currentInstruction.value = 'Great! Now smile for the camera'; print("Forced: Face right rotation step completed"); break; case LivenessStatus.checkSmile: _isSmiled.value = true; _addSuccessfulStep('Smiling'); status.value = LivenessStatus.checkEyesOpen; currentInstruction.value = 'Excellent! Now open your eyes wide'; print("Forced: Smile step completed"); break; case LivenessStatus.checkEyesOpen: _isEyeOpen.value = true; _isFaceReadyForPhoto.value = true; _addSuccessfulStep('Eyes Open'); _addSuccessfulStep('Face Ready For Photo'); status.value = LivenessStatus.readyForPhoto; currentInstruction.value = 'Perfect! Hold still for photo capture'; print("Forced: Eyes open step completed"); break; case LivenessStatus.readyForPhoto: forceCaptureImage(); break; default: print("Forced: No action for current state: ${status.value}"); break; } } // Method to force capture image manually (for debugging) Future forceCaptureImage() async { if (_cameraController == null || !_cameraController!.value.isInitialized) { print("Cannot force capture: camera not initialized"); return; } if (_cameraController!.value.isTakingPicture) { print("Cannot force capture: camera already taking picture"); return; } try { print("Forcing manual image capture..."); status.value = LivenessStatus.photoTaken; currentInstruction.value = 'Capturing photo...'; final XFile file = await _cameraController!.takePicture(); _isCaptured.value = true; _capturedImage.value = file; status.value = LivenessStatus.completed; currentInstruction.value = 'Liveness check successful! (Manual capture)'; print("Manual image capture successful"); } catch (e) { print('Error during manual image capture: $e'); status.value = LivenessStatus.failed; currentInstruction.value = 'Failed to capture image manually: $e'; } } // Reset Process void resetProcess() { // Reset all state variables _isFaceInFrame.value = false; _isFaceLeft.value = false; _isFaceRight.value = false; _isEyeOpen.value = false; _isNoFace.value = false; _isMultiFace.value = false; _isCaptured.value = false; _isSmiled.value = false; _isFaceReadyForPhoto.value = false; _isDifferentPerson.value = false; _processingImage = false; _throttled = false; status.value = LivenessStatus.preparing; currentInstruction.value = 'Initializing camera...'; _successfulSteps.clear(); // Try to adjust camera parameters if controller exists if (_cameraController != null && _cameraController!.value.isInitialized) { // Reset zoom to minimum for better face detection _cameraController! .getMinZoomLevel() .then((minZoom) { _cameraController!.setZoomLevel(minZoom); }) .catchError((e) { print("Failed to reset zoom level: $e"); }); } else { // Reinitialize camera if needed _initializeCamera(); } } // Kunci orientasi ke portrait Future _lockDeviceOrientation() async { try { print("🔒 Locking device orientation to portrait"); await SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); print("✅ Device orientation locked to portrait"); } catch (e) { print("❌ Failed to lock orientation: $e"); } } // Tambahkan fungsi untuk mengembalikan orientasi Future _resetDeviceOrientation() async { try { print("🔓 Resetting device orientation"); await SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); } catch (e) { print("❌ Failed to reset orientation: $e"); } } String _getCurrentDeviceOrientation() { final orientation = MediaQuery.of(Get.context!).orientation; return orientation == Orientation.portrait ? "Portrait" : "Landscape"; } } import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; class CameraScreen extends StatefulWidget { const CameraScreen({super.key}); @override State createState() => _CameraScreenState(); } class _CameraScreenState extends State with WidgetsBindingObserver, TickerProviderStateMixin { CameraController? controller; String error=''; FaceDetector faceDetector = FaceDetector( options: FaceDetectorOptions( enableClassification: true, enableContours: true, enableTracking: true, enableLandmarks: true, performanceMode: Platform.isAndroid ? FaceDetectorMode.fast : FaceDetectorMode.accurate, ), ); final orientations = { DeviceOrientation.portraitUp: 0, DeviceOrientation.landscapeLeft: 90, DeviceOrientation.portraitDown: 180, DeviceOrientation.landscapeRight: 270, }; initialize() async { controller = CameraController( cameras[1], ResolutionPreset.medium, imageFormatGroup: Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888, ); if (mounted) setState(() {}); initializeCameraController(controller!.description); if (mounted) setState(() {}); } @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); initialize(); } @override void dispose() { faceDetector.close(); if (controller != null) controller!.dispose(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { return; } if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { initialize(); } } Future initializeCameraController() async { try { await controller!.initialize(); controller!.setFlashMode(FlashMode.off); // If the controller is updated then update the UI. controller!.addListener(() { if (mounted) setState(() {}); controller!.startImageStream(_processCameraImage).then((value) {}); }); if (mounted) setState(() {}); } on CameraException catch (e) { switch (e.code) { case 'CameraAccessDenied': Get.back(); cameraDialog(); break; case 'CameraAccessDeniedWithoutPrompt': Get.back(); cameraDialog(); break; case 'CameraAccessRestricted': Get.back(); cameraDialog(); break; default: Get.back(); EasyLoading.showError(e.description!); break; } } if (mounted) setState(() {}); } cameraDialog() { Get.defaultDialog( title: 'Camera Access', middleText: 'Looks like you have not provided permission to camera completely. Enable it in the settings.', onConfirm: () async { Get.back(); await openAppSettings(); }, ); } clearCaptured(String message) { debugPrint(message); imageFile = null; error = message; if (mounted) setState(() {}); return; } void _processCameraImage(CameraImage image) { final inputImage = inputImageFromCameraImage(image); if (inputImage == null) return; checkImage(inputImage); } checkImage(InputImage inputImage) async { List faces = await faceDetector.processImage(inputImage); debugPrint('faces: $faces'); if (faces.isEmpty) { clearCaptured('No faces detected in the image.'); } if (faces.length > 1) { clearCaptured( 'Multiple faces detected. Please try capturing a single face.', ); } // Handle the detected faces for (Face face in faces) { if (face.leftEyeOpenProbability != null && face.rightEyeOpenProbability != null) { final leftEyeOpen = face.leftEyeOpenProbability! > 0.5; final rightEyeOpen = face.rightEyeOpenProbability! > 0.5; if (leftEyeOpen && rightEyeOpen) { error = 'Click capture to save this image'; if (mounted) setState(() {}); return; } else if (!leftEyeOpen && !rightEyeOpen) { clearCaptured('Both eyes are closed!'); } else if (leftEyeOpen && !rightEyeOpen) { clearCaptured('Left eye is open, right eye is closed.'); } else if (!leftEyeOpen && rightEyeOpen) { clearCaptured('Right eye is open, left eye is closed.'); } } else { clearCaptured( 'Looks like either one or both eyes have not been captured properly. Please try again.', ); } } } InputImage? inputImageFromCameraImage(CameraImage image) { final camera = cameras[1]; final sensorOrientation = camera.sensorOrientation; InputImageRotation? rotation; if (Platform.isIOS) { rotation = InputImageRotationValue.fromRawValue(sensorOrientation); } else if (Platform.isAndroid) { var rotationCompensation = orientations[controller!.value.deviceOrientation]; if (rotationCompensation == null) return null; if (camera.lensDirection == CameraLensDirection.front) { // front-facing rotationCompensation = (sensorOrientation + rotationCompensation) % 360; } else { // back-facing rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360; } rotation = InputImageRotationValue.fromRawValue(rotationCompensation); } if (rotation == null) return null; // get image format final format = InputImageFormatValue.fromRawValue(image.format.raw); if (format == null || (Platform.isAndroid && format != InputImageFormat.nv21) || (Platform.isIOS && format != InputImageFormat.bgra8888)) return null; // since format is constraint to nv21 or bgra8888, both only have one plane if (image.planes.length != 1) return null; final plane = image.planes.first; // compose InputImage using bytes return InputImage.fromBytes( bytes: plane.bytes, metadata: InputImageMetadata( size: Size(image.width.toDouble(), image.height.toDouble()), rotation: rotation, // used only in Android format: format, // used only in iOS bytesPerRow: plane.bytesPerRow, // used only in iOS ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text('Capture Your Selfie'), ), bottomNavigationBar: controller == null ? null : Column( mainAxisSize: MainAxisSize.min, children: [ Text(error), SizedBox(height:30), ElevatedButton( child: Text('Capture'), onPressed: () async { await controller!.takePicture().then((value) {}); }, ), SizedBox(height:20), ], ), body: controller == null ? Center(child: Text('Need to access camera to capture selfie')) : CameraPreview(controller!), ); } }