import 'dart:async'; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'frame_analyzer.dart'; /// 相机抽象(对应 Kotlin CameraController): /// /// - Android:自写原生通道([NativeCameraController])。分析帧在 Kotlin 侧 /// 旋转成竖屏后经 EventChannel 回传,Flutter 侧恒 rotation=0——与 iOS /// (camera_avfoundation 插件,帧本来就是竖屏方向)行为一致,消除 /// "横屏传感器帧 → 90° 旋转 + FIT_COVER crop 映射"的标注偏移根因。 /// - iOS:camera 插件(原逻辑)。帧已竖屏,rotation 恒 0。 abstract class AppCameraController { static Future create() async { if (defaultTargetPlatform == TargetPlatform.android) { return NativeCameraController(); } return PluginCameraController.create(); } /// 分析流回调实际触发次数(诊断用,与 analyzer 帧计数区分) int get streamCallbacks; bool get isInitialized; bool get isStreaming; String? get errorDescription; /// 检测框 overlay 应使用的旋转角。两平台帧都已竖屏 → 恒 0。 int get rotationDegrees => 0; /// 诊断:传感器方向 / 显示旋转(仅 Android 原生通道上报) int? get sensorOrientation => null; int? get displayDegrees => null; /// 预览实际应用的旋转圈数(仅 Android 原生通道上报,诊断用) int get quarterTurns => -1; /// 诊断:轮询原生侧帧状态(Android 返回计数;iOS 返回空) Future> stats() async => const {}; Future start(FrameAnalyzer analyzer); Future stop(); Future getMinZoomLevel(); Future getMaxZoomLevel(); Future setZoomLevel(double value); /// 预览 widget:Android 为原生 SurfaceView(AndroidView),iOS 为插件纹理 Widget buildPreview(); } /// Android:自写原生相机通道(Kotlin CameraChannel)。 class NativeCameraController extends AppCameraController { static const MethodChannel _channel = MethodChannel('observer/camera'); static const EventChannel _frames = EventChannel('observer/camera/frames'); StreamSubscription? _sub; bool _streaming = false; String? _error; /// 原生侧注册的预览纹理(SurfaceTexture) int? _textureId; int _textureW = 1920; int _textureH = 1080; int _quarterTurns = 1; @override int streamCallbacks = 0; int? _sensorOrientation; int? _displayDegrees; @override int? get sensorOrientation => _sensorOrientation; @override int? get displayDegrees => _displayDegrees; @override int get quarterTurns => _quarterTurns; @override bool get isInitialized => _streaming; @override bool get isStreaming => _streaming; @override String? get errorDescription => _error; @override Future> stats() async { try { final r = await _channel.invokeMethod>('stats'); if (r != null) { // 旋转/显示角度随轮询实时刷新:手机旋转后预览与帧旋转都跟着变 final dd = r['displayDegrees']; if (dd is int) _displayDegrees = dd; final turns = r['quarterTurns']; if (turns is int) _quarterTurns = turns; } return r ?? const {}; } catch (e) { return {'pollErr': '$e'}; } } @override Future start(FrameAnalyzer analyzer) async { await stop(); // 先订阅再启动:原生 start 绑定后立刻推帧,避免首帧竞态 _sub = _frames.receiveBroadcastStream().listen((event) { // 计数放最前:任何到达的事件都先记账,后续解析失败也不丢计数 streamCallbacks++; try { final f = event as List; final w = f[0] as int; final h = f[1] as int; final rotation = f[2] as int; final bgra = f[3] as bool; final bytes = f[4] as Uint8List; analyzer.analyzeRaw( planes: [bytes], strides: [w * 4], width: w, height: h, isBgra: true, rgbaOrder: !bgra, rotationDegrees: rotation, ); } catch (e) { _error = '帧解析: $e'; analyzer.recordStreamError('frames parse: $e'); } }, onError: (Object e) { _error = '$e'; analyzer.recordStreamError('frames: $e'); }); analyzer.reset(); analyzer.worker?.reset(); try { final r = await _channel.invokeMethod>('start'); _textureId = r?['textureId'] as int?; _textureW = (r?['w'] as num?)?.toInt() ?? _textureW; _textureH = (r?['h'] as num?)?.toInt() ?? _textureH; _quarterTurns = (r?['quarterTurns'] as num?)?.toInt() ?? 1; _sensorOrientation = (r?['sensorOrientation'] as num?)?.toInt(); _displayDegrees = (r?['displayDegrees'] as num?)?.toInt(); } catch (e) { _error = '$e'; analyzer.recordStreamError('camera start: $e'); rethrow; } _streaming = true; } @override Future stop() async { _streaming = false; await _sub?.cancel(); _sub = null; try { await _channel.invokeMethod('stop'); } catch (_) {} } @override Future getMinZoomLevel() async { final r = await _channel.invokeMethod>('getZoomRange'); return (r != null && r.isNotEmpty ? (r[0] as num).toDouble() : 1.0); } @override Future getMaxZoomLevel() async { final r = await _channel.invokeMethod>('getZoomRange'); return (r != null && r.length > 1 ? (r[1] as num).toDouble() : 1.0); } @override Future setZoomLevel(double value) async { try { await _channel.invokeMethod('setZoom', value); } catch (_) {} } /// 预览纹理:Flutter 引擎渲染 Texture 时已自动应用 SurfaceTexture 变换矩阵 /// (传感器方向补偿,内容已转成自然方向的竖屏),因此: /// 1. 区域声明旋转后的尺寸(宽高互换)——否则横屏区域会把竖屏内容横向拉伸 /// 2. RotatedBox 只按显示旋转补偿(quarterTurns = 屏转/90,竖屏 0 / 横屏 1) /// 再 FittedBox cover(= CoordinateMapper 的 FIT_COVER 数学一致)填满全屏。 /// 不用 AndroidView+SurfaceView——平台视图会盖住 Flutter UI(诊断行/设置按钮/overlay) @override Widget buildPreview() { final id = _textureId; if (id == null) return const SizedBox.shrink(); return FittedBox( fit: BoxFit.cover, child: RotatedBox( quarterTurns: _quarterTurns % 4, child: SizedBox( width: _textureH.toDouble(), height: _textureW.toDouble(), child: Texture(textureId: id), ), ), ); } } /// iOS:camera 插件封装(原实现)。 class PluginCameraController extends AppCameraController { final List cameras; CameraController? controller; @override int streamCallbacks = 0; PluginCameraController._(this.cameras); static Future create() async { final cameras = await availableCameras(); if (cameras.isEmpty) return null; return PluginCameraController._(cameras); } @override bool get isInitialized => controller?.value.isInitialized ?? false; @override bool get isStreaming => controller?.value.isStreamingImages ?? false; @override String? get errorDescription => controller?.value.errorDescription; @override Future start(FrameAnalyzer analyzer) async { await stop(); final desc = cameras.firstWhere( (c) => c.lensDirection == CameraLensDirection.back, orElse: () => cameras.first); // iOS image stream 帧已按竖屏方向输出(无需旋转), // 与 Android 原生通道(帧原生侧旋转成竖屏)统一 rotation=0 final c = CameraController(desc, ResolutionPreset.veryHigh, enableAudio: false, imageFormatGroup: ImageFormatGroup.bgra8888); controller = c; await c.initialize(); // 相机(重新)启动后重置运动/背景参考与抽帧节流,避免旧场景残留 analyzer.reset(); analyzer.worker?.reset(); debugPrint('[camera] initialized, starting image stream'); try { await c.startImageStream((image) { streamCallbacks++; try { analyzer.analyze(image, 0); } catch (e, st) { debugPrint('[camera] analyze error: $e\n$st'); analyzer.recordStreamError('analyze: $e'); } }); debugPrint('[camera] startImageStream ok'); } catch (e, st) { debugPrint('[camera] startImageStream FAILED: $e\n$st'); analyzer.recordStreamError('startImageStream: $e'); rethrow; } } @override Future stop() async { final c = controller; if (c == null) return; controller = null; try { await c.stopImageStream(); } catch (_) {} await c.dispose(); } @override Future getMinZoomLevel() => controller!.getMinZoomLevel(); @override Future getMaxZoomLevel() => controller!.getMaxZoomLevel(); @override Future setZoomLevel(double value) => controller!.setZoomLevel(value); @override Widget buildPreview() => CameraPreview(controller!); }