// ignore_for_file: constant_identifier_names import 'package:camera/camera.dart' as camera; import 'package:flutter/foundation.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart' as mlkit; import 'package:vitalapp/rpc.dart'; import 'package:vitalapp/architecture/storage/storage.dart'; import 'package:vnote_device_plugin/events/event_type.dart'; import 'package:fis_common/logger/logger.dart'; abstract class FacialRecognitionExceptionCode { static const ABORT = -1; static const VOID = 0; static const FACE_NOT_FOUNT = 1001; static const FACE_OVER_COUNT = 1002; static const PTHOT_TAKE_ERROR = 2001; static const PTHOT_TYPE_ERROR = 2002; static const PTHOT_UPLOAD_FAIL = 2003; static const RESULT_UNCHECKED = 3001; static const RESULT_NOT_CLEAR = 3002; } class FacialRecognitionException implements Exception { final int code; final String message; FacialRecognitionException(this.code, this.message); @override String toString() { return "[FacialRecognitionException] code: $code, message: $message."; } } /// 人脸识别采集器 class FacialRecognitionCapturer { /// 重试次数限制 /// /// - 默认`10`次 final int retryLimit; /// 单次识别图像已缓存事件 final frameCachedEvent = FEventHandler(); /// 识别成功事件 final successEvent = FEventHandler(); /// 识别失败事件 final failEvent = FEventHandler(); /// 识别超时事件 - 超过限制次数 final timeoutEvent = FEventHandler(); FacialRecognitionCapturer({this.retryLimit = 10}); int _workedCount = 0; _Worker? _worker; Object? _result; bool _isBusy = false; bool _isPause = false; camera.CameraController? _cameraController; mlkit.FaceDetector? _faceDetector; /// 识别结果 Object? get result => _result; /// 初始化 Future init( camera.CameraController? cameraController, mlkit.FaceDetector faceDetector, ) async { _cameraController = cameraController; _faceDetector = faceDetector; logger.i("FacialRecognitionCapturer init."); } /// 暂停 void callPause() { _isPause = true; logger.i("FacialRecognitionCapturer pause."); } /// 继续 void callContinue() { _isPause = false; logger.i("FacialRecognitionCapturer continue."); } /// 开启工作 Future run() async { logger.i("FacialRecognitionCapturer start run..."); _isBusy = true; while (_isBusy) { if (_workedCount > retryLimit) { stop(); timeoutEvent.emit(this, null); break; } if (_isPause == false) { _workedCount++; await _doOnce(); } // 每次间隔100ms检测一次 await Future.delayed(const Duration(milliseconds: 100)); } logger.i("FacialRecognitionCapturer run finish."); } /// 停止工作 void stop() { logger.i("FacialRecognitionCapturer stop..."); _isBusy = false; _workedCount = retryLimit + 1; _worker?.stop(); _worker = null; logger.i("FacialRecognitionCapturer stop done."); } Future _doOnce() async { try { final $ = WorkVariables(); $.cameraController = _cameraController; $.faceDetector = _faceDetector; $.imgCachedCallback = (value) { // 通知识别帧已缓存本地 frameCachedEvent.emit(this, value); }; _worker = _Worker($); await _worker!.init(); await _worker!.run(); _result = $.result; if ($.result != null) { // 暂停等待success回调处理后发送继续工作信号 callPause(); // 识别成功 successEvent.emit(this, $.result!); } } on FacialRecognitionException catch (e) { if (e.code > 0) { failEvent.emit(this, e); } } catch (e) { logger.e("FacialRecognitionCapturer do once error.", e); } } } class _Worker { final steps = Function()>[]; late final WorkVariables $; bool isContinue = false; _Worker(WorkVariables variables) { $ = variables; steps.add(takePhoto); steps.add(detectFace); steps.add(checkFaceNum); steps.add(uploadFile); } Future init() async {} Future run() async { isContinue = true; final count = steps.length; for (var i = 0; i < count; i++) { await steps[i].call(); } isContinue = false; } void stop() { isContinue = false; } /// 执行一次拍摄 Future takePhoto() async { try { final c = $.cameraController; if (c == null || c.value.isInitialized == false) { return abortWithException( FacialRecognitionExceptionCode.PTHOT_TAKE_ERROR, "未识别到摄像头", ); } if (c.value.isTakingPicture) { return abort(); } final file = await c.takePicture(); $.cachedFile = file; $.imgCachedCallback?.call(""); } on camera.CameraException catch (e) { logger.e("FacialRecognitionCapturer._Worker takePhoto error.", e); // TODO: 暂时不提示 // abortWithException( // FacialRecognitionExceptionCode.PTHOT_TAKE_ERROR, // e.description ?? "拍摄失败", // ); abort(); } } /// 检测一次人脸 Future detectFace() async { try { final image = mlkit.InputImage.fromFilePath($.cachedFile!.path); final faces = await $.faceDetector!.processImage(image); $.faceNum = faces.length; } catch (e) { logger.e("FacialRecognitionCapturer._Worker detectFace error.", e); abort(); } } /// 检查人脸数量 Future checkFaceNum() async { final num = $.faceNum; if (num == 0) { return abortWithException( FacialRecognitionExceptionCode.FACE_NOT_FOUNT, "请将面部保持在识别框内", ); } if (num > 1) { return abortWithException( FacialRecognitionExceptionCode.FACE_OVER_COUNT, "请保持只有一张面部在识别范围内", ); } } /// 上传图像 Future uploadFile() async { try { final file = $.cachedFile!; final String fileType = file.path.split('.').last; if (['png', 'jpg'].contains(fileType) == false) { return abortWithException( FacialRecognitionExceptionCode.PTHOT_TYPE_ERROR, "上传的图像类型错误", ); } $.imgCachedCallback?.call(file.path); final url = await rpc.storage.upload( file, fileType: fileType, ); if (url == null || url.isEmpty) { return abortWithException( FacialRecognitionExceptionCode.PTHOT_UPLOAD_FAIL, "上传失败", ); } $.result = url; } catch (e) { logger.e("FacialRecognitionCapturer._Worker upload file error.", e); abortWithException( FacialRecognitionExceptionCode.PTHOT_UPLOAD_FAIL, "上传人像失败", ); } finally { $.imgCachedCallback?.call(""); } } void abort() { isContinue = false; throw FacialRecognitionException( FacialRecognitionExceptionCode.ABORT, "中断", ); } void abortWithException(int code, String message) { isContinue = false; throw FacialRecognitionException(code, message); } } class WorkVariables { camera.CameraController? cameraController; mlkit.FaceDetector? faceDetector; ValueChanged? imgCachedCallback; camera.XFile? cachedFile; int faceNum = 0; String? result; }