diff --git a/lib/common/widgets/face/face_live_camera_preview.dart b/lib/common/widgets/face/face_live_camera_preview.dart new file mode 100644 index 0000000..154fe48 --- /dev/null +++ b/lib/common/widgets/face/face_live_camera_preview.dart @@ -0,0 +1,194 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:pj_supermarket_cloud/common/models/plugs/face_recognition_session_models.dart'; +import 'package:pj_supermarket_cloud/common/utils/plugs/face_discern_plugs.dart'; +import 'package:pj_supermarket_cloud/common/widgets/face/face_camera_preview.dart'; + +/// 主副屏共用的常驻人脸摄像头预览。 +/// +/// 组件只持有当前 Flutter Engine 的 Texture 和原生状态订阅,不创建、结束或重试 +/// 业务识别 Session,因此查询结果展示的十秒内仍能保持实时画面。 +class FaceLiveCameraPreview extends StatefulWidget { + /// 创建跟随宿主页面生命周期的实时预览区域。 + const FaceLiveCameraPreview({ + super.key, + required this.statusText, + required this.statusColor, + this.scale = 1, + }); + + /// 查询状态对应的可见文案,避免原生空闲态被误解为仍在识别下一位。 + final String statusText; + + /// 查询状态对应的语义颜色,必须与文字同时表达状态。 + final Color statusColor; + + /// 宿主 Engine 自己计算的尺寸比例,默认按 1920 x 1080 设计尺寸一比一展示。 + final double scale; + + /// 创建当前 Flutter Engine 的 Texture 生命周期状态。 + @override + State createState() => _FaceLiveCameraPreviewState(); +} + +/// [FaceLiveCameraPreview] 的 Texture、原生事件和异常状态所有者。 +class _FaceLiveCameraPreviewState extends State { + /// 当前 Flutter Engine 持有的原生预览纹理。 + FaceRecognitionPreviewTexture? _texture; + + /// 最近一次原生人脸会话事件,用于方向、人脸框和设备状态渲染。 + FaceRecognitionSessionEvent? _event; + + /// 常驻原生会话事件订阅,页面销毁时必须取消。 + StreamSubscription? _eventSubscription; + + /// Texture 异步创建序号,用于释放页面销毁或重建后迟到的旧资源。 + int _textureRequestId = 0; + + /// 当前预览初始化失败信息;为空表示按 Texture 和原生状态正常展示。 + String? _previewError; + + /// 订阅原生状态并创建当前 Engine 独立的输出 Texture。 + @override + void initState() { + super.initState(); + _eventSubscription = FaceDiscernPlugs.sessionStream.listen( + _handleEvent, + onError: _handleEventError, + ); + unawaited(_loadInitialState()); + unawaited(_initializeTexture()); + } + + /// 读取挂载前已存在的热摄像头状态,避免页面刚进入时长期停留在初始化提示。 + Future _loadInitialState() async { + try { + final event = await FaceDiscernPlugs.getRecognitionState(); + // EventChannel 可能先送达真实检测帧;迟到的 MethodChannel 快照不含 faceBoxes, + // 只能用于尚未收到任何事件的首次占位,不能把已经显示的人脸框清空。 + if (mounted && _event == null) setState(() => _event = event); + } catch (_) { + // 后续 EventChannel 仍可恢复状态,首次快照失败不覆盖真实 Texture 画面。 + } + } + + /// 更新最近原生状态;致命失败后的冷重试需要替换已经失效的 Renderer 输出。 + void _handleEvent(FaceRecognitionSessionEvent event) { + if (!mounted || !_shouldAcceptEvent(event)) return; + final previousStatus = _event?.status; + final shouldReplace = + (previousStatus == FaceRecognitionSessionStatus.cold || + previousStatus == FaceRecognitionSessionStatus.failed) && + (event.status == FaceRecognitionSessionStatus.initializing || + event.status == FaceRecognitionSessionStatus.detecting); + setState(() { + _event = event; + _previewError = null; + }); + if (shouldReplace) unawaited(_initializeTexture(replace: true)); + } + + /// 按原生单调序号拒绝乱序快照,避免跨会话的旧空框覆盖较新的多人检测结果。 + bool _shouldAcceptEvent(FaceRecognitionSessionEvent event) { + final current = _event; + if (current == null) return true; + if (current.eventSequence > 0 && event.eventSequence > 0) { + if (event.eventSequence != current.eventSequence) { + return event.eventSequence > current.eventSequence; + } + // MethodChannel 快照不含框;允许相同序号的真实 EventChannel 事件补齐它。 + return current.faceBoxes.isEmpty && event.faceBoxes.isNotEmpty; + } + if (event.sessionId != current.sessionId) { + // 兼容旧版原生事件:活动识别期间不得让迟到的空闲资源快照清掉当前人脸框。 + return !(current.isActive && event.sessionId.isEmpty); + } + return event.revision >= current.revision; + } + + /// 将原生事件通道异常收敛为预览内提示,不中断主屏查询状态机。 + void _handleEventError(Object error) { + if (mounted) setState(() => _previewError = '摄像头状态连接异常,请检查设备'); + } + + /// 创建或替换预览 Texture,并释放异步迟到或已被替换的输出。 + Future _initializeTexture({bool replace = false}) async { + final requestId = ++_textureRequestId; + if (replace) { + final previous = _texture; + _texture = null; + if (previous != null) { + try { + await FaceDiscernPlugs.releasePreviewTexture(previous.textureId); + } catch (_) { + // 原生 Renderer 已冷释放时旧端点可能失效,仍要继续创建新 Texture 恢复画面。 + } + } + if (mounted) setState(() {}); + } + try { + final texture = await FaceDiscernPlugs.createPreviewTexture(); + if (!mounted || requestId != _textureRequestId) { + await FaceDiscernPlugs.releasePreviewTexture(texture.textureId); + return; + } + setState(() { + _texture = texture; + _previewError = null; + }); + } catch (_) { + // 已被新一轮 Texture 请求替代的旧异常不得覆盖新画面。 + if (mounted && requestId == _textureRequestId) { + setState(() => _previewError = '摄像头画面暂不可用,请检查设备'); + } + } + } + + /// 取消事件订阅并释放当前 Engine 的 Texture,不触碰业务识别 Session。 + @override + void dispose() { + _textureRequestId++; + unawaited(_eventSubscription?.cancel()); + final texture = _texture; + _texture = null; + if (texture != null) { + unawaited(FaceDiscernPlugs.releasePreviewTexture(texture.textureId)); + } + super.dispose(); + } + + /// 构建不会因 Session 暂停或结果展示而移除的实时画面。 + @override + Widget build(BuildContext context) { + final status = _previewError == null + ? _event?.status ?? FaceRecognitionSessionStatus.initializing + : FaceRecognitionSessionStatus.failed; + return Stack( + fit: StackFit.expand, + children: [ + FaceCameraPreview( + texture: _texture, + event: _event, + status: status, + statusText: widget.statusText, + statusColor: widget.statusColor, + scale: widget.scale, + ), + if (_previewError != null) + Center( + child: Text( + _previewError!, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 16 * widget.scale, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ); + } +}