474 lines
16 KiB
Dart
474 lines
16 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
import 'package:pj_supermarket_cloud/common/index.dart';
|
|
|
|
class FaceRecognitionSessionHandle {
|
|
const FaceRecognitionSessionHandle({
|
|
required this.sessionId,
|
|
required this.scene,
|
|
required this.events,
|
|
required this.result,
|
|
});
|
|
|
|
final String sessionId;
|
|
final FaceRecognitionScene scene;
|
|
final Stream<FaceRecognitionSessionEvent> events;
|
|
final Future<FaceStream> result;
|
|
}
|
|
|
|
/// 主屏唯一人脸会话入口,统一处理权限、原生状态、双屏动作和结果幂等。
|
|
///
|
|
/// Camera 与虹软热保持由 Android 负责;Dart 结束会话只关闭当前业务和 UI,
|
|
/// 不会让连续排队的下一位顾客重新打开摄像头。
|
|
class FaceRecognitionSessionService extends GetxService {
|
|
static const Duration _viceStateTimeout = Duration(seconds: 2);
|
|
|
|
final cameraOpen = false.obs;
|
|
final cameraStatus = FaceRecognitionSessionStatus.cold.obs;
|
|
|
|
StreamSubscription<FaceRecognitionSessionEvent>? _cameraStateSubscription;
|
|
StreamSubscription<FaceRecognitionSessionEvent>? _nativeStateSubscription;
|
|
StreamSubscription<FaceStream?>? _faceSubscription;
|
|
StreamController<FaceRecognitionSessionEvent>? _eventController;
|
|
Completer<FaceStream>? _resultCompleter;
|
|
String? _activeSessionId;
|
|
FaceRecognitionScene? _activeScene;
|
|
int _revision = 0;
|
|
int _nativeRevision = 0;
|
|
bool _resultLocked = false;
|
|
bool _nativeStarted = false;
|
|
bool _startFailureEmitted = false;
|
|
int? _payableAmount;
|
|
int? _totalAmount;
|
|
int? _discountAmount;
|
|
SubScreenAction? _lastViceAction;
|
|
String? _lastViceMessage;
|
|
|
|
bool get isActive => _activeSessionId != null;
|
|
String? get activeSessionId => _activeSessionId;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
// 顶栏需要跨业务 Session 展示 Camera 热状态,因此常驻订阅不能跟随弹框关闭。
|
|
_cameraStateSubscription = FaceDiscernPlugs.sessionStream.listen(
|
|
_trackCameraState,
|
|
onError: (_) => cameraOpen.value = false,
|
|
);
|
|
unawaited(_refreshCameraState());
|
|
}
|
|
|
|
Future<void> _refreshCameraState() async {
|
|
try {
|
|
_trackCameraState(await FaceDiscernPlugs.getRecognitionState());
|
|
} catch (error) {
|
|
logger.w('|人脸识别|读取摄像头状态失败:$error');
|
|
}
|
|
}
|
|
|
|
void _trackCameraState(FaceRecognitionSessionEvent event) {
|
|
cameraOpen.value = event.warm;
|
|
cameraStatus.value = event.status;
|
|
}
|
|
|
|
bool owns(String? sessionId) =>
|
|
sessionId != null && sessionId == _activeSessionId;
|
|
|
|
Future<FaceRecognitionSessionHandle> start({
|
|
required FaceRecognitionScene scene,
|
|
int? payableAmount,
|
|
int? totalAmount,
|
|
int? discountAmount,
|
|
}) async {
|
|
if (_activeSessionId != null) {
|
|
throw const FaceRecognitionFailure(
|
|
code: 'SESSION_BUSY',
|
|
message: '已有进行中的人脸识别,请勿重复操作',
|
|
);
|
|
}
|
|
final cameraPermission = await Permission.camera.request();
|
|
if (!cameraPermission.isGranted) {
|
|
throw FaceRecognitionFailure(
|
|
code: 'CAMERA_PERMISSION_DENIED',
|
|
message: cameraPermission.isPermanentlyDenied
|
|
? '相机权限已被永久拒绝,请在系统设置中开启'
|
|
: '人脸识别需要相机权限,请授权后重试',
|
|
);
|
|
}
|
|
|
|
final sessionId = DateTime.now().microsecondsSinceEpoch.toString();
|
|
logger.i('|人脸识别|发起会话sessionId:$sessionId|scene:${scene.name}|');
|
|
_activeSessionId = sessionId;
|
|
_activeScene = scene;
|
|
_revision = 0;
|
|
_nativeRevision = 0;
|
|
_resultLocked = false;
|
|
_nativeStarted = false;
|
|
_startFailureEmitted = false;
|
|
_payableAmount = payableAmount;
|
|
_totalAmount = totalAmount;
|
|
_discountAmount = discountAmount;
|
|
_lastViceAction = null;
|
|
_lastViceMessage = null;
|
|
_eventController =
|
|
StreamController<FaceRecognitionSessionEvent>.broadcast();
|
|
_resultCompleter = Completer<FaceStream>();
|
|
_nativeStateSubscription = FaceDiscernPlugs.sessionStream.listen(
|
|
_handleNativeState,
|
|
onError: _handleNativeError,
|
|
);
|
|
_faceSubscription = FaceDiscernPlugs.faceStream.listen(_handleFaceResult);
|
|
|
|
final handle = FaceRecognitionSessionHandle(
|
|
sessionId: sessionId,
|
|
scene: scene,
|
|
events: _eventController!.stream,
|
|
result: _resultCompleter!.future,
|
|
);
|
|
unawaited(_startNative(
|
|
sessionId: sessionId,
|
|
scene: scene,
|
|
payableAmount: payableAmount,
|
|
totalAmount: totalAmount,
|
|
discountAmount: discountAmount,
|
|
));
|
|
return handle;
|
|
}
|
|
|
|
Future<void> _startNative({
|
|
required String sessionId,
|
|
required FaceRecognitionScene scene,
|
|
int? payableAmount,
|
|
int? totalAmount,
|
|
int? discountAmount,
|
|
}) async {
|
|
try {
|
|
_startFailureEmitted = false;
|
|
// 副屏仅承担顾客提示,发送异常或插件不回包都不能阻断原生 Camera 主链路。
|
|
unawaited(_sendViceState(
|
|
SubScreenAction.faceRecognitionStart,
|
|
message: '正在启动摄像头',
|
|
payableAmount: payableAmount,
|
|
totalAmount: totalAmount,
|
|
discountAmount: discountAmount,
|
|
));
|
|
if (_activeSessionId != sessionId) return;
|
|
logger.i('|人脸识别|开始原生调用startRecognition|sessionId:$sessionId|');
|
|
final initial = await FaceDiscernPlugs.startRecognition(
|
|
sessionId: sessionId,
|
|
scene: scene,
|
|
);
|
|
logger.i(
|
|
'|人脸识别|原生调用startRecognition完成|sessionId:$sessionId|'
|
|
'status:${initial.status.name}|revision:${initial.revision}|',
|
|
);
|
|
if (_activeSessionId == sessionId) {
|
|
_nativeStarted = true;
|
|
if (initial.revision > _nativeRevision) {
|
|
_nativeRevision = initial.revision;
|
|
_eventController?.add(initial);
|
|
}
|
|
}
|
|
} on PlatformException catch (error) {
|
|
if (_activeSessionId != sessionId) return;
|
|
_nativeStarted = true;
|
|
_startFailureEmitted = true;
|
|
logger.e(
|
|
'|人脸识别|原生调用startRecognition失败|sessionId:$sessionId|'
|
|
'code:${error.code}|message:${error.message}|',
|
|
);
|
|
unawaited(_sendViceState(
|
|
SubScreenAction.faceRecognitionFailed,
|
|
message: error.message ?? '无法启动人脸识别,请检查设备后重试',
|
|
));
|
|
_emitLocalFailure(FaceRecognitionFailure(
|
|
code: error.code,
|
|
message: error.message ?? '无法启动人脸识别,请检查设备后重试',
|
|
));
|
|
} catch (error, stack) {
|
|
if (_activeSessionId != sessionId) return;
|
|
_nativeStarted = true;
|
|
_startFailureEmitted = true;
|
|
logger.e(
|
|
'|人脸识别|启动原生会话异常|sessionId:$sessionId|'
|
|
'error:$error|stack:$stack|',
|
|
);
|
|
_emitLocalFailure(const FaceRecognitionFailure(
|
|
code: 'RECOGNITION_START_FAILED',
|
|
message: '无法启动人脸识别,请检查设备后重试',
|
|
));
|
|
}
|
|
}
|
|
|
|
Future<void> retry() async {
|
|
final sessionId = _activeSessionId;
|
|
if (sessionId == null) return;
|
|
_resultLocked = false;
|
|
_startFailureEmitted = false;
|
|
if (_nativeStarted) {
|
|
await FaceDiscernPlugs.retryRecognition(sessionId);
|
|
return;
|
|
}
|
|
await _startNative(
|
|
sessionId: sessionId,
|
|
scene: _activeScene!,
|
|
payableAmount: _payableAmount,
|
|
totalAmount: _totalAmount,
|
|
discountAmount: _discountAmount,
|
|
);
|
|
}
|
|
|
|
/// 取消当前业务人脸会话,并在释放原生资源前先让副屏退出人脸页。
|
|
Future<void> cancel({String message = '已取消人脸识别'}) async {
|
|
final sessionId = _activeSessionId;
|
|
if (sessionId == null) return;
|
|
// 主屏弹框会在本方法返回后立即关闭,必须先串行投递取消状态;
|
|
// 否则后续的预约查询消息可能先到,副屏仍会停留在人脸识别页。
|
|
await _sendViceState(
|
|
SubScreenAction.faceRecognitionCancel,
|
|
message: message,
|
|
);
|
|
logger.i('|人脸识别|开始原生调用cancelRecognition|sessionId:$sessionId|');
|
|
await FaceDiscernPlugs.cancelRecognition(sessionId, message: message);
|
|
if (_resultCompleter?.isCompleted == false) {
|
|
_resultCompleter!.completeError(
|
|
FaceRecognitionFailure(code: 'CANCELLED', message: message),
|
|
);
|
|
}
|
|
await _disposeSession();
|
|
}
|
|
|
|
Future<void> finish() async {
|
|
final sessionId = _activeSessionId;
|
|
if (sessionId == null) return;
|
|
logger.i('|人脸识别|开始原生调用finishRecognition|sessionId:$sessionId|');
|
|
await FaceDiscernPlugs.finishRecognition(sessionId);
|
|
await _disposeSession();
|
|
}
|
|
|
|
Future<void> suspend() async {
|
|
final sessionId = _activeSessionId;
|
|
if (sessionId == null) return;
|
|
unawaited(_sendViceState(
|
|
SubScreenAction.faceRecognitionCancel,
|
|
message: '应用已进入后台,人脸识别已结束',
|
|
));
|
|
logger.i('|人脸识别|开始原生调用suspendRecognition|sessionId:$sessionId|');
|
|
await FaceDiscernPlugs.suspendRecognition(
|
|
reason: '应用已进入后台,人脸识别已结束',
|
|
);
|
|
if (_resultCompleter?.isCompleted == false) {
|
|
_resultCompleter!.completeError(const FaceRecognitionFailure(
|
|
code: 'CANCELLED',
|
|
message: '应用已进入后台,人脸识别已结束',
|
|
));
|
|
}
|
|
await _disposeSession();
|
|
}
|
|
|
|
void _handleNativeState(FaceRecognitionSessionEvent event) {
|
|
final sessionId = _activeSessionId;
|
|
if (sessionId == null ||
|
|
event.sessionId != sessionId ||
|
|
event.revision <= _nativeRevision) {
|
|
return;
|
|
}
|
|
_nativeRevision = event.revision;
|
|
// MethodChannel 失败会先于 EventChannel 中已排队的 initializing 事件返回。
|
|
// 本地已经进入失败态后,必须丢弃这些旧进度,避免副屏从“需要重试”倒退到“识别中”。
|
|
if (_startFailureEmitted &&
|
|
(event.status == FaceRecognitionSessionStatus.initializing ||
|
|
event.status == FaceRecognitionSessionStatus.detecting)) {
|
|
logger.w(
|
|
'|人脸识别|忽略启动失败后迟到的原生进度|sessionId:$sessionId|'
|
|
'status:${event.status.name}|revision:${event.revision}|',
|
|
);
|
|
return;
|
|
}
|
|
_eventController?.add(event);
|
|
switch (event.status) {
|
|
case FaceRecognitionSessionStatus.initializing:
|
|
case FaceRecognitionSessionStatus.detecting:
|
|
unawaited(_sendViceState(
|
|
SubScreenAction.faceRecognitionDetecting,
|
|
message: event.message.isEmpty ? '请正对摄像头' : event.message,
|
|
));
|
|
break;
|
|
case FaceRecognitionSessionStatus.success:
|
|
unawaited(_sendViceState(
|
|
SubScreenAction.faceRecognitionSuccess,
|
|
message: '识别成功,正在读取信息',
|
|
));
|
|
break;
|
|
case FaceRecognitionSessionStatus.failed:
|
|
unawaited(_sendViceState(
|
|
SubScreenAction.faceRecognitionFailed,
|
|
message: event.message.isEmpty ? '人脸识别失败,请重试' : event.message,
|
|
));
|
|
final code = event.errorCode;
|
|
if (code == 'LOW_MEMORY' || code == 'SESSION_SUSPENDED') {
|
|
_completeWithFailure(FaceRecognitionFailure(
|
|
code: code!,
|
|
message: event.message.isEmpty ? '人脸识别已结束,请重新发起' : event.message,
|
|
));
|
|
}
|
|
break;
|
|
case FaceRecognitionSessionStatus.cancelled:
|
|
unawaited(_sendViceState(
|
|
SubScreenAction.faceRecognitionCancel,
|
|
message: event.message.isEmpty ? '已取消人脸识别' : event.message,
|
|
));
|
|
_completeWithFailure(FaceRecognitionFailure(
|
|
code: 'CANCELLED',
|
|
message: event.message.isEmpty ? '已取消人脸识别' : event.message,
|
|
));
|
|
break;
|
|
case FaceRecognitionSessionStatus.cold:
|
|
case FaceRecognitionSessionStatus.warmIdle:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void _handleNativeError(Object error) {
|
|
const failure = FaceRecognitionFailure(
|
|
code: 'SESSION_STREAM_ERROR',
|
|
message: '人脸识别服务连接异常,请重试',
|
|
);
|
|
_emitLocalFailure(failure);
|
|
_completeWithFailure(failure);
|
|
}
|
|
|
|
void _emitLocalFailure(FaceRecognitionFailure failure) {
|
|
final sessionId = _activeSessionId;
|
|
final scene = _activeScene;
|
|
if (sessionId == null || scene == null) return;
|
|
_eventController?.add(FaceRecognitionSessionEvent(
|
|
sessionId: sessionId,
|
|
scene: scene,
|
|
status: FaceRecognitionSessionStatus.failed,
|
|
revision: ++_revision,
|
|
width: 1280,
|
|
height: 720,
|
|
rotation: 0,
|
|
mirror: true,
|
|
warm: false,
|
|
livenessEnabled: false,
|
|
keepMaxFace: false,
|
|
similarityThreshold: 0.8,
|
|
message: failure.message,
|
|
faceBoxes: const [],
|
|
errorCode: failure.code,
|
|
));
|
|
}
|
|
|
|
void _handleFaceResult(FaceStream? event) {
|
|
final activeSessionId = _activeSessionId;
|
|
if (event == null || activeSessionId == null || _resultLocked) return;
|
|
// EventChannel 可能在会话切换后仍投递上一帧;空会话或旧会话结果都不能绑定到下一笔订单。
|
|
if (event.sessionId != activeSessionId) return;
|
|
final userId = event.userId?.trim();
|
|
if (userId == null || userId.isEmpty) return;
|
|
_resultLocked = true;
|
|
_resultCompleter?.complete(event);
|
|
}
|
|
|
|
void _completeWithFailure(FaceRecognitionFailure failure) {
|
|
if (_resultCompleter?.isCompleted == false) {
|
|
_resultCompleter!.completeError(failure);
|
|
}
|
|
}
|
|
|
|
Future<void> _sendViceState(
|
|
SubScreenAction action, {
|
|
required String message,
|
|
int? payableAmount,
|
|
int? totalAmount,
|
|
int? discountAmount,
|
|
}) async {
|
|
final sessionId = _activeSessionId;
|
|
final scene = _activeScene;
|
|
if (sessionId == null || scene == null || !AppSubScreenTool.isAvailable) {
|
|
return;
|
|
}
|
|
if (_lastViceAction == action && _lastViceMessage == message) return;
|
|
final revision = ++_revision;
|
|
// 版本在发出前即固定,避免插件不回包期间重复状态持续堆积 MethodChannel 请求。
|
|
_lastViceAction = action;
|
|
_lastViceMessage = message;
|
|
logger.d(
|
|
'|人脸识别|发送副屏状态|sessionId:$sessionId|action:${action.code}|'
|
|
'revision:$revision|',
|
|
);
|
|
try {
|
|
final delivered = await AppRuntimeTool.sendViceMessage(
|
|
action.code,
|
|
params: {
|
|
'sessionId': sessionId,
|
|
'revision': revision,
|
|
'scene': scene.name,
|
|
'message': message,
|
|
if (payableAmount != null) 'payableAmount': payableAmount,
|
|
if (totalAmount != null) 'totalAmount': totalAmount,
|
|
if (discountAmount != null) 'discountAmount': discountAmount,
|
|
},
|
|
).timeout(_viceStateTimeout);
|
|
if (!delivered) {
|
|
logger.w(
|
|
'|人脸识别|副屏状态未投递,已按尽力通知处理|'
|
|
'sessionId:$sessionId|action:${action.code}|revision:$revision|',
|
|
);
|
|
return;
|
|
}
|
|
logger.d(
|
|
'|人脸识别|副屏状态发送完成|sessionId:$sessionId|'
|
|
'action:${action.code}|revision:$revision|',
|
|
);
|
|
} on TimeoutException {
|
|
logger.w(
|
|
'|人脸识别|副屏状态发送超时,已按尽力通知处理|'
|
|
'sessionId:$sessionId|action:${action.code}|revision:$revision|'
|
|
'timeout:${_viceStateTimeout.inMilliseconds}ms|',
|
|
);
|
|
} catch (error) {
|
|
logger.w(
|
|
'|人脸识别|副屏状态同步失败|sessionId:$sessionId|'
|
|
'action:${action.code}|revision:$revision|error:$error|',
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _disposeSession() async {
|
|
await _nativeStateSubscription?.cancel();
|
|
await _faceSubscription?.cancel();
|
|
_nativeStateSubscription = null;
|
|
_faceSubscription = null;
|
|
await _eventController?.close();
|
|
_eventController = null;
|
|
_resultCompleter = null;
|
|
_activeSessionId = null;
|
|
_activeScene = null;
|
|
_revision = 0;
|
|
_nativeRevision = 0;
|
|
_resultLocked = false;
|
|
_nativeStarted = false;
|
|
_startFailureEmitted = false;
|
|
_payableAmount = null;
|
|
_totalAmount = null;
|
|
_discountAmount = null;
|
|
_lastViceAction = null;
|
|
_lastViceMessage = null;
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
unawaited(_cameraStateSubscription?.cancel());
|
|
_cameraStateSubscription = null;
|
|
unawaited(suspend());
|
|
super.onClose();
|
|
}
|
|
}
|