一次大的提交
This commit is contained in:
642
docs/UAV_VIDEO_USAGE_EXAMPLES.md
Normal file
642
docs/UAV_VIDEO_USAGE_EXAMPLES.md
Normal file
@@ -0,0 +1,642 @@
|
||||
# 无人机实时视频接口使用示例
|
||||
|
||||
## 快速开始
|
||||
|
||||
本指南展示如何在你的项目中快速集成无人机实时视频功能。
|
||||
|
||||
## 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<MyCustomVideoPage> createState() => _MyCustomVideoPageState();
|
||||
}
|
||||
|
||||
class _MyCustomVideoPageState extends State<MyCustomVideoPage> {
|
||||
late DroneStationBloc _bloc;
|
||||
UavVideoStreamEntity? _videoStream;
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
UavLensType? _currentLensType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
// 默认加载广角镜头
|
||||
_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<UavLensType>(
|
||||
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<DroneStationBloc, DroneStationState>(
|
||||
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<void> _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<String> cameraIndices;
|
||||
|
||||
const MultiCameraVideoPage({
|
||||
super.key,
|
||||
required this.droneSn,
|
||||
required this.cameraIndices,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MultiCameraVideoPage> createState() => _MultiCameraVideoPageState();
|
||||
}
|
||||
|
||||
class _MultiCameraVideoPageState extends State<MultiCameraVideoPage> {
|
||||
late DroneStationBloc _bloc;
|
||||
int _currentCameraIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
_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<DroneStationBloc, DroneStationState>(
|
||||
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<AutoRefreshVideoPage> createState() => _AutoRefreshVideoPageState();
|
||||
}
|
||||
|
||||
class _AutoRefreshVideoPageState extends State<AutoRefreshVideoPage> {
|
||||
late DroneStationBloc _bloc;
|
||||
Timer? _refreshTimer;
|
||||
static const _tokenRefreshInterval = Duration(minutes: 55); // 每55分钟刷新一次(Token有效期约1小时)
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
_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<void> _startRecording() async {
|
||||
// 根据使用的 RTC SDK 调用相应的录制 API
|
||||
}
|
||||
|
||||
Future<void> _stopRecording() async {
|
||||
// 停止录制并保存文件
|
||||
}
|
||||
```
|
||||
|
||||
### 添加截图功能
|
||||
|
||||
```dart
|
||||
// TODO: 集成 RTC SDK 的截图功能
|
||||
Future<void> _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. 联系开发团队
|
||||
Reference in New Issue
Block a user