Commit cf3b74d6 authored by 张宏's avatar 张宏

清除ndk版本错误

parent 8983fa7f
{ {
"dart.flutterSdkPath": ".fvm/versions/3.41.9", "dart.flutterSdkPath": ".fvm/versions/3.41.9",
"java.configuration.updateBuildConfiguration": "interactive" "java.configuration.updateBuildConfiguration": "disabled"
} }
\ No newline at end of file
...@@ -24,7 +24,7 @@ if (flutterVersionName == null) { ...@@ -24,7 +24,7 @@ if (flutterVersionName == null) {
android { android {
namespace "com.qialg.laki_icu_app" namespace "com.qialg.laki_icu_app"
compileSdkVersion flutter.compileSdkVersion compileSdk = 36
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
...@@ -49,6 +49,9 @@ android { ...@@ -49,6 +49,9 @@ android {
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
} }
flavorDimensions "environment" flavorDimensions "environment"
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- USB 摄像头权限 --> <!-- USB 摄像头权限 -->
<uses-feature android:name="android.hardware.usb.host" android:required="true" /> <uses-feature android:name="android.hardware.usb.host" android:required="true" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application <application
android:label="@string/app_name" android:label="@string/app_name"
......
...@@ -4,42 +4,3 @@ import io.flutter.embedding.android.FlutterActivity ...@@ -4,42 +4,3 @@ import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
} }
// package com.example.laki_icu_app
// import com.example.laki_icu_app.camera.UsbCameraPlugin
// import com.example.laki_icu_app.camera.UsbCameraViewFactory
// import io.flutter.embedding.android.FlutterActivity
// import io.flutter.embedding.engine.FlutterEngine
// import io.flutter.plugin.common.MethodChannel
// class MainActivity : FlutterActivity() {
// companion object {
// const val USB_CAMERA_CHANNEL = "usb_camera"
// const val USB_CAMERA_VIEW_TYPE = "usb_camera_view"
// }
// private var usbCameraPlugin: UsbCameraPlugin? = null
// override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
// super.configureFlutterEngine(flutterEngine)
// // 注册 PlatformViewFactory —— 用于在 Flutter 中渲染摄像头预览画面
// flutterEngine
// .platformViewsController
// .registry
// .registerViewFactory(USB_CAMERA_VIEW_TYPE, UsbCameraViewFactory())
// // 注册 MethodChannel —— 用于 Flutter 与原生层通信(拍照、录像等控制指令)
// val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, USB_CAMERA_CHANNEL)
// usbCameraPlugin = UsbCameraPlugin(this, channel)
// }
// override fun onDestroy() {
// usbCameraPlugin?.dispose()
// super.onDestroy()
// }
// }
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">智能酒店</string> <string name="app_name">Laki_icu</string>
</resources> </resources>
...@@ -23,6 +23,13 @@ rootProject.buildDir = '../build' ...@@ -23,6 +23,13 @@ rootProject.buildDir = '../build'
subprojects { subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}" project.buildDir = "${rootProject.buildDir}/${project.name}"
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
// :libnative 等第三方子模块未显式设置 ndkVersion,AGP 8.11 默认给 27,
// 与 ndk.dir (28.2) 和 :app/:jni 的 flutter.ndkVersion (28.2) 冲突。
// 在插件 apply 后立即注入 28.2,因为 :libnative 自己的 android{} 里没写 ndkVersion,不会被覆盖。
plugins.withId('com.android.library') {
android.ndkVersion = '28.2.13676358'
}
} }
tasks.register("clean", Delete) { tasks.register("clean", Delete) {
......
...@@ -24,3 +24,35 @@ plugins { ...@@ -24,3 +24,35 @@ plugins {
} }
include(":app") include(":app")
// USB Camera 插件子模块引用
// flutter_usbcamera 的 build.gradle 写死了 implementation project(':libausbc')
// 必须在根项目 settings 中注册 :libausbc, :libuvc, :libnative
// 使用字符串 indexOf 从 .flutter-plugins-dependencies 中解析路径(避免正则转义问题)
val pluginsFile = java.io.File(rootProject.projectDir.parentFile, ".flutter-plugins-dependencies")
val text = pluginsFile.readText()
// JSON 格式: {"name":"flutter_usbcamera","path":"/some/path/",...}
// 找到 "flutter_usbcamera" 然后找它后面的第一个 "path" 值
val cameraKey = "\"flutter_usbcamera\""
val cameraIdx = text.indexOf(cameraKey)
if (cameraIdx >= 0) {
// 从 cameraKey 之后找 "\"path\":\""
val afterKey = text.substring(cameraIdx + cameraKey.length)
val pathKey = "\"path\":\""
val pathIdx = afterKey.indexOf(pathKey)
if (pathIdx >= 0) {
val pathStart = pathIdx + pathKey.length
val pathEnd = afterKey.indexOf("\"", pathStart)
if (pathEnd >= 0) {
val pluginPath = afterKey.substring(pathStart, pathEnd) + "/android"
include(":libuvc")
project(":libuvc").projectDir = java.io.File(pluginPath, "libuvc")
include(":libnative")
project(":libnative").projectDir = java.io.File(pluginPath, "libnative")
include(":libausbc")
project(":libausbc").projectDir = java.io.File(pluginPath, "libausbc")
}
}
}
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>智能酒店</string> <string>Laki_icu</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
......
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter_usbcamera/flutter_usbcamera.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:path_provider/path_provider.dart';
/// USB 摄像头服务层 /// USB 摄像头服务层
/// ///
/// 职责: /// 职责:
/// - 封装 Flutter 与 Android 原生层之间的 MethodChannel 通信 /// - 封装 [UsbCameraController],对上层提供统一的业务接口
/// - 提供拍照、录像、设备查询等操作接口 /// - 监听设备热插拔事件,自动管理摄像头生命周期
/// - 处理摄像头权限申请 /// - 将事件流中的拍摄完成回调转换为 Future 返回值
/// ///
/// MethodChannel 名称: "usb_camera" /// 涉及页面:首页监护舱 - 监控画面
class UsbCameraService { class UsbCameraService {
static const _channel = MethodChannel('usb_camera'); static UsbCameraService? _instance;
final UsbCameraController _controller;
// ==================== 权限管理 ==================== StreamSubscription<CameraEvent>? _eventSub;
/// 检查并请求摄像头权限 // 事件广播流(供 UI 层订阅设备状态变化)
/// 返回 true 表示权限已授予 final StreamController<CameraEvent> _eventController =
static Future<bool> requestCameraPermission() async { StreamController<CameraEvent>.broadcast();
// iOS 需要 CAMERA 权限,Android 需要 CAMERA 权限
if (Platform.isAndroid) { // 拍照/录像完成时的 Completer
final status = await Permission.camera.request(); Completer<String?>? _captureCompleter;
return status.isGranted;
} else if (Platform.isIOS) { UsbCameraService._() : _controller = UsbCameraController();
final status = await Permission.camera.request();
return status.isGranted; /// 单例实例
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;
} }
return false;
// 转发事件给所有订阅者
_eventController.add(event);
} }
/// 检查摄像头权限是否已授予 /// 内部打开摄像头方法
static Future<bool> hasCameraPermission() async { Future<void> _openCameraInternal(int deviceId) async {
return await Permission.camera.isGranted; try {
await _controller.openCamera(
deviceId,
request: const CameraRequest(
previewWidth: 640,
previewHeight: 480,
renderMode: RenderMode.opengl,
previewFormat: PreviewFormat.mjpeg,
),
);
debugPrint('[UsbCameraService] 摄像头已打开: deviceId=$deviceId');
} catch (e) {
debugPrint('[UsbCameraService] 打开摄像头失败: $e');
}
} }
// ==================== 设备管理 ==================== // ==================== 设备管理 ====================
/// 获取已连接的 USB 摄像头设备列表 /// 获取已连接的 USB 设备列表
/// 返回 List<Map>,每个 Map 包含 deviceId、deviceName 等字段 Future<List<UsbDevice>> getDeviceList() async {
static Future<List<Map<String, dynamic>>> getDeviceList() async {
try { try {
final result = await _channel.invokeMethod('getDeviceList'); return await _controller.getDeviceList();
if (result is List) {
return result.cast<Map<dynamic, dynamic>>().map((e) {
return Map<String, dynamic>.from(e);
}).toList();
}
return [];
} catch (e) { } catch (e) {
debugPrint('UsbCameraService.getDeviceList error: $e'); debugPrint('[UsbCameraService] getDeviceList error: $e');
return []; return [];
} }
} }
// ==================== 摄像头控制 ==================== /// 手动打开指定摄像头
Future<bool> openCamera(int deviceId, {CameraRequest? request}) async {
/// 打开摄像头(由 PlatformView 创建时自动触发)
/// 通常不需要手动调用
static Future<bool> openCamera({int? deviceId}) async {
try { try {
final result = await _channel.invokeMethod('openCamera', { await _controller.openCamera(
if (deviceId != null) 'deviceId': deviceId, deviceId,
}); request: request ?? const CameraRequest(
return result == true; previewWidth: 640,
previewHeight: 480,
renderMode: RenderMode.opengl,
previewFormat: PreviewFormat.mjpeg,
),
);
return true;
} catch (e) { } catch (e) {
debugPrint('UsbCameraService.openCamera error: $e'); debugPrint('[UsbCameraService] openCamera error: $e');
return false; return false;
} }
} }
// ==================== 摄像头控制 ====================
/// 关闭摄像头 /// 关闭摄像头
static Future<bool> closeCamera() async { Future<void> closeCamera() async {
try { try {
final result = await _channel.invokeMethod('closeCamera'); await _controller.closeCamera();
return result == true; _isOpened = false;
_isRecording = false;
} catch (e) { } catch (e) {
debugPrint('UsbCameraService.closeCamera error: $e'); debugPrint('[UsbCameraService] closeCamera error: $e');
return false;
} }
} }
/// 拍照 /// 拍照
/// [path] 可选,指定照片保存路径;不传则使用默认路径 /// [customPath] 可选,指定照片保存路径;不传则自动生成
/// 返回照片文件的路径 /// 返回照片文件路径
static Future<String?> takePhoto({String? path}) async { Future<String?> takePhoto({String? customPath}) async {
if (!_isOpened) {
debugPrint('[UsbCameraService] 摄像头未打开,无法拍照');
return null;
}
try { try {
final result = await _channel.invokeMethod('takePhoto', { _captureCompleter = Completer<String?>();
if (path != null) 'path': path,
}); final path = customPath ?? await _generatePhotoPath();
return result as String?; final success = await _controller.captureImage(path: path);
if (!success) {
_captureCompleter = null;
return null;
}
// 等待 onCaptureComplete 事件返回路径
final result = await _captureCompleter!.future;
return result;
} catch (e) { } catch (e) {
debugPrint('UsbCameraService.takePhoto error: $e'); _captureCompleter = null;
debugPrint('[UsbCameraService] takePhoto error: $e');
return null; return null;
} }
} }
/// 开始录像 /// 开始录像
/// [path] 可选,指定视频保存路径;不传则使用默认路径 /// [customPath] 可选,指定视频保存路径
static Future<bool> startRecord({String? path}) async { Future<bool> startRecord({String? customPath}) async {
if (!_isOpened) {
debugPrint('[UsbCameraService] 摄像头未打开,无法录像');
return false;
}
try { try {
final result = await _channel.invokeMethod('startRecord', { final path = customPath ?? await _generateVideoPath();
if (path != null) 'path': path, final success = await _controller.captureVideoStart(path: path);
}); if (success) {
return result == true; _isRecording = true;
}
return success;
} catch (e) { } catch (e) {
debugPrint('UsbCameraService.startRecord error: $e'); debugPrint('[UsbCameraService] startRecord error: $e');
return false; return false;
} }
} }
/// 停止录像 /// 停止录像
/// 返回视频文件的路径 /// 返回视频文件路径
static Future<String?> stopRecord() async { Future<String?> stopRecord() async {
try { if (!_isRecording) {
final result = await _channel.invokeMethod('stopRecord'); debugPrint('[UsbCameraService] 当前未在录像');
return result as String?;
} catch (e) {
debugPrint('UsbCameraService.stopRecord error: $e');
return null; return null;
} }
}
/// 查询摄像头是否已打开
static Future<bool> isCameraOpened() async {
try { try {
final result = await _channel.invokeMethod('isCameraOpened'); _captureCompleter = Completer<String?>();
return result == true;
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) { } catch (e) {
debugPrint('UsbCameraService.isCameraOpened error: $e'); _captureCompleter = null;
return false; _isRecording = false;
debugPrint('[UsbCameraService] stopRecord error: $e');
return null;
} }
} }
/// 设置预览分辨率 // ==================== 路径生成 ====================
static Future<bool> setPreviewSize(int width, int height) async {
try { /// 生成照片保存路径
final result = await _channel.invokeMethod('setPreviewSize', { Future<String> _generatePhotoPath() async {
'width': width, final dir = await _getCameraDirectory();
'height': height, final timestamp = _timestamp();
}); return '${dir.path}/IMG_$timestamp.jpg';
return result == true; }
} catch (e) {
debugPrint('UsbCameraService.setPreviewSize error: $e'); /// 生成视频保存路径
return false; 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] 资源已释放');
} }
} }
...@@ -7,12 +7,16 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; ...@@ -7,12 +7,16 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 职责: /// 职责:
/// - 提供拍照、录像、刷新三个操作按钮 /// - 提供拍照、录像、刷新三个操作按钮
/// - 录像按钮支持录制中/停止录制两种状态切换 /// - 录像按钮支持录制中/停止录制两种状态切换
/// - 摄像头未就绪时按钮置灰不可操作
/// ///
/// 涉及页面:首页监护舱页面 - 监控画面底部控制栏 /// 涉及页面:首页监护舱页面 - 监控画面底部控制栏
class CameraControls extends StatelessWidget { class CameraControls extends StatelessWidget {
/// 是否正在录像 /// 是否正在录像
final bool isRecording; final bool isRecording;
/// 摄像头是否已就绪(已打开)
final bool cameraReady;
/// 拍照回调 /// 拍照回调
final VoidCallback? onTakePhoto; final VoidCallback? onTakePhoto;
...@@ -25,6 +29,7 @@ class CameraControls extends StatelessWidget { ...@@ -25,6 +29,7 @@ class CameraControls extends StatelessWidget {
const CameraControls({ const CameraControls({
super.key, super.key,
this.isRecording = false, this.isRecording = false,
this.cameraReady = false,
this.onTakePhoto, this.onTakePhoto,
this.onToggleRecord, this.onToggleRecord,
this.onRefresh, this.onRefresh,
...@@ -39,19 +44,22 @@ class CameraControls extends StatelessWidget { ...@@ -39,19 +44,22 @@ class CameraControls extends StatelessWidget {
icon: Icons.refresh, icon: Icons.refresh,
onPressed: onRefresh, onPressed: onRefresh,
tooltip: '刷新画面', tooltip: '刷新画面',
enabled: true, // 刷新按钮始终可用
), ),
SizedBox(width: 16.w), SizedBox(width: 16.w),
_buildControlButton( _buildControlButton(
icon: Icons.camera_alt, icon: Icons.camera_alt,
onPressed: onTakePhoto, onPressed: cameraReady ? onTakePhoto : null,
tooltip: '拍照', tooltip: '拍照',
enabled: cameraReady,
), ),
SizedBox(width: 16.w), SizedBox(width: 16.w),
_buildControlButton( _buildControlButton(
icon: isRecording ? Icons.stop : Icons.videocam, icon: isRecording ? Icons.stop : Icons.videocam,
onPressed: onToggleRecord, onPressed: cameraReady ? onToggleRecord : null,
tooltip: isRecording ? '停止录像' : '开始录像', tooltip: isRecording ? '停止录像' : '开始录像',
isActive: isRecording, isActive: isRecording,
enabled: cameraReady,
), ),
], ],
); );
...@@ -63,41 +71,49 @@ class CameraControls extends StatelessWidget { ...@@ -63,41 +71,49 @@ class CameraControls extends StatelessWidget {
VoidCallback? onPressed, VoidCallback? onPressed,
String? tooltip, String? tooltip,
bool isActive = false, bool isActive = false,
bool enabled = true,
}) { }) {
final effectiveOpacity = enabled ? 1.0 : 0.35;
return Tooltip( return Tooltip(
message: tooltip ?? '', message: tooltip ?? '',
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: enabled
HapticFeedback.lightImpact(); ? () {
onPressed?.call(); HapticFeedback.lightImpact();
}, onPressed?.call();
child: AnimatedContainer( }
duration: const Duration(milliseconds: 200), : null,
width: 60.w, child: Opacity(
height: 60.w, opacity: effectiveOpacity,
decoration: BoxDecoration( child: AnimatedContainer(
color: isActive duration: const Duration(milliseconds: 200),
? const Color(0xCCFF4444) width: 60.w,
: const Color(0x331E10B6), height: 60.w,
borderRadius: BorderRadius.circular(30.r), decoration: BoxDecoration(
border: Border.all( color: isActive
color: isActive ? Colors.redAccent : Colors.white24, ? const Color(0xCCFF4444)
width: 1, : const Color(0x331E10B6),
borderRadius: BorderRadius.circular(30.r),
border: Border.all(
color: isActive ? Colors.redAccent : Colors.white24,
width: 1,
),
boxShadow: isActive
? [
BoxShadow(
color: Colors.redAccent.withValues(alpha: 0.4),
blurRadius: 12.r,
spreadRadius: 2,
),
]
: null,
),
child: Icon(
icon,
color: Colors.white,
size: 32.w,
), ),
boxShadow: isActive
? [
BoxShadow(
color: Colors.redAccent.withValues(alpha: 0.4),
blurRadius: 12.r,
spreadRadius: 2,
),
]
: null,
),
child: Icon(
icon,
color: Colors.white,
size: 32.w,
), ),
), ),
), ),
......
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_usbcamera/flutter_usbcamera.dart';
import 'package:laki_icu_app/services/usb_camera_service.dart'; import 'package:laki_icu_app/services/usb_camera_service.dart';
import 'camera_controls.dart'; import 'camera_controls.dart';
...@@ -8,9 +10,9 @@ import 'camera_controls.dart'; ...@@ -8,9 +10,9 @@ import 'camera_controls.dart';
/// 监控画面组件 —— 集成 USB 摄像头预览画面 /// 监控画面组件 —— 集成 USB 摄像头预览画面
/// ///
/// 职责: /// 职责:
/// - 通过 AndroidView 嵌入 Android 原生 USBCameraTextureView 预览画面 /// - 通过 [UsbCameraPreview] 嵌入 USB 摄像头实时预览画面
/// - 监听 [UsbCameraService.eventStream] 处理设备热插拔和状态变化
/// - 底部控制栏:拍照、录像、刷新按钮 /// - 底部控制栏:拍照、录像、刷新按钮
/// - 通过 [UsbCameraService] 与原生层通信
/// ///
/// 涉及页面:首页监护舱页面 /// 涉及页面:首页监护舱页面
class MonitoringScreen extends StatefulWidget { class MonitoringScreen extends StatefulWidget {
...@@ -30,13 +32,103 @@ class MonitoringScreen extends StatefulWidget { ...@@ -30,13 +32,103 @@ class MonitoringScreen extends StatefulWidget {
} }
class _MonitoringScreenState extends State<MonitoringScreen> { class _MonitoringScreenState extends State<MonitoringScreen> {
bool _isRecording = false; final UsbCameraService _cameraService = UsbCameraService.instance;
StreamSubscription<CameraEvent>? _eventSub;
bool _cameraReady = false; bool _cameraReady = false;
bool _isRecording = false;
@override
void initState() {
super.initState();
_initCamera();
}
Future<void> _initCamera() async {
// 监听摄像头事件流
_eventSub = _cameraService.eventStream.listen(_onCameraEvent);
// 初始化 USB 监听
await _cameraService.initialize();
// 检查是否已有设备连接
_refreshState();
}
void _onCameraEvent(CameraEvent event) {
if (!mounted) return;
switch (event.type) {
case CameraEventType.onAttachDev:
// 设备插入
setState(() {});
break;
case CameraEventType.onDetachDev:
case CameraEventType.onDisConnectDev:
// 设备拔出
setState(() {
_cameraReady = false;
_isRecording = false;
});
break;
case CameraEventType.onConnectDev:
// 权限通过,摄像头即将打开
setState(() {});
break;
case CameraEventType.onCancelDev:
// 权限被拒绝
setState(() {
_cameraReady = false;
});
break;
case CameraEventType.onCameraState:
if (event.state == 'opened') {
setState(() => _cameraReady = true);
} else if (event.state == 'closed' || event.state == 'error') {
setState(() {
_cameraReady = false;
_isRecording = false;
});
}
break;
case CameraEventType.onCaptureComplete:
setState(() => _isRecording = false);
if (event.path != null && mounted) {
final isVideo = event.captureType == 'video' ||
(event.path?.endsWith('.mp4') ?? false);
_showSnackBar(isVideo ? '录像已保存' : '拍照成功');
}
break;
case CameraEventType.onCaptureError:
setState(() => _isRecording = false);
if (mounted) {
_showSnackBar('拍摄失败: ${event.error ?? '未知错误'}');
}
break;
default:
break;
}
}
void _refreshState() {
setState(() {
_cameraReady = _cameraService.isOpened;
_isRecording = _cameraService.isRecording;
});
}
@override @override
void dispose() { void dispose() {
// 退出页面时关闭摄像头 _eventSub?.cancel();
UsbCameraService.closeCamera(); _cameraService.dispose();
super.dispose(); super.dispose();
} }
...@@ -56,7 +148,7 @@ class _MonitoringScreenState extends State<MonitoringScreen> { ...@@ -56,7 +148,7 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
children: [ children: [
// 摄像头预览画面 // 摄像头预览画面
_buildCameraPreview(), _buildCameraPreview(),
// 摄像头未就绪时的提示 // 摄像头未就绪时的占位提示
if (!_cameraReady) _buildPlaceholder(), if (!_cameraReady) _buildPlaceholder(),
// 底部控制按钮 // 底部控制按钮
Positioned( Positioned(
...@@ -64,9 +156,10 @@ class _MonitoringScreenState extends State<MonitoringScreen> { ...@@ -64,9 +156,10 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
right: 24.w, right: 24.w,
child: CameraControls( child: CameraControls(
isRecording: _isRecording, isRecording: _isRecording,
cameraReady: _cameraReady,
onTakePhoto: _onTakePhoto, onTakePhoto: _onTakePhoto,
onToggleRecord: _onToggleRecord, onToggleRecord: _onToggleRecord,
onRefresh: widget.onRefresh, onRefresh: _onRefresh,
), ),
), ),
], ],
...@@ -74,22 +167,11 @@ class _MonitoringScreenState extends State<MonitoringScreen> { ...@@ -74,22 +167,11 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
); );
} }
/// 构建 USB 摄像头预览画面(通过 PlatformView /// 构建 USB 摄像头预览画面(通过 flutter_usbcamera 插件
Widget _buildCameraPreview() { Widget _buildCameraPreview() {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(20.r), borderRadius: BorderRadius.circular(20.r),
child: SizedBox.expand( child: const UsbCameraPreview(),
child: AndroidView(
viewType: 'usb_camera_view',
onPlatformViewCreated: (_) {
setState(() {
_cameraReady = true;
});
},
creationParams: const <String, dynamic>{},
creationParamsCodec: const StandardMessageCodec(),
),
),
); );
} }
...@@ -128,31 +210,49 @@ class _MonitoringScreenState extends State<MonitoringScreen> { ...@@ -128,31 +210,49 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
/// 拍照 /// 拍照
Future<void> _onTakePhoto() async { Future<void> _onTakePhoto() async {
final path = await UsbCameraService.takePhoto(); if (!_cameraReady) {
_showSnackBar('摄像头未就绪');
return;
}
final path = await _cameraService.takePhoto();
if (path != null && mounted) { if (path != null && mounted) {
_showSnackBar('拍照成功: $path'); _showSnackBar('拍照成功');
} }
widget.onSnapshot?.call(); widget.onSnapshot?.call();
} }
/// 切换录像状态 /// 切换录像状态
Future<void> _onToggleRecord() async { Future<void> _onToggleRecord() async {
if (!_cameraReady) {
_showSnackBar('摄像头未就绪');
return;
}
if (_isRecording) { if (_isRecording) {
final path = await UsbCameraService.stopRecord(); final path = await _cameraService.stopRecord();
if (path != null && mounted) { if (path != null && mounted) {
_showSnackBar('录像已保存: $path'); _showSnackBar('录像已保存');
} }
setState(() => _isRecording = false); setState(() => _isRecording = false);
} else { } else {
final success = await UsbCameraService.startRecord(); final success = await _cameraService.startRecord();
if (success) { if (success) {
setState(() => _isRecording = true); setState(() => _isRecording = true);
if (mounted) _showSnackBar('开始录像'); if (mounted) _showSnackBar('开始录像');
} else {
if (mounted) _showSnackBar('开始录像失败');
} }
} }
widget.onRecord?.call(); widget.onRecord?.call();
} }
/// 刷新画面
void _onRefresh() {
_refreshState();
widget.onRefresh?.call();
}
void _showSnackBar(String message) { void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
......
...@@ -102,7 +102,7 @@ class LoginView extends StatelessWidget { ...@@ -102,7 +102,7 @@ class LoginView extends StatelessWidget {
), ),
SizedBox(height: 40.h), SizedBox(height: 40.h),
const Text( const Text(
'智能酒店管理系统', 'Laki icu 智能仓',
style: TextStyle( style: TextStyle(
color: Color(0xFF333333), color: Color(0xFF333333),
fontSize: 24, fontSize: 24,
......
...@@ -51,6 +51,7 @@ dependencies: ...@@ -51,6 +51,7 @@ dependencies:
fluttertoast: ^9.0.0 fluttertoast: ^9.0.0
permission_handler: ^11.3.1 permission_handler: ^11.3.1
flutter_usbcamera: ^0.0.1 flutter_usbcamera: ^0.0.1
path_provider: ^2.1.2
# flutter_usbcamera: ^0.0.1 # flutter_usbcamera: ^0.0.1
# fl_chart: ^0.71.0 # fl_chart: ^0.71.0
# fl_chart: ^1.1.0 # fl_chart: ^1.1.0
......
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