import 'dart:typed_data'; import 'package:camera/camera.dart'; import '../detection/detection_result.dart'; import '../detection/detector_worker.dart'; import 'camera_view_model.dart'; /// 抽帧节流 + 后台推理(对应 Kotlin FrameAnalyzer)。 /// 推理在后台 isolate(DetectorWorker)执行,主 isolate 只投递帧与收结果。 class FrameAnalyzer { /// 连续检测:100ms 一帧 int intervalMs = 100; /// null = 模型加载失败,仅预览不分析 final DetectorWorker? worker; final CameraViewModel viewModel; int _lastDetectMs = 0; /// 诊断计数:推理调用/异常次数 int detectCalls = 0; int detectErrors = 0; String? lastError; /// 最近一次完整处理(预处理+推理+运动检测)耗时 ms(worker 侧) int lastProcessMs = 0; /// 图像流回调是否到达(诊断用) int framesReceived = 0; FrameAnalyzer({required this.worker, required this.viewModel}) { worker?.onResult = _onResult; worker?.onError = _onError; } 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}) { 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 (!_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(); }