93 lines
3.2 KiB
Dart
93 lines
3.2 KiB
Dart
import 'package:camera/camera.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/services.dart';
|
||
|
||
import 'frame_analyzer.dart';
|
||
|
||
/// camera 插件封装:后摄图像流(对应 Kotlin CameraController)。
|
||
class AppCameraController {
|
||
final List<CameraDescription> cameras;
|
||
CameraController? controller;
|
||
|
||
/// 图像流回调实际触发次数(诊断用,与 analyzer 帧计数区分)
|
||
int streamCallbacks = 0;
|
||
|
||
AppCameraController._(this.cameras);
|
||
|
||
static Future<AppCameraController?> create() async {
|
||
final cameras = await availableCameras();
|
||
if (cameras.isEmpty) return null;
|
||
return AppCameraController._(cameras);
|
||
}
|
||
|
||
bool get isInitialized => controller?.value.isInitialized ?? false;
|
||
|
||
CameraController get currentController =>
|
||
controller ?? (throw StateError('camera not initialized'));
|
||
|
||
/// 图像流送达时的旋转角(传感器 → 竖屏显示所需的顺时针旋转)。
|
||
/// 与 CameraX rotationDegrees 同公式;预览本身由平台旋转,检测框 overlay
|
||
/// 用同一角度映射即可对齐。
|
||
int get rotationDegrees {
|
||
final c = controller;
|
||
if (c == null) return 0;
|
||
final deviceDegrees = switch (c.value.deviceOrientation) {
|
||
DeviceOrientation.portraitUp => 0,
|
||
DeviceOrientation.landscapeLeft => 90,
|
||
DeviceOrientation.portraitDown => 180,
|
||
DeviceOrientation.landscapeRight => 270,
|
||
};
|
||
final sensor = c.description.sensorOrientation;
|
||
final isFront =
|
||
c.description.lensDirection == CameraLensDirection.front;
|
||
final degrees = (isFront ? sensor + deviceDegrees : sensor - deviceDegrees) % 360;
|
||
return degrees < 0 ? degrees + 360 : degrees;
|
||
}
|
||
|
||
Future<void> start(FrameAnalyzer analyzer) async {
|
||
await stop();
|
||
final desc = cameras.firstWhere(
|
||
(c) => c.lensDirection == CameraLensDirection.back,
|
||
orElse: () => cameras.first);
|
||
// iOS 用默认 bgra8888(420v 在部分 iOS 版本上视频输出静默不送帧),
|
||
// Android 用 yuv420 多平面。
|
||
final fmt = defaultTargetPlatform == TargetPlatform.iOS
|
||
? ImageFormatGroup.bgra8888
|
||
: ImageFormatGroup.yuv420;
|
||
final c = CameraController(desc, ResolutionPreset.high,
|
||
enableAudio: false, imageFormatGroup: fmt);
|
||
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, rotationDegrees);
|
||
} 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;
|
||
}
|
||
}
|
||
|
||
Future<void> stop() async {
|
||
final c = controller;
|
||
if (c == null) return;
|
||
controller = null;
|
||
try {
|
||
await c.stopImageStream();
|
||
} catch (_) {}
|
||
await c.dispose();
|
||
}
|
||
}
|