import 'dart:async'; import 'dart:typed_data'; import 'package:camera/camera.dart'; import '../detection/detection_result.dart'; import '../detection/detector_worker.dart'; import '../feedback/false_target_capture.dart'; import 'camera_view_model.dart'; /// 抽帧节流 + 后台推理(对应 Kotlin FrameAnalyzer)。 /// 推理在后台 isolate(DetectorWorker)执行,主 isolate 只投递帧与收结果。 class FrameAnalyzer { /// 连续检测:100ms 一帧 int intervalMs = 100; DetectorWorker? _worker; /// 当前推理 worker(null = 仅预览不分析);相机启动后可用 [attachWorker] /// 原地替换(相机帧流回调闭包捕获的是本 analyzer 对象,无需重启相机) DetectorWorker? get worker => _worker; final CameraViewModel viewModel; int _lastDetectMs = 0; /// 诊断计数:推理调用/异常次数 int detectCalls = 0; int detectErrors = 0; String? lastError; /// 最近一次完整处理(预处理+推理+运动检测)耗时 ms(worker 侧) int lastProcessMs = 0; /// 图像流回调是否到达(诊断用) int framesReceived = 0; /// 假目标上报快照:armed 时下一帧(节流前)拷贝单平面像素并交付。 /// Completer 泛型必须可空:运行时 future 类型决定 .timeout(onTimeout) 的 /// 回调签名,非空 Completer 会让 `() => null` 触发运行时子类型错误 Completer? _snapCompleter; FrameAnalyzer({DetectorWorker? worker, required this.viewModel}) { attachWorker(worker); } /// 取下一帧快照(假目标上报用;在节流判定之前捕获,~33ms 内必有帧) Future takeSnapshot() { final existing = _snapCompleter; if (existing != null && !existing.isCompleted) { return existing.future; } final c = Completer(); _snapCompleter = c; return c.future; } /// 帧到达(节流判定前):armed 则拷贝像素完成快照 void _captureIfNeeded( Uint8List plane, int bytesPerRow, int width, int height, bool bgra) { final c = _snapCompleter; if (c == null || c.isCompleted) return; _snapCompleter = null; c.complete(FrameSnapshot( plane: Uint8List.fromList(plane), bytesPerRow: bytesPerRow, width: width, height: height, bgra: bgra, )); } /// 替换推理 worker(null = 停识别仅预览)。旧 worker 在此释放;若相机已 /// 启动则新 worker 立即接管后续帧——重启相机会重新 initialize /// (iOS ~1s+)且预览闪断,原地替换即时生效(2026-09-03)。 void attachWorker(DetectorWorker? w) { if (identical(_worker, w)) return; final old = _worker; _worker = w; w?.onResult = _onResult; w?.onError = _onError; old?.dispose(); _lastDetectMs = 0; } void recordStreamError(String msg) { lastError = msg; detectErrors++; } void _onResult( List results, List motion, List novelty, int rotation, int width, int height, int processMs, String yuvDiag) { detectCalls++; lastProcessMs = processMs; viewModel.onFramesAnalyzed( results, rotation, width, height, motion, novelty, detectCalls: detectCalls, detectErrors: detectErrors, lastError: lastError, framesReceived: framesReceived, lastProcessMs: lastProcessMs, yuvDiag: yuvDiag, ); } void _onError(String msg) { detectErrors++; lastError = msg; viewModel.onFramesAnalyzed( const [], 90, 0, 0, const [], const [], detectCalls: detectCalls, detectErrors: detectErrors, lastError: lastError, framesReceived: framesReceived, lastProcessMs: lastProcessMs, ); } /// 节流与 busy 丢帧判定(两入口共用);通过后才允许投递 bool _canSend() { framesReceived++; final w = worker; if (w == null) return false; final now = DateTime.now().millisecondsSinceEpoch; if (now - _lastDetectMs < intervalMs) return false; _lastDetectMs = now; if (w.busy) return false; // 上一帧未返回则丢帧,避免在途积压 return true; } void analyze(CameraImage image, int rotationDegrees, {bool rgbaOrder = false}) { final p = image.planes.isNotEmpty ? image.planes.first : null; if (p != null) { _captureIfNeeded( p.bytes, p.bytesPerRow, image.width, image.height, !rgbaOrder); } if (!_canSend()) return; worker!.analyze(image, rotationDegrees, rgbaOrder: rgbaOrder); } /// 原生相机通道帧(Android):字节已在 Kotlin 侧旋转成竖屏,rotation=0。 /// isBgra=true + rgbaOrder 与插件路径同语义:false=BGRA(rOff=2)/true=RGBA(rOff=0) void analyzeRaw({ required List planes, required List strides, required int width, required int height, required bool isBgra, required bool rgbaOrder, int rotationDegrees = 0, }) { if (planes.isNotEmpty) { _captureIfNeeded(planes.first, strides.first, width, height, isBgra); } if (!_canSend()) return; worker!.analyzeRaw( planes: planes, strides: strides, width: width, height: height, isBgra: isBgra, rgbaOrder: rgbaOrder, rotationDegrees: rotationDegrees, ); } void reset() => _lastDetectMs = 0; void dispose() => _worker?.dispose(); }