Commit cf1809a5 authored by 张宏's avatar 张宏

usb摄像头对接完成

parent 026b37a8
......@@ -20,6 +20,9 @@ class TcpClientCubit extends Cubit<TcpClientState> {
bool _isManualDisconnect = false;
bool _isConnecting = false;
/// Socket 数据监听的订阅,需要在 close 时显式取消
StreamSubscription<dynamic>? _socketSub;
final StreamController<Uint8List> _dataController =
StreamController.broadcast();
......@@ -65,6 +68,12 @@ class TcpClientCubit extends Cubit<TcpClientState> {
timeout: Duration(milliseconds: _timeout),
);
if (isClosed) {
_socket?.close();
_socket = null;
return false;
}
_socket!.setOption(SocketOption.tcpNoDelay, true);
_listenSocket();
......@@ -73,6 +82,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
_startHeartbeat();
return true;
} catch (e) {
if (isClosed) return false;
emit(state.copyWith(
connectionStatus: TcpConnectionStatus.error,
error: e.toString(),
......@@ -86,7 +96,8 @@ class TcpClientCubit extends Cubit<TcpClientState> {
/// 监听 Socket 数据
void _listenSocket() {
_socket!.listen(
_socketSub?.cancel();
_socketSub = _socket!.listen(
(data) {
final uint8List = Uint8List.fromList(data);
if (!_dataController.isClosed) {
......@@ -94,6 +105,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
}
},
onError: (e) {
if (isClosed) return;
emit(state.copyWith(
connectionStatus: TcpConnectionStatus.error,
error: e.toString(),
......@@ -101,7 +113,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
_tryReconnect();
},
onDone: () {
if (!_isManualDisconnect) {
if (!_isManualDisconnect && !isClosed) {
_tryReconnect();
}
},
......@@ -110,6 +122,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
/// 自动重连
void _tryReconnect() {
if (isClosed) return;
if (_isManualDisconnect) return;
if (_currentRetry >= _maxRetryCount) {
emit(state.copyWith(connectionStatus: TcpConnectionStatus.disconnected));
......@@ -121,7 +134,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
_reconnectTimer?.cancel();
_reconnectTimer = Timer(Duration(milliseconds: _retryInterval), () {
connect();
if (!isClosed) connect();
});
}
......@@ -173,14 +186,24 @@ class TcpClientCubit extends Cubit<TcpClientState> {
_isManualDisconnect = true;
_reconnectTimer?.cancel();
_heartbeatTimer?.cancel();
_socketSub?.cancel();
_socketSub = null;
_socket?.close();
_socket = null;
emit(state.copyWith(connectionStatus: TcpConnectionStatus.disconnected));
if (!isClosed) {
emit(state.copyWith(connectionStatus: TcpConnectionStatus.disconnected));
}
}
@override
Future<void> close() {
disconnect();
_isManualDisconnect = true;
_reconnectTimer?.cancel();
_heartbeatTimer?.cancel();
_socketSub?.cancel();
_socketSub = null;
_socket?.close();
_socket = null;
_dataController.close();
return super.close();
}
......
......@@ -8,6 +8,12 @@ class TcpServerCubit extends Cubit<TcpServerState> {
ServerSocket? _serverSocket;
final List<Socket> _clients = [];
/// 服务端 Socket 监听的订阅
StreamSubscription<Socket>? _serverSub;
/// 每个客户端 Socket 的订阅
final List<StreamSubscription<dynamic>> _clientSubs = [];
final StreamController<Map<String, dynamic>> _dataController =
StreamController.broadcast();
......@@ -26,9 +32,15 @@ class TcpServerCubit extends Cubit<TcpServerState> {
try {
_serverSocket =
await ServerSocket.bind(InternetAddress.anyIPv4, state.port);
if (isClosed) {
_serverSocket?.close();
_serverSocket = null;
return;
}
emit(state.copyWith(isRunning: true, port: _serverSocket!.port));
_listenClients();
} catch (e) {
if (isClosed) return;
if (e.toString().contains('already in use')) {
emit(state.copyWith(port: state.port + 1));
start();
......@@ -40,7 +52,12 @@ class TcpServerCubit extends Cubit<TcpServerState> {
/// 监听客户端连接
void _listenClients() {
_serverSocket!.listen((client) {
_serverSub?.cancel();
_serverSub = _serverSocket!.listen((client) {
if (isClosed) {
client.close();
return;
}
_clients.add(client);
emit(state.copyWith(connectedClients: _clients.length));
_handleClient(client);
......@@ -49,8 +66,9 @@ class TcpServerCubit extends Cubit<TcpServerState> {
/// 处理单个客户端数据
void _handleClient(Socket client) {
client.listen(
final sub = client.listen(
(data) {
if (_dataController.isClosed) return;
_dataController.add({
'client': client,
'address': client.remoteAddress.address,
......@@ -61,6 +79,7 @@ class TcpServerCubit extends Cubit<TcpServerState> {
onDone: () => _removeClient(client),
onError: (e) => _removeClient(client),
);
_clientSubs.add(sub);
}
/// 群发字节数据给所有客户端
......@@ -85,24 +104,35 @@ class TcpServerCubit extends Cubit<TcpServerState> {
/// 移除客户端
void _removeClient(Socket client) {
_clients.remove(client);
emit(state.copyWith(connectedClients: _clients.length));
client.destroy();
if (!isClosed) {
emit(state.copyWith(connectedClients: _clients.length));
}
}
/// 停止服务端
Future<void> stop() async {
for (var sub in _clientSubs) {
await sub.cancel();
}
_clientSubs.clear();
for (var c in _clients) {
await c.close();
}
_clients.clear();
await _serverSocket?.close();
emit(state.copyWith(isRunning: false, connectedClients: 0));
_serverSocket = null;
if (!isClosed) {
emit(state.copyWith(isRunning: false, connectedClients: 0));
}
}
@override
Future<void> close() async {
await stop();
_dataController.close();
_serverSub?.cancel();
_serverSub = null;
await _dataController.close();
return super.close();
}
}
......@@ -16,9 +16,13 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
emit(state.copyWith(isLoading: true, error: null));
try {
final metrics = await _monitoringService.getMetrics();
if (isClosed) return;
final patientInfo = await _monitoringService.getPatientInfo();
if (isClosed) return;
final alerts = await _monitoringService.getAlerts();
if (isClosed) return;
final menuItems = await _monitoringService.getMenuItems();
if (isClosed) return;
emit(state.copyWith(
isLoading: false,
......@@ -30,6 +34,7 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
error: null,
));
} catch (e) {
if (isClosed) return;
emit(state.copyWith(
isLoading: false,
status: HomeIndexStatus.failure,
......@@ -41,9 +46,13 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
Future<void> refreshData() async {
try {
final metrics = await _monitoringService.getMetrics();
if (isClosed) return;
final patientInfo = await _monitoringService.getPatientInfo();
if (isClosed) return;
final alerts = await _monitoringService.getAlerts();
if (isClosed) return;
final menuItems = await _monitoringService.getMenuItems();
if (isClosed) return;
emit(state.copyWith(
status: HomeIndexStatus.success,
......@@ -54,6 +63,7 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
error: null,
));
} catch (e) {
if (isClosed) return;
emit(state.copyWith(
status: HomeIndexStatus.failure,
error: e.toString(),
......
......@@ -17,6 +17,9 @@ class CameraControls extends StatelessWidget {
/// 摄像头是否已就绪(已打开)
final bool cameraReady;
/// 是否已全屏
final bool isFullscreen;
/// 拍照回调
final VoidCallback? onTakePhoto;
......@@ -26,13 +29,18 @@ class CameraControls extends StatelessWidget {
/// 刷新回调
final VoidCallback? onRefresh;
/// 切换全屏回调
final VoidCallback? onToggleFullscreen;
const CameraControls({
super.key,
this.isRecording = false,
this.cameraReady = false,
this.isFullscreen = false,
this.onTakePhoto,
this.onToggleRecord,
this.onRefresh,
this.onToggleFullscreen,
});
@override
......@@ -47,7 +55,12 @@ class CameraControls extends StatelessWidget {
enabled: true, // 刷新按钮始终可用
),
SizedBox(width: 16.w),
// todo 放大,全屏
_buildControlButton(
icon: isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
onPressed: onToggleFullscreen,
tooltip: isFullscreen ? '退出全屏' : '全屏放大',
enabled: cameraReady,
),
// _buildControlButton(
// icon: Icons.camera_alt,
......
......@@ -12,7 +12,7 @@ import 'camera_controls.dart';
/// 职责:
/// - 通过 [UsbCameraPreview] 嵌入 USB 摄像头实时预览画面
/// - 监听 [UsbCameraService.eventStream] 处理设备热插拔和状态变化
/// - 底部控制栏:拍照、录像、刷新按钮
/// - 底部控制栏:刷新、全屏按钮
///
/// 涉及页面:首页监护舱页面
class MonitoringScreen extends StatefulWidget {
......@@ -38,6 +38,15 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
bool _cameraReady = false;
bool _isRecording = false;
bool _isFullscreen = false;
/// 用于强制重建 PlatformView 的计数器
/// flutter_usbcamera 底层使用单例 [mCameraView] 绑定唯一 TextureView,
/// 退出全屏时 generation+1 使原始 UsbCameraPreview 以新 Key 重建,
/// 触发原生 onCameraViewCreated → mCameraView 重新指向此处的 TextureView
int _cameraViewGeneration = 0;
OverlayEntry? _fullscreenEntry;
@override
void initState() {
......@@ -128,53 +137,64 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
@override
void dispose() {
_eventSub?.cancel();
_fullscreenEntry?.remove();
_fullscreenEntry = null;
_cameraService.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xCC63A0FF), Color(0xCC0024C4)],
),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white24, width: 1),
),
child: Stack(
children: [
// 摄像头预览画面
_buildCameraPreview(),
// 摄像头未就绪时的占位提示
if (!_cameraReady) _buildPlaceholder(),
// 底部控制按钮
Positioned(
bottom: 24.h,
right: 24.w,
child: CameraControls(
isRecording: _isRecording,
cameraReady: _cameraReady,
onTakePhoto: _onTakePhoto,
onToggleRecord: _onToggleRecord,
onRefresh: _onRefresh,
// AspectRatio 放在最外层,让整个 Container 宽度贴合 16:9 视频比例,
// 避免父级 Expanded 撑宽后出现右侧空白
return ClipRRect(
borderRadius: BorderRadius.circular(20.r),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xCC63A0FF), Color(0xCC0024C4)],
),
border: Border.all(color: Colors.white24, width: 4),
),
],
child: Stack(
children: [
// 摄像头预览画面
_buildCameraPreview(),
// 摄像头未就绪时的占位提示
if (!_cameraReady) _buildPlaceholder(),
// 底部控制按钮
Positioned(
bottom: 24.h,
right: 24.w,
child: CameraControls(
isRecording: _isRecording,
cameraReady: _cameraReady,
isFullscreen: _isFullscreen,
onTakePhoto: _onTakePhoto,
onToggleRecord: _onToggleRecord,
onRefresh: _onRefresh,
onToggleFullscreen: _onToggleFullscreen,
),
),
],
),
),
),
);
}
/// 构建 USB 摄像头预览画面(通过 flutter_usbcamera 插件)
///
/// 使用 [_cameraViewGeneration] 作为 Key 的一部分:
/// 退出全屏时 generation+1 使 Widget 以新 Key 重建,
/// 对应的 PlatformView 也随之重建并触发 onCameraViewCreated
Widget _buildCameraPreview() {
return ClipRRect(
borderRadius: BorderRadius.circular(20.r),
child: const AspectRatio(
aspectRatio: 16 / 9,
child: UsbCameraPreview(),
),
return UsbCameraPreview(
key: ValueKey('camera_$_cameraViewGeneration'),
);
}
......@@ -256,6 +276,86 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
widget.onRefresh?.call();
}
// ==================== 全屏逻辑 ====================
/// 切换全屏模式
///
/// flutter_usbcamera 底层将 [mCameraView] 作为单例绑定唯一 TextureView,
/// 不能同时在两个 Widget 中渲染 [UsbCameraPreview](第二个会黑屏)。
///
/// 解决思路:关闭摄像头 → 切换 View 容器 → 重新打开摄像头。
/// - 进入全屏:关摄像头 → 插 Overlay(含 UsbCameraPreview)→ 开摄像头
/// - 退出全屏:关摄像头 → 移 Overlay → 增 generation 强制重建原始
/// PlatformView → 开摄像头
Future<void> _onToggleFullscreen() async {
if (!_cameraReady) {
_showSnackBar('摄像头未就绪');
return;
}
if (_isFullscreen) {
await _exitFullscreen();
} else {
await _enterFullscreen();
}
}
/// 进入全屏
Future<void> _enterFullscreen() async {
final deviceId = _cameraService.controller.currentDeviceId;
if (deviceId == null) return;
// 在 await 之前捕获 overlay,避免 use_build_context_synchronously
final overlay = Overlay.of(context);
// 1. 关闭摄像头,释放当前 PlatformView 的绑定
await _cameraService.closeCamera();
// 2. 插入全屏 Overlay —— 其 UsbCameraPreview 会创建新 PlatformView
// 并触发原生 onCameraViewCreated → mCameraView 指向新 TextureView
setState(() => _isFullscreen = true);
_fullscreenEntry = OverlayEntry(
builder: (_) => _FullscreenOverlay(onExit: _exitFullscreen),
);
overlay.insert(_fullscreenEntry!);
// 3. 等待 PlatformView 初始化完成
await Future.delayed(const Duration(milliseconds: 150));
// 4. 重新打开摄像头,渲染到 Overlay 中的 TextureView
await _cameraService.openCamera(deviceId);
}
/// 退出全屏
Future<void> _exitFullscreen() async {
final deviceId = _cameraService.controller.currentDeviceId;
// 1. 关闭摄像头
if (deviceId != null) {
await _cameraService.closeCamera();
}
// 2. 移除全屏 Overlay(其 PlatformView 被销毁)
_fullscreenEntry?.remove();
_fullscreenEntry = null;
// 3. 增加 generation → rebuild 时 UsbCameraPreview 的 Key 变化
// → 旧 PlatformView 被销毁,新 PlatformView 被创建
// → onCameraViewCreated → mCameraView 指向此处的 TextureView
setState(() {
_isFullscreen = false;
_cameraViewGeneration++;
});
// 4. 等待新 PlatformView 初始化完成
await Future.delayed(const Duration(milliseconds: 150));
// 5. 重新打开摄像头,渲染到原始位置的 TextureView
if (deviceId != null) {
await _cameraService.openCamera(deviceId);
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
......@@ -270,3 +370,52 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
);
}
}
/// 全屏摄像头预览浮层
///
/// 通过 [OverlayEntry] 插入到当前页面,而非通过 Navigator.push 创建新路由,
/// 避免 flutter_usbcamera 的 PlatformView 冲突。
class _FullscreenOverlay extends StatelessWidget {
final VoidCallback onExit;
const _FullscreenOverlay({required this.onExit});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.black,
child: Stack(
children: [
// 全屏摄像头画面(16:9 居中)
const Center(
child: AspectRatio(
aspectRatio: 16 / 9,
child: UsbCameraPreview(),
),
),
// 顶部退出按钮
Positioned(
top: MediaQuery.of(context).padding.top + 16.h,
left: 16.w,
child: GestureDetector(
onTap: onExit,
child: Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(24.r),
),
child: Icon(
Icons.fullscreen_exit,
color: Colors.white,
size: 28.w,
),
),
),
),
],
),
);
}
}
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:laki_icu_app/blocs/auth/auth_bloc.dart';
import 'package:laki_icu_app/blocs/auth/auth_event.dart';
......@@ -9,6 +11,8 @@ class LoginCubit extends Cubit<LoginState> {
final AuthBloc authBloc;
final StorageService storageService;
StreamSubscription<AuthState>? _authSub;
LoginCubit({
required this.authBloc,
required this.storageService,
......@@ -17,7 +21,8 @@ class LoginCubit extends Cubit<LoginState> {
}
void _listenAuthState() {
authBloc.stream.listen((authState) {
_authSub = authBloc.stream.listen((authState) {
if (isClosed) return;
if (authState is AuthFailure) {
emit(state.copyWith(isLoading: false, error: authState.error));
} else if (authState is AuthLoading) {
......@@ -54,6 +59,7 @@ class LoginCubit extends Cubit<LoginState> {
Future<void> loadRememberCredentials() async {
final credentials = await storageService.getRememberCredentials();
if (isClosed) return;
if (credentials['username'] != null) {
emit(state.copyWith(
rememberPassword: true,
......@@ -66,4 +72,10 @@ class LoginCubit extends Cubit<LoginState> {
void forgotPwd() {
// TODO: 跳转到忘记密码页面
}
@override
Future<void> close() {
_authSub?.cancel();
return super.close();
}
}
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