# 无人机实时视频接口使用示例 ## 快速开始 本指南展示如何在你的项目中快速集成无人机实时视频功能。 ## 1. 基础用法 - 直接导航到视频页面 最简单的方式是直接导航到 `UavLiveVideoPage`: ```dart import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/uav_live_video_page.dart'; // 在任意地方调用 void _openVideo() { Navigator.push( context, MaterialPageRoute( builder: (context) => UavLiveVideoPage( droneSn: '1581F8HGX253U00A063U', // 无人机序列号 cameraIndex: '176-0-0', // 摄像头编号 ), ), ); } ``` ## 2. 高级用法 - 在自定义页面中集成 如果你需要在自己的页面中集成视频功能,可以按照以下步骤操作: ### 步骤 1: 创建页面并初始化 BLoC ```dart import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../../core/di/injection.dart'; import '../../domain/entities/uav_video_stream_entity.dart'; import '../bloc/drone_station_bloc.dart'; import '../bloc/drone_station_event.dart'; import '../bloc/drone_station_state.dart'; class MyCustomVideoPage extends StatefulWidget { final String droneSn; final String cameraIndex; const MyCustomVideoPage({ super.key, required this.droneSn, required this.cameraIndex, }); @override State createState() => _MyCustomVideoPageState(); } class _MyCustomVideoPageState extends State { late DroneStationBloc _bloc; UavVideoStreamEntity? _videoStream; bool _isLoading = false; String? _errorMessage; UavLensType? _currentLensType; @override void initState() { super.initState(); _bloc = sl(); // 默认加载广角镜头 _loadVideoStream(UavLensType.wide); } @override void dispose() { _bloc.close(); super.dispose(); } } ``` ### 步骤 2: 实现加载视频流方法 ```dart /// 加载视频流 void _loadVideoStream(UavLensType lensType) { setState(() { _isLoading = true; _errorMessage = null; _currentLensType = lensType; }); _bloc.add( UavVideoStreamLoad( sn: widget.droneSn, cameraIndex: widget.cameraIndex, lensType: lensType, qualityType: VideoQualityType.adaptive, // 自适应清晰度 videoExpire: 720000000, // Token有效期(毫秒) ), ); } ``` ### 步骤 3: 构建 UI 并监听状态变化 ```dart @override Widget build(BuildContext context) { return BlocProvider.value( value: _bloc, child: Scaffold( backgroundColor: Colors.black, appBar: AppBar( backgroundColor: Colors.black, elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), title: const Text( '无人机实时视频', style: TextStyle(color: Colors.white), ), centerTitle: true, actions: [ // 镜头切换按钮 PopupMenuButton( icon: const Icon(Icons.videocam, color: Colors.white), tooltip: '切换镜头', onSelected: (lensType) { _loadVideoStream(lensType); }, itemBuilder: (context) => [ const PopupMenuItem( value: UavLensType.wide, child: Row( children: [ Icon(Icons.videocam, size: 20), SizedBox(width: 8), Text('广角镜头'), ], ), ), const PopupMenuItem( value: UavLensType.zoom, child: Row( children: [ Icon(Icons.zoom_in, size: 20), SizedBox(width: 8), Text('变焦镜头'), ], ), ), const PopupMenuItem( value: UavLensType.ir, child: Row( children: [ Icon(Icons.thermostat, size: 20), SizedBox(width: 8), Text('红外镜头'), ], ), ), ], ), ], ), body: BlocConsumer( listener: (context, state) { // 监听状态变化 if (state is UavVideoStreamLoaded) { setState(() { _videoStream = state.videoStream; _isLoading = false; }); debugPrint('=== 视频流加载成功 ==='); debugPrint('URL Type: ${state.videoStream.urlType}'); debugPrint('AppId: ${state.videoStream.appId}'); debugPrint('RoomId: ${state.videoStream.roomId}'); debugPrint('UserId: ${state.videoStream.userId}'); // TODO: 这里可以初始化 RTC 引擎并显示视频 _initRtcEngine(state.videoStream); } else if (state is UavVideoStreamError) { setState(() { _errorMessage = state.message; _isLoading = false; }); } }, builder: (context, state) { // 根据状态渲染不同的 UI // 加载中状态 if (_isLoading || state is UavVideoStreamLoading) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: Colors.white), SizedBox(height: 16), Text( '正在加载视频流...', style: TextStyle(color: Colors.white), ), ], ), ); } // 错误状态 if (_errorMessage != null || state is UavVideoStreamError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline, size: 48, color: Colors.red, ), const SizedBox(height: 16), Text( _errorMessage ?? (state as UavVideoStreamError).message, style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton( onPressed: () { if (_currentLensType != null) { _loadVideoStream(_currentLensType!); } }, child: const Text('重试'), ), ], ), ); } // 无视频信号 if (_videoStream == null) { return const Center( child: Text( '暂无视频信号', style: TextStyle(color: Colors.white), ), ); } // 视频已加载,显示视频画面 return Container( width: double.infinity, height: double.infinity, color: Colors.black, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.videocam_off, size: 64, color: Colors.grey, ), const SizedBox(height: 16), Text( '视频流已获取\nURL Type: ${_videoStream!.urlType}\nCamera: $_currentLensType', style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, ), const SizedBox(height: 16), Text( '请集成 RTC SDK 后在此处显示视频画面', style: TextStyle( color: Colors.grey[600], fontSize: 12, ), ), ], ), ), ); }, ), ), ); } ``` ### 步骤 4: 初始化 RTC 引擎(可选) ```dart /// 初始化 RTC 引擎 Future _initRtcEngine(UavVideoStreamEntity videoStream) async { final appId = videoStream.appId; final roomId = videoStream.roomId; final token = videoStream.token; final userId = videoStream.userId.isNotEmpty ? videoStream.userId : 'user_${DateTime.now().millisecondsSinceEpoch}'; if (appId.isEmpty || roomId.isEmpty || token.isEmpty) { debugPrint('RTC 参数缺失'); return; } debugPrint('=== RTC 初始化 ==='); debugPrint('AppId: $appId'); debugPrint('RoomId: $roomId'); debugPrint('UserId: $userId'); debugPrint('URL Type: ${videoStream.urlType}'); // 根据 urlType 选择不同的 RTC SDK final sdkType = videoStream.urlType.toLowerCase() == 'agora' ? RtcSdkType.agora : RtcSdkType.volcengine; if (sdkType == RtcSdkType.agora) { await _initAgoraEngine(appId, roomId, token, userId); } else { await _initVolcEngine(appId, roomId, token, userId); } } // TODO: 实现具体的 RTC 引擎初始化逻辑 // 参考 drone_station_detail_page.dart 中的实现 ``` ## 3. 常见场景示例 ### 场景 1: 从列表页跳转到视频页 ```dart // 在设备列表中点击某个设备 void _onDeviceTap(Device device) { Navigator.push( context, MaterialPageRoute( builder: (context) => UavLiveVideoPage( droneSn: device.sn, cameraIndex: device.cameraIndex, ), ), ); } ``` ### 场景 2: 支持多个摄像头切换 ```dart class MultiCameraVideoPage extends StatefulWidget { final String droneSn; final List cameraIndices; const MultiCameraVideoPage({ super.key, required this.droneSn, required this.cameraIndices, }); @override State createState() => _MultiCameraVideoPageState(); } class _MultiCameraVideoPageState extends State { late DroneStationBloc _bloc; int _currentCameraIndex = 0; @override void initState() { super.initState(); _bloc = sl(); _loadCurrentCamera(); } void _loadCurrentCamera() { _bloc.add( UavVideoStreamLoad( sn: widget.droneSn, cameraIndex: widget.cameraIndices[_currentCameraIndex], lensType: UavLensType.wide, ), ); } void _switchCamera(int index) { setState(() { _currentCameraIndex = index; }); _loadCurrentCamera(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('摄像头 ${_currentCameraIndex + 1}/${widget.cameraIndices.length}'), actions: [ // 切换摄像头按钮 IconButton( icon: const Icon(Icons.switch_camera), onPressed: () { final nextIndex = (_currentCameraIndex + 1) % widget.cameraIndices.length; _switchCamera(nextIndex); }, ), ], ), body: BlocBuilder( builder: (context, state) { // ... 根据状态渲染UI }, ), ); } } ``` ### 场景 3: 自动刷新视频流(Token 过期时) ```dart class AutoRefreshVideoPage extends StatefulWidget { final String droneSn; final String cameraIndex; const AutoRefreshVideoPage({ super.key, required this.droneSn, required this.cameraIndex, }); @override State createState() => _AutoRefreshVideoPageState(); } class _AutoRefreshVideoPageState extends State { late DroneStationBloc _bloc; Timer? _refreshTimer; static const _tokenRefreshInterval = Duration(minutes: 55); // 每55分钟刷新一次(Token有效期约1小时) @override void initState() { super.initState(); _bloc = sl(); _loadVideoStream(); // 启动定时刷新 _startAutoRefresh(); } void _startAutoRefresh() { _refreshTimer?.cancel(); _refreshTimer = Timer.periodic(_tokenRefreshInterval, (timer) { debugPrint('⏰ 自动刷新视频流 Token'); _loadVideoStream(); }); } void _loadVideoStream() { _bloc.add( UavVideoStreamLoad( sn: widget.droneSn, cameraIndex: widget.cameraIndex, lensType: UavLensType.wide, ), ); } @override void dispose() { _refreshTimer?.cancel(); _bloc.close(); super.dispose(); } } ``` ## 4. 最佳实践 ### ✅ 推荐做法 1. **始终检查错误状态** ```dart if (state is UavVideoStreamError) { // 显示友好的错误提示 showSnackBar(context, '视频加载失败: ${state.message}'); } ``` 2. **提供重试机制** ```dart ElevatedButton( onPressed: () => _loadVideoStream(_currentLensType!), child: const Text('重试'), ) ``` 3. **记录关键日志** ```dart debugPrint('视频流加载成功: URL Type=${videoStream.urlType}'); ``` 4. **合理设置 Token 有效期** ```dart videoExpire: 720000000, // 约8天,避免频繁刷新 ``` 5. **及时释放资源** ```dart @override void dispose() { _bloc.close(); super.dispose(); } ``` ### ❌ 避免的做法 1. **不要在页面外直接调用 API** ```dart // 错误:绕过 BLoC 直接调用 final result = await repository.getUavVideoStream(...); // ✅ 正确:通过 BLoC 管理状态 _bloc.add(UavVideoStreamLoad(...)); ``` 2. **不要忘记处理加载状态** ```dart // ❌ 错误:没有加载指示器 if (state is UavVideoStreamLoaded) { ... } // ✅ 正确:显示加载状态 if (state is UavVideoStreamLoading) { return CircularProgressIndicator(); } ``` 3. **不要硬编码参数** ```dart // 错误:硬编码 sn: '1581F8HGX253U00A063U', // ✅ 正确:使用变量 sn: widget.droneSn, ``` ## 5. 故障排查 ### 问题 1: 视频流加载失败 **可能原因:** - 网络问题 - 设备序列号错误 - 摄像头编号错误 - Token 过期 **解决方案:** 1. 检查网络连接 2. 验证 `droneSn` 和 `cameraIndex` 是否正确 3. 查看控制台日志输出 4. 尝试重新加载 ### 问题 2: 视频画面不显示 **可能原因:** - RTC SDK 未正确初始化 - RTC 参数解析失败 - SDK 版本不兼容 **解决方案:** 1. 检查 `urlType` 字段,确认使用正确的 SDK 2. 验证 AppId、RoomId、Token、UserId 是否正确解析 3. 查看 RTC SDK 的日志输出 4. 参考 `drone_station_detail_page.dart` 中的实现 ### 问题 3: 镜头切换无效 **可能原因:** - 后端不支持该镜头类型 - 摄像头不支持指定的镜头 **解决方案:** 1. 检查后端返回的错误信息 2. 尝试其他镜头类型 3. 联系后端确认支持的镜头类型 ## 6. 扩展功能 ### 添加视频录制功能 ```dart // TODO: 集成 RTC SDK 的录制功能 Future _startRecording() async { // 根据使用的 RTC SDK 调用相应的录制 API } Future _stopRecording() async { // 停止录制并保存文件 } ``` ### 添加截图功能 ```dart // TODO: 集成 RTC SDK 的截图功能 Future _takeScreenshot() async { // 根据使用的 RTC SDK 调用相应的截图 API // 保存截图到相册 } ``` ### 添加视频质量切换 ```dart void _changeQuality(VideoQualityType quality) { _bloc.add( UavVideoStreamLoad( sn: widget.droneSn, cameraIndex: widget.cameraIndex, lensType: _currentLensType, qualityType: quality, // 切换清晰度 ), ); } ``` ## 7. 相关文档 - [完整集成指南](./UAV_VIDEO_INTEGRATION_GUIDE.md) - [API 接口文档](../lib/core/consts/http_api_consts.dart) - [实体类定义](../lib/features/v2/device_list/domain/entities/uav_video_stream_entity.dart) - [BLoC 状态管理](../lib/features/v2/device_list/presentation/bloc/) ## 8. 技术支持 如遇到问题,请: 1. 查看控制台日志输出 2. 参考示例代码 `uav_live_video_page.dart` 3. 查阅集成指南文档 4. 联系开发团队