11
This commit is contained in:
1 parent
2ee78a1561
commit
3f9a7df721
1 file changed
+194
@@ -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<FaceLiveCameraPreview> createState() => _FaceLiveCameraPreviewState();
|
||||
}
|
||||
|
||||
/// [FaceLiveCameraPreview] 的 Texture、原生事件和异常状态所有者。
|
||||
class _FaceLiveCameraPreviewState extends State<FaceLiveCameraPreview> {
|
||||
/// 当前 Flutter Engine 持有的原生预览纹理。
|
||||
FaceRecognitionPreviewTexture? _texture;
|
||||
|
||||
/// 最近一次原生人脸会话事件,用于方向、人脸框和设备状态渲染。
|
||||
FaceRecognitionSessionEvent? _event;
|
||||
|
||||
/// 常驻原生会话事件订阅,页面销毁时必须取消。
|
||||
StreamSubscription<FaceRecognitionSessionEvent>? _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<void> _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<void> _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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user