Commit 9895a233 authored by 张宏's avatar 张宏

加入全屏页面

parent f7ca117d
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 摄像头控制按钮组件
///
/// 职责:
/// - 提供拍照、录像、刷新三个操作按钮
/// - 录像按钮支持录制中/停止录制两种状态切换
/// - 摄像头未就绪时按钮置灰不可操作
///
/// 涉及页面:首页监护舱页面 - 监控画面底部控制栏
class CameraControls extends StatelessWidget {
/// 是否正在录像
final bool isRecording;
/// 摄像头是否已就绪(已打开)
final bool cameraReady;
/// 是否已全屏
final bool isFullscreen;
/// 拍照回调
final VoidCallback? onTakePhoto;
/// 切换录像状态回调(开始/停止)
final VoidCallback? onToggleRecord;
/// 刷新回调
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
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildControlButton(
icon: Icons.refresh,
onPressed: onRefresh,
tooltip: '刷新画面',
enabled: true, // 刷新按钮始终可用
),
SizedBox(width: 16.w),
_buildControlButton(
icon: isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
onPressed: onToggleFullscreen,
tooltip: isFullscreen ? '退出全屏' : '全屏放大',
enabled: cameraReady,
),
// _buildControlButton(
// icon: Icons.camera_alt,
// onPressed: cameraReady ? onTakePhoto : null,
// tooltip: '拍照',
// enabled: cameraReady,
// ),
// SizedBox(width: 16.w),
// _buildControlButton(
// icon: isRecording ? Icons.stop : Icons.videocam,
// onPressed: cameraReady ? onToggleRecord : null,
// tooltip: isRecording ? '停止录像' : '开始录像',
// isActive: isRecording,
// enabled: cameraReady,
// ),
],
);
}
/// 构建单个控制按钮
Widget _buildControlButton({
required IconData icon,
VoidCallback? onPressed,
String? tooltip,
bool isActive = false,
bool enabled = true,
}) {
final effectiveOpacity = enabled ? 1.0 : 0.35;
return Tooltip(
message: tooltip ?? '',
child: GestureDetector(
onTap: enabled
? () {
HapticFeedback.lightImpact();
onPressed?.call();
}
: null,
child: Opacity(
opacity: effectiveOpacity,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isActive
? const Color(0xCCFF4444)
: 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,
),
),
),
),
);
}
}
import 'dart:async';
import 'package:flutter/material.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 'camera_controls.dart';
/// 监控画面组件 —— 集成 USB 摄像头预览画面
///
/// 职责:
/// - 通过 [UsbCameraPreview] 嵌入 USB 摄像头实时预览画面
/// - 监听 [UsbCameraService.eventStream] 处理设备热插拔和状态变化
/// - 底部控制栏:刷新、全屏按钮
///
/// 涉及页面:首页监护舱页面
class MonitoringScreen extends StatefulWidget {
final VoidCallback? onRefresh;
final VoidCallback? onSnapshot;
final VoidCallback? onRecord;
const MonitoringScreen({
super.key,
this.onRefresh,
this.onSnapshot,
this.onRecord,
});
@override
State<MonitoringScreen> createState() => _MonitoringScreenState();
}
class _MonitoringScreenState extends State<MonitoringScreen> {
final UsbCameraService _cameraService = UsbCameraService.instance;
StreamSubscription<CameraEvent>? _eventSub;
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() {
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
void dispose() {
_eventSub?.cancel();
_fullscreenEntry?.remove();
_fullscreenEntry = null;
_cameraService.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 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 UsbCameraPreview(
key: ValueKey('camera_$_cameraViewGeneration'),
);
}
/// 未连接 USB 摄像头时的占位提示
Widget _buildPlaceholder() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.videocam_off,
size: 60.w,
color: Colors.white38,
),
SizedBox(height: 16.h),
Text(
'USB 摄像头未连接',
style: TextStyle(
fontSize: 24.sp,
color: Colors.white54,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8.h),
Text(
'请插入 USB 摄像头设备',
style: TextStyle(
fontSize: 18.sp,
color: Colors.white38,
),
),
],
),
);
}
/// 拍照
Future<void> _onTakePhoto() async {
if (!_cameraReady) {
_showSnackBar('摄像头未就绪');
return;
}
final path = await _cameraService.takePhoto();
if (path != null && mounted) {
_showSnackBar('拍照成功');
}
widget.onSnapshot?.call();
}
/// 切换录像状态
Future<void> _onToggleRecord() async {
if (!_cameraReady) {
_showSnackBar('摄像头未就绪');
return;
}
if (_isRecording) {
final path = await _cameraService.stopRecord();
if (path != null && mounted) {
_showSnackBar('录像已保存');
}
setState(() => _isRecording = false);
} else {
final success = await _cameraService.startRecord();
if (success) {
setState(() => _isRecording = true);
if (mounted) _showSnackBar('开始录像');
} else {
if (mounted) _showSnackBar('开始录像失败');
}
}
widget.onRecord?.call();
}
/// 刷新画面
void _onRefresh() {
_refreshState();
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(
content: Text(message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(24.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.r),
),
),
);
}
}
/// 全屏摄像头预览浮层
///
/// 通过 [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 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
/// WebRTC 视频全屏播放页面
///
/// 全屏展示远程摄像头实时画面,点击任意位置退出全屏。
class WebrtcFullscreenPage extends StatelessWidget {
final RTCVideoRenderer renderer;
const WebrtcFullscreenPage({super.key, required this.renderer});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Stack(
fit: StackFit.expand,
children: [
// 全屏视频画面
Center(
child: RTCVideoView(
renderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
),
),
// 顶部提示栏
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.fromLTRB(20, 48, 20, 12),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.black54, Colors.transparent],
),
),
child: const Row(
children: [
Icon(Icons.fullscreen_exit, color: Colors.white70, size: 20),
SizedBox(width: 8),
Text(
'点击任意位置退出全屏',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
),
),
],
),
),
);
}
}
......@@ -3,6 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../../../../services/webrtc_service.dart';
import 'webrtc_fullscreen_page.dart';
/// WebRTC 实时视频播放组件
///
......@@ -49,6 +50,9 @@ class WebrtcVideoWidget extends StatelessWidget {
_buildVideoLayer(),
// 状态指示层
_buildStatusOverlay(),
// 全屏按钮(仅连接状态下显示)
if (connectionState == WebrtcConnectionState.connected)
_buildFullscreenButton(context),
],
),
),
......@@ -143,6 +147,36 @@ class WebrtcVideoWidget extends StatelessWidget {
);
}
/// 全屏按钮(右下角)
Widget _buildFullscreenButton(BuildContext context) {
return Positioned(
right: 12.w,
bottom: 12.h,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => WebrtcFullscreenPage(renderer: renderer),
),
);
},
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.fullscreen,
color: Colors.white,
size: 22.w,
),
),
),
);
}
/// 未连接时的占位
Widget _buildPlaceholder() {
return Center(
......
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