Commit ddc45594 authored by 张宏's avatar 张宏

cc 合并到 master

parent f0145cb5
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_usbcamera/flutter_usbcamera.dart';
import 'package:path_provider/path_provider.dart';
/// USB 摄像头服务层
///
/// 职责:
/// - 封装 [UsbCameraController],对上层提供统一的业务接口
/// - 监听设备热插拔事件,自动管理摄像头生命周期
/// - 将事件流中的拍摄完成回调转换为 Future 返回值
///
/// 涉及页面:首页监护舱 - 监控画面
class UsbCameraService {
static UsbCameraService? _instance;
final UsbCameraController _controller;
StreamSubscription<CameraEvent>? _eventSub;
// 事件广播流(供 UI 层订阅设备状态变化)
final StreamController<CameraEvent> _eventController =
StreamController<CameraEvent>.broadcast();
// 拍照/录像完成时的 Completer
Completer<String?>? _captureCompleter;
UsbCameraService._() : _controller = UsbCameraController();
/// 单例实例
static UsbCameraService get instance {
_instance ??= UsbCameraService._();
return _instance!;
}
/// USB 设备事件流(设备插拔、摄像头状态、拍摄回调等)
Stream<CameraEvent> get eventStream => _eventController.stream;
/// 当前控制器引用(供需要直接调用的场景)
UsbCameraController get controller => _controller;
/// 摄像头是否已打开
bool _isOpened = false;
bool get isOpened => _isOpened;
/// 是否正在录像
bool _isRecording = false;
bool get isRecording => _isRecording;
// ==================== 初始化与销毁 ====================
/// 初始化:注册 USB 监听,开始检测设备插拔
Future<void> initialize() async {
if (_eventSub != null) return; // 已初始化
// 监听插件事件流
_eventSub = UsbCameraController.events.listen(_handleEvent);
// 注册 USB 监听
await _controller.registerUsb();
debugPrint('[UsbCameraService] 初始化完成,USB 监听已注册');
}
/// 处理来自插件的所有事件
void _handleEvent(CameraEvent event) {
debugPrint('[UsbCameraService] 收到事件: ${event.type.name}, '
'deviceId=${event.deviceId}, state=${event.state}, path=${event.path}');
switch (event.type) {
case CameraEventType.onAttachDev:
// USB 摄像头插入 → 自动请求权限
if (event.deviceId != null) {
_controller.requestPermission(event.deviceId!);
}
break;
case CameraEventType.onDetachDev:
case CameraEventType.onDisConnectDev:
// 设备拔出或断开
_isOpened = false;
_isRecording = false;
break;
case CameraEventType.onConnectDev:
// 权限通过,自动打开摄像头
if (event.deviceId != null) {
_openCameraInternal(event.deviceId!);
}
break;
case CameraEventType.onCancelDev:
// 权限被拒绝
_isOpened = false;
break;
case CameraEventType.onCameraState:
if (event.state == 'opened') {
_isOpened = true;
} else if (event.state == 'closed' || event.state == 'error') {
_isOpened = false;
_isRecording = false;
}
break;
case CameraEventType.onCaptureBegin:
// 拍摄开始
break;
case CameraEventType.onCaptureComplete:
// 拍摄完成 → 解析 Completer
_isRecording = false;
if (_captureCompleter != null && !_captureCompleter!.isCompleted) {
_captureCompleter!.complete(event.path);
_captureCompleter = null;
}
break;
case CameraEventType.onCaptureError:
// 拍摄出错
_isRecording = false;
if (_captureCompleter != null && !_captureCompleter!.isCompleted) {
_captureCompleter!.completeError(
Exception(event.error ?? '拍摄失败'),
);
_captureCompleter = null;
}
break;
default:
break;
}
// 转发事件给所有订阅者
_eventController.add(event);
}
/// 内部打开摄像头方法
Future<void> _openCameraInternal(int deviceId) async {
try {
await _controller.openCamera(
deviceId,
request: const CameraRequest(
previewWidth: 1280,
previewHeight: 720,
renderMode: RenderMode.opengl,
previewFormat: PreviewFormat.mjpeg,
),
);
debugPrint('[UsbCameraService] 摄像头已打开: deviceId=$deviceId');
} catch (e) {
debugPrint('[UsbCameraService] 打开摄像头失败: $e');
}
}
// ==================== 设备管理 ====================
/// 获取已连接的 USB 设备列表
Future<List<UsbDevice>> getDeviceList() async {
try {
return await _controller.getDeviceList();
} catch (e) {
debugPrint('[UsbCameraService] getDeviceList error: $e');
return [];
}
}
/// 手动打开指定摄像头
Future<bool> openCamera(int deviceId, {CameraRequest? request}) async {
try {
await _controller.openCamera(
deviceId,
request: request ?? const CameraRequest(
previewWidth: 1280,
previewHeight: 720,
renderMode: RenderMode.opengl,
previewFormat: PreviewFormat.mjpeg,
),
);
return true;
} catch (e) {
debugPrint('[UsbCameraService] openCamera error: $e');
return false;
}
}
// ==================== 摄像头控制 ====================
/// 关闭摄像头
Future<void> closeCamera() async {
try {
await _controller.closeCamera();
_isOpened = false;
_isRecording = false;
} catch (e) {
debugPrint('[UsbCameraService] closeCamera error: $e');
}
}
/// 拍照
/// [customPath] 可选,指定照片保存路径;不传则自动生成
/// 返回照片文件路径
Future<String?> takePhoto({String? customPath}) async {
if (!_isOpened) {
debugPrint('[UsbCameraService] 摄像头未打开,无法拍照');
return null;
}
try {
_captureCompleter = Completer<String?>();
final path = customPath ?? await _generatePhotoPath();
final success = await _controller.captureImage(path: path);
if (!success) {
_captureCompleter = null;
return null;
}
// 等待 onCaptureComplete 事件返回路径
final result = await _captureCompleter!.future;
return result;
} catch (e) {
_captureCompleter = null;
debugPrint('[UsbCameraService] takePhoto error: $e');
return null;
}
}
/// 开始录像
/// [customPath] 可选,指定视频保存路径
Future<bool> startRecord({String? customPath}) async {
if (!_isOpened) {
debugPrint('[UsbCameraService] 摄像头未打开,无法录像');
return false;
}
try {
final path = customPath ?? await _generateVideoPath();
final success = await _controller.captureVideoStart(path: path);
if (success) {
_isRecording = true;
}
return success;
} catch (e) {
debugPrint('[UsbCameraService] startRecord error: $e');
return false;
}
}
/// 停止录像
/// 返回视频文件路径
Future<String?> stopRecord() async {
if (!_isRecording) {
debugPrint('[UsbCameraService] 当前未在录像');
return null;
}
try {
_captureCompleter = Completer<String?>();
final success = await _controller.captureVideoStop();
if (!success) {
_captureCompleter = null;
_isRecording = false;
return null;
}
// 等待 onCaptureComplete 事件返回路径
final result = await _captureCompleter!.future;
_isRecording = false;
return result;
} catch (e) {
_captureCompleter = null;
_isRecording = false;
debugPrint('[UsbCameraService] stopRecord error: $e');
return null;
}
}
// ==================== 路径生成 ====================
/// 生成照片保存路径
Future<String> _generatePhotoPath() async {
final dir = await _getCameraDirectory();
final timestamp = _timestamp();
return '${dir.path}/IMG_$timestamp.jpg';
}
/// 生成视频保存路径
Future<String> _generateVideoPath() async {
final dir = await _getCameraDirectory();
final timestamp = _timestamp();
return '${dir.path}/VID_$timestamp.mp4';
}
/// 获取 USB 摄像头存储目录
Future<Directory> _getCameraDirectory() async {
final baseDir = Platform.isAndroid
? await getExternalStorageDirectory()
: await getApplicationDocumentsDirectory();
final cameraDir = Directory('${baseDir!.path}/usb_camera');
if (!await cameraDir.exists()) {
await cameraDir.create(recursive: true);
}
return cameraDir;
}
/// 生成时间戳字符串
String _timestamp() {
final now = DateTime.now();
return '${now.year}${_pad(now.month)}${_pad(now.day)}_'
'${_pad(now.hour)}${_pad(now.minute)}${_pad(now.second)}';
}
String _pad(int value) => value.toString().padLeft(2, '0');
// ==================== 销毁 ====================
/// 释放所有资源
Future<void> dispose() async {
_captureCompleter = null;
_isOpened = false;
_isRecording = false;
await _eventSub?.cancel();
_eventSub = null;
await _eventController.close();
await _controller.dispose();
debugPrint('[UsbCameraService] 资源已释放');
}
}
This diff is collapsed.
import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/services/webrtc_service.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
enum HomeIndexStatus {
initial,
loading,
success,
failure,
}
class HomeIndexState extends Equatable {
final HomeIndexStatus status;
final bool isLoading;
final String? error;
final List<MonitoringMetricBO> metrics;
final PatientInfoBO? patientInfo;
final List<AlertInfoBO> alerts;
final List<MonitoringMenuItemBO> menuItems;
/// WebRTC 视频连接状态
final WebrtcConnectionState videoConnectionState;
/// 当前视频传输模式
final VideoStreamMode videoStreamMode;
/// 是否正在切换传输模式(用于 UI loading 状态)
final bool isSwitchingMode;
/// P2P 模式是否已连接
final bool isP2pConnected;
/// 蓝牙扫描中
final bool isBluetoothScanning;
/// 蓝牙绑定/连接处理中
final bool isBluetoothBinding;
/// 正在绑定/连接的蓝牙设备 ID
final String bindingBluetoothDeviceId;
/// 扫描到的蓝牙设备
final List<BluetoothScanDevice> bluetoothDevices;
/// 已绑定蓝牙设备 ID
final String? boundBluetoothDeviceId;
/// 已绑定蓝牙设备名称
final String? boundBluetoothDeviceName;
/// 蓝牙连接状态
final BluetoothConnectionStatus bluetoothConnectionStatus;
/// 蓝牙状态提示
final String? bluetoothMessage;
/// 最近收到的蓝牙原始 Hex 数据
final String? latestBluetoothRawHex;
/// 最近解析出的 MCU 上报数据
final McuReport? latestMcuReport;
const HomeIndexState({
this.status = HomeIndexStatus.initial,
this.isLoading = false,
this.error,
this.metrics = const [],
this.patientInfo,
this.alerts = const [],
this.menuItems = const [],
this.videoConnectionState = WebrtcConnectionState.disconnected,
this.videoStreamMode = VideoStreamMode.webrtc,
this.isSwitchingMode = false,
this.isP2pConnected = false,
this.isBluetoothScanning = false,
this.isBluetoothBinding = false,
this.bindingBluetoothDeviceId = '',
this.bluetoothDevices = const [],
this.boundBluetoothDeviceId,
this.boundBluetoothDeviceName,
this.bluetoothConnectionStatus = BluetoothConnectionStatus.disconnected,
this.bluetoothMessage,
this.latestBluetoothRawHex,
this.latestMcuReport,
});
HomeIndexState copyWith({
HomeIndexStatus? status,
bool? isLoading,
String? error,
List<MonitoringMetricBO>? metrics,
PatientInfoBO? patientInfo,
List<AlertInfoBO>? alerts,
List<MonitoringMenuItemBO>? menuItems,
WebrtcConnectionState? videoConnectionState,
VideoStreamMode? videoStreamMode,
bool? isSwitchingMode,
bool? isP2pConnected,
bool? isBluetoothScanning,
bool? isBluetoothBinding,
String? bindingBluetoothDeviceId,
List<BluetoothScanDevice>? bluetoothDevices,
String? boundBluetoothDeviceId,
String? boundBluetoothDeviceName,
BluetoothConnectionStatus? bluetoothConnectionStatus,
String? bluetoothMessage,
String? latestBluetoothRawHex,
McuReport? latestMcuReport,
bool clearBoundBluetoothDevice = false,
bool clearLatestMcuReport = false,
}) {
return HomeIndexState(
status: status ?? this.status,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
metrics: metrics ?? this.metrics,
patientInfo: patientInfo ?? this.patientInfo,
alerts: alerts ?? this.alerts,
menuItems: menuItems ?? this.menuItems,
videoConnectionState: videoConnectionState ?? this.videoConnectionState,
videoStreamMode: videoStreamMode ?? this.videoStreamMode,
isSwitchingMode: isSwitchingMode ?? this.isSwitchingMode,
isP2pConnected: isP2pConnected ?? this.isP2pConnected,
isBluetoothScanning: isBluetoothScanning ?? this.isBluetoothScanning,
isBluetoothBinding: isBluetoothBinding ?? this.isBluetoothBinding,
bindingBluetoothDeviceId:
bindingBluetoothDeviceId ?? this.bindingBluetoothDeviceId,
bluetoothDevices: bluetoothDevices ?? this.bluetoothDevices,
boundBluetoothDeviceId: clearBoundBluetoothDevice
? null
: boundBluetoothDeviceId ?? this.boundBluetoothDeviceId,
boundBluetoothDeviceName: clearBoundBluetoothDevice
? null
: boundBluetoothDeviceName ?? this.boundBluetoothDeviceName,
bluetoothConnectionStatus:
bluetoothConnectionStatus ?? this.bluetoothConnectionStatus,
bluetoothMessage: bluetoothMessage ?? this.bluetoothMessage,
latestBluetoothRawHex:
latestBluetoothRawHex ?? this.latestBluetoothRawHex,
latestMcuReport:
clearLatestMcuReport ? null : latestMcuReport ?? this.latestMcuReport,
);
}
@override
List<Object?> get props => [
status,
isLoading,
error,
metrics,
patientInfo,
alerts,
menuItems,
videoConnectionState,
videoStreamMode,
isSwitchingMode,
isP2pConnected,
isBluetoothScanning,
isBluetoothBinding,
bindingBluetoothDeviceId,
bluetoothDevices,
boundBluetoothDeviceId,
boundBluetoothDeviceName,
bluetoothConnectionStatus,
bluetoothMessage,
latestBluetoothRawHex,
latestMcuReport,
];
}
This diff is collapsed.
......@@ -50,8 +50,8 @@ dependencies:
logger: ^2.7.0
fluttertoast: ^9.0.0
permission_handler: ^11.3.1
flutter_usbcamera:
path: ./third_party/flutter_usbcamera
# flutter_usbcamera:
# path: ./third_party/flutter_usbcamera
path_provider: ^2.1.2
flutter_webrtc:
path: ./plugins/flutter_webrtc
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment