一次大的提交
This commit is contained in:
296
docs/DRONE_STATION_OSD_CARD_USAGE.md
Normal file
296
docs/DRONE_STATION_OSD_CARD_USAGE.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# 无人机机场 OSD 实时数据展示功能
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
在无人机机场详情页面添加了 **OSD 实时数据卡片**,通过 MQTT 订阅 `thing/product/${gatewaySn}/osd` topic,实时展示机场的遥测数据。
|
||||
|
||||
## 🎯 核心特性
|
||||
|
||||
### 1. 滑动窗口设计
|
||||
- **左右滑动切换**:用户可以左右滑动查看不同的 OSD 数据字段
|
||||
- **底部指示器**:显示当前页码和总页数
|
||||
- **滑动提示**:左右箭头提示用户可以滑动
|
||||
|
||||
### 2. 智能状态管理
|
||||
- **在线时**:显示实时 OSD 数据(电量、温度、风速等)
|
||||
- **离线时**:显示"机场离线,暂无实时数据"提示
|
||||
- **加载中**:显示加载动画
|
||||
|
||||
### 3. 数据字段展示
|
||||
|
||||
目前展示的 OSD 字段包括:
|
||||
|
||||
| 字段 | 图标 | 颜色规则 | 示例值 |
|
||||
|------|------|---------|--------|
|
||||
| 电量 | 🔋 battery_full | >50% 绿色, 20-50% 橙色, <20% 红色 | 85% |
|
||||
| 温度 | 🌡️ thermostat | 蓝色 | 25°C |
|
||||
| 风速 | 💨 air | 绿色 | 3.5 m/s |
|
||||
| 降雨量 | 💧 water_drop | 蓝色 | 小雨 / 12 mm |
|
||||
| 网络状态 | 📶 network_check | 灰色 | 等级 4 |
|
||||
| GPS | 🛰️ gps_fixed | 紫色 | 12 颗 |
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
lib/features/v2/device_list/
|
||||
├── presentation/
|
||||
│ ├── pages/
|
||||
│ │ └── drone_station_detail_page.dart # 详情页(集成 OSD 卡片)
|
||||
│ └── widgets/
|
||||
│ └── drone_station_osd_card.dart # OSD 实时数据卡片组件
|
||||
```
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 1. MQTT 数据订阅
|
||||
|
||||
```dart
|
||||
// 订阅机场 OSD topic
|
||||
final stationTopic = 'thing/product/$gatewaySn/osd';
|
||||
|
||||
// 监听数据流
|
||||
_subscription = _dataSource.stationOsdStream.listen((osd) {
|
||||
setState(() {
|
||||
_currentOsd = osd;
|
||||
_parseOsdFields(osd); // 解析并更新 UI
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 数据解析
|
||||
|
||||
```dart
|
||||
void _parseOsdFields(DroneOsdEntity osd) {
|
||||
final data = osd.rawData;
|
||||
|
||||
_osdFields = [
|
||||
{
|
||||
'icon': Icons.battery_full_rounded,
|
||||
'label': '电量',
|
||||
'value': data['battery'] != null ? '${data['battery']}%' : '未知',
|
||||
'color': _getBatteryColor(data['battery']),
|
||||
},
|
||||
// ... 其他字段
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 滑动卡片实现
|
||||
|
||||
使用 `PageView.builder` 实现左右滑动:
|
||||
|
||||
```dart
|
||||
PageView.builder(
|
||||
itemCount: _osdFields.length,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final field = _osdFields[index];
|
||||
return _buildOsdPage(field, index);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## 🎨 UI 设计
|
||||
|
||||
### 卡片布局
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ [图标] 电量 ◀ ▶ │
|
||||
│ │
|
||||
│ 85% │
|
||||
│ │
|
||||
│ 1 / 6 │
|
||||
│ │
|
||||
│ ● ○ ○ ○ ○ ○ │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
### 状态指示器
|
||||
- **当前页**:长条蓝色圆点(16px)
|
||||
- **其他页**:短条灰色圆点(6px)
|
||||
|
||||
## 📊 数据映射规则
|
||||
|
||||
### 降雨量格式化
|
||||
|
||||
```dart
|
||||
String _formatRainfall(dynamic rainfall) {
|
||||
if (rainfall is String) {
|
||||
switch (rainfall.toLowerCase()) {
|
||||
case 'no_rain': return '无降雨';
|
||||
case 'light_rain': return '小雨';
|
||||
case 'moderate_rain': return '中雨';
|
||||
case 'heavy_rain': return '大雨';
|
||||
default: return '$rainfall mm';
|
||||
}
|
||||
}
|
||||
return '$rainfall mm';
|
||||
}
|
||||
```
|
||||
|
||||
### 电量颜色规则
|
||||
|
||||
```dart
|
||||
Color _getBatteryColor(dynamic battery) {
|
||||
final value = battery is num ? battery.toDouble() : 0.0;
|
||||
if (value > 50) return Color(0xFF00B42A); // 绿色
|
||||
if (value > 20) return Color(0xFFFF7D00); // 橙色
|
||||
return Color(0xFFF53F3F); // 红色
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 生命周期管理
|
||||
|
||||
### 启动监听
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startListening(); // 开始监听 MQTT
|
||||
}
|
||||
```
|
||||
|
||||
### 停止监听
|
||||
```dart
|
||||
@override
|
||||
void dispose() {
|
||||
_stopListening(); // 取消订阅,释放资源
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### 动态更新
|
||||
```dart
|
||||
@override
|
||||
void didUpdateWidget(DroneStationOsdCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// gatewaySn 或 isOnline 变化时重新订阅
|
||||
if (oldWidget.gatewaySn != widget.gatewaySn ||
|
||||
oldWidget.isOnline != widget.isOnline) {
|
||||
_stopListening();
|
||||
_startListening();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 在详情页中集成
|
||||
|
||||
```dart
|
||||
DroneStationOsdCard(
|
||||
gatewaySn: widget.station.gatewaySn,
|
||||
isOnline: detail.isOnline,
|
||||
)
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `gatewaySn` | String | 机场网关序列号(用于 MQTT topic) |
|
||||
| `isOnline` | bool | 机场是否在线(决定是否显示实时数据) |
|
||||
|
||||
## 📝 扩展建议
|
||||
|
||||
### 1. 添加更多 OSD 字段
|
||||
|
||||
在 `_parseOsdFields` 方法中添加新字段:
|
||||
|
||||
```dart
|
||||
{
|
||||
'icon': Icons.new_icon,
|
||||
'label': '新字段名称',
|
||||
'value': data['newField'] != null ? '${data['newField']}' : '未知',
|
||||
'color': const Color(0xFF165DFF),
|
||||
},
|
||||
```
|
||||
|
||||
### 2. 自定义滑动方向
|
||||
|
||||
修改 `PageView` 的 `scrollDirection`:
|
||||
|
||||
```dart
|
||||
PageView.builder(
|
||||
scrollDirection: Axis.vertical, // 改为上下滑动
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 自动轮播
|
||||
|
||||
添加定时器自动切换卡片:
|
||||
|
||||
```dart
|
||||
Timer.periodic(Duration(seconds: 3), (timer) {
|
||||
if (_osdFields.isNotEmpty) {
|
||||
setState(() {
|
||||
_currentIndex = (_currentIndex + 1) % _osdFields.length;
|
||||
});
|
||||
_pageController.animateToPage(_currentIndex, ...);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 手势优化
|
||||
|
||||
添加双击放大、长按复制等功能:
|
||||
|
||||
```dart
|
||||
GestureDetector(
|
||||
onDoubleTap: () {
|
||||
// 双击放大
|
||||
},
|
||||
onLongPress: () {
|
||||
// 长按复制数值
|
||||
Clipboard.setData(ClipboardData(text: field['value']));
|
||||
},
|
||||
child: _buildOsdPage(field, index),
|
||||
)
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **MQTT 连接**:确保应用已连接到 MQTT 服务器
|
||||
2. **topic 格式**:必须为 `thing/product/${gatewaySn}/osd`
|
||||
3. **数据格式**:MQTT 消息 payload 必须是 JSON 格式
|
||||
4. **内存管理**:页面销毁时必须取消订阅,避免内存泄漏
|
||||
5. **在线判断**:只有机场在线时才订阅 MQTT,离线时不订阅
|
||||
|
||||
## 🐛 调试技巧
|
||||
|
||||
### 查看 MQTT 日志
|
||||
|
||||
```dart
|
||||
debugPrint('🔊 [DroneStationOsdCard] 开始监听机场 OSD: ${widget.gatewaySn}');
|
||||
debugPrint('✅ [DroneStationOsdCard] 收到机场 OSD 数据');
|
||||
```
|
||||
|
||||
### 检查数据解析
|
||||
|
||||
```dart
|
||||
debugPrint('📊 [DroneStationOsdCard] OSD 原始数据: ${osd.rawData}');
|
||||
debugPrint('📊 [DroneStationOsdCard] 解析后字段数: ${_osdFields.length}');
|
||||
```
|
||||
|
||||
## 📦 依赖项
|
||||
|
||||
- `flutter_bloc`: 状态管理
|
||||
- `mqtt_client`: MQTT 通信
|
||||
- `equatable`: 实体类比较
|
||||
- `get_it`: 依赖注入
|
||||
|
||||
## ✅ 完成清单
|
||||
|
||||
- [x] 创建 `DroneStationOsdCard` 组件
|
||||
- [x] 实现 MQTT 数据订阅
|
||||
- [x] 实现滑动窗口 UI
|
||||
- [x] 实现数据解析和格式化
|
||||
- [x] 集成到详情页
|
||||
- [x] 添加状态管理(在线/离线/加载中)
|
||||
- [x] 添加生命周期管理
|
||||
- [x] 添加调试日志
|
||||
195
docs/UAV_VIDEO_COMPLETION_SUMMARY.md
Normal file
195
docs/UAV_VIDEO_COMPLETION_SUMMARY.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 无人机实时视频接口对接完成总结
|
||||
|
||||
## ✅ 已完成工作
|
||||
|
||||
### 1. API 层
|
||||
- [x] 添加 API 常量 `changeUAVLens` 到 `HttpApiConsts`
|
||||
|
||||
### 2. 实体层 (Domain/Entities)
|
||||
- [x] 创建 `UavVideoStreamEntity` 实体类
|
||||
- [x] 定义 `UavLensType` 枚举(广角、变焦、红外)
|
||||
- [x] 定义 `VideoQualityType` 枚举(自适应、低、中、高清晰度)
|
||||
- [x] 实现 RTC 参数解析方法(appId、roomId、token、userId)
|
||||
|
||||
### 3. 数据源层 (Data/Datasources)
|
||||
- [x] 扩展 `DroneStationDataSource` 接口,添加 `getUavVideoStream` 方法
|
||||
- [x] 实现 `DroneStationDataSourceImpl.getUavVideoStream`
|
||||
- [x] 支持可选的镜头类型参数
|
||||
- [x] 支持自定义清晰度和 Token 有效期
|
||||
- [x] 添加详细的日志输出用于调试
|
||||
|
||||
### 4. 仓库层 (Data/Repositories)
|
||||
- [x] 扩展 `DroneStationRepository` 接口
|
||||
- [x] 实现 `DroneStationRepositoryImpl.getUavVideoStream`
|
||||
- [x] 使用 `Either<Failure, T>` 模式处理错误
|
||||
|
||||
### 5. 用例层 (Domain/UseCases)
|
||||
- [x] 创建 `GetUavVideoStreamUseCase`
|
||||
- [x] 封装业务逻辑,供 BLoC 调用
|
||||
- [x] 提供合理的默认值
|
||||
|
||||
### 6. 状态管理层 (Presentation/BLoC)
|
||||
- [x] 添加 `UavVideoStreamLoad` 事件
|
||||
- [x] 添加 `UavVideoStreamLoading` 状态
|
||||
- [x] 添加 `UavVideoStreamLoaded` 状态
|
||||
- [x] 添加 `UavVideoStreamError` 状态
|
||||
- [x] 在 `DroneStationBloc` 中实现事件处理逻辑
|
||||
|
||||
### 7. 依赖注入 (DI)
|
||||
- [x] 导入 `GetUavVideoStreamUseCase`
|
||||
- [x] 注册 `GetUavVideoStreamUseCase` 为单例
|
||||
- [x] 更新 `DroneStationBloc` 工厂,注入新的 UseCase
|
||||
|
||||
### 8. UI 层 (Presentation/Pages)
|
||||
- [x] 创建示例页面 `UavLiveVideoPage`
|
||||
- [x] 实现镜头切换功能(PopupMenuButton)
|
||||
- [x] 实现加载状态显示
|
||||
- [x] 实现错误处理和重试机制
|
||||
- [x] 预留 RTC SDK 集成位置
|
||||
|
||||
### 9. 测试
|
||||
- [x] 编写单元测试 `get_uav_video_stream_usecase_test.dart`
|
||||
- [x] 测试成功场景
|
||||
- [x] 测试失败场景
|
||||
- [x] 测试默认参数
|
||||
|
||||
### 10. 文档
|
||||
- [x] 创建集成指南 `UAV_VIDEO_INTEGRATION_GUIDE.md`
|
||||
- [x] 创建使用示例 `UAV_VIDEO_USAGE_EXAMPLES.md`
|
||||
|
||||
## 📁 文件清单
|
||||
|
||||
### 新增文件(7个)
|
||||
1. `lib/features/v2/device_list/domain/entities/uav_video_stream_entity.dart` - 实体类
|
||||
2. `lib/features/v2/device_list/domain/usecases/get_uav_video_stream_usecase.dart` - 用例
|
||||
3. `lib/features/v2/device_list/presentation/pages/uav_live_video_page.dart` - 示例页面
|
||||
4. `test/features/v2/device_list/domain/usecases/get_uav_video_stream_usecase_test.dart` - 单元测试
|
||||
5. `docs/UAV_VIDEO_INTEGRATION_GUIDE.md` - 集成指南
|
||||
6. `docs/UAV_VIDEO_USAGE_EXAMPLES.md` - 使用示例
|
||||
7. `docs/UAV_VIDEO_COMPLETION_SUMMARY.md` - 本文件
|
||||
|
||||
### 修改文件(9个)
|
||||
1. `lib/core/consts/http_api_consts.dart` - 添加 API 常量
|
||||
2. `lib/features/v2/device_list/data/datasources/drone_station_datasource.dart` - 添加接口方法
|
||||
3. `lib/features/v2/device_list/data/datasources/drone_station_datasource_impl.dart` - 实现接口
|
||||
4. `lib/features/v2/device_list/data/repositories/drone_station_repository_impl.dart` - 实现仓库
|
||||
5. `lib/features/v2/device_list/domain/repositories/drone_station_repository.dart` - 添加仓库接口
|
||||
6. `lib/features/v2/device_list/presentation/bloc/drone_station_event.dart` - 添加事件
|
||||
7. `lib/features/v2/device_list/presentation/bloc/drone_station_state.dart` - 添加状态
|
||||
8. `lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart` - 添加事件处理
|
||||
9. `lib/core/di/injection.dart` - 注册依赖
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 清洁架构设计
|
||||
- **分层清晰**:Entity → UseCase → Repository → DataSource → BLoC → UI
|
||||
- **依赖倒置**:高层模块不依赖低层模块的具体实现
|
||||
- **可测试性**:每层都可以独立测试
|
||||
|
||||
### 可扩展性
|
||||
- **多镜头支持**:通过枚举轻松扩展更多镜头类型
|
||||
- **多清晰度支持**:支持自适应、低、中、高四种清晰度
|
||||
- **多 RTC SDK 支持**:兼容火山引擎和声网两种 RTC SDK
|
||||
- **可复用**:可在任何页面中使用,无需重复实现
|
||||
|
||||
### 健壮性
|
||||
- **错误处理**:使用 `Either<Failure, T>` 统一处理错误
|
||||
- **默认值**:提供合理的默认参数,简化调用
|
||||
- **日志记录**:详细的日志输出,方便调试
|
||||
- **重试机制**:支持手动重试和自动刷新
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方式一:直接使用示例页面
|
||||
```dart
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UavLiveVideoPage(
|
||||
droneSn: '1581F8HGX253U00A063U',
|
||||
cameraIndex: '176-0-0',
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### 方式二:在自定义页面中集成
|
||||
参考 `docs/UAV_VIDEO_USAGE_EXAMPLES.md` 中的详细示例。
|
||||
|
||||
## 📋 API 说明
|
||||
|
||||
### 请求参数
|
||||
```json
|
||||
{
|
||||
"sn": "1581F8HGX253U00A063U", // 必填:设备序列号
|
||||
"lensType": "", // 可选:wide/zoom/ir
|
||||
"cameraIndex": "176-0-0", // 必填:摄像头编号
|
||||
"qualityType": "adaptive", // 可选:adaptive/low/medium/high
|
||||
"videoExpire": 720000000 // 可选:Token有效期(毫秒)
|
||||
}
|
||||
```
|
||||
|
||||
### 返回数据
|
||||
```json
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 200,
|
||||
"data": {
|
||||
"sn": "1581F8HGX253U00A063U",
|
||||
"camera_index": "176-0-0",
|
||||
"url": "app_id=xxx&room_id=xxx&token=xxx&user_id=xxx",
|
||||
"expire_ts": 1781155234,
|
||||
"url_type": "volc" // volc 或 agora
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 后续工作
|
||||
|
||||
1. [ ] 集成 RTC SDK 显示实际视频画面
|
||||
- 根据 `urlType` 选择火山引擎或声网 SDK
|
||||
- 初始化 RTC 引擎并加入房间
|
||||
- 渲染远程视频流
|
||||
|
||||
2. [ ] 添加视频录制功能
|
||||
- 开始/停止录制
|
||||
- 保存录制文件
|
||||
|
||||
3. [ ] 添加截图功能
|
||||
- 截取当前帧
|
||||
- 保存到相册
|
||||
|
||||
4. [ ] 优化视频加载超时处理
|
||||
- 设置合理的超时时间
|
||||
- 提供友好的超时提示
|
||||
|
||||
5. [ ] 支持多路视频同时观看
|
||||
- 分屏显示多个摄像头
|
||||
- 切换主视图
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [完整集成指南](./UAV_VIDEO_INTEGRATION_GUIDE.md)
|
||||
- [使用示例](./UAV_VIDEO_USAGE_EXAMPLES.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/)
|
||||
|
||||
## ✅ 验证结果
|
||||
|
||||
- [x] 所有文件编译通过,无错误
|
||||
- [x] 单元测试编写完成
|
||||
- [x] 依赖注入配置正确
|
||||
- [x] 文档齐全
|
||||
|
||||
## 💡 注意事项
|
||||
|
||||
1. **不要在页面上直接调用 API**:必须通过 BLoC 管理状态
|
||||
2. **及时释放资源**:在 `dispose()` 中关闭 BLoC
|
||||
3. **合理设置 Token 有效期**:建议设置为较长的时间,避免频繁刷新
|
||||
4. **检查 RTC 参数**:确保 AppId、RoomId、Token、UserId 正确解析
|
||||
5. **根据 urlType 选择 SDK**:volc 使用火山引擎,agora 使用声网
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
无人机实时视频接口已成功对接,采用清洁架构设计,具有良好的可扩展性和可维护性。所有代码已通过编译检查,单元测试覆盖主要场景,文档齐全。下一步只需集成 RTC SDK 即可显示实际视频画面。
|
||||
323
docs/UAV_VIDEO_INTEGRATION_GUIDE.md
Normal file
323
docs/UAV_VIDEO_INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# 无人机实时视频接口对接说明
|
||||
|
||||
## 概述
|
||||
|
||||
本模块实现了无人机实时视频流的获取功能,采用清洁架构设计,支持多种镜头类型切换(广角、变焦、红外),并兼容火山引擎和声网两种RTC SDK。
|
||||
|
||||
## API 接口
|
||||
|
||||
### 接口地址
|
||||
```
|
||||
POST http://1.95.137.212:59015/iot/UAV/changeLens
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
```json
|
||||
{
|
||||
"sn": "1581F8HGX253U00A063U", // 无人机设备序列号(必填)
|
||||
"lensType": "", // 镜头类型:wide(广角)、zoom(变焦)、ir(红外),可为空
|
||||
"cameraIndex": "176-0-0", // 摄像头编号(必填)
|
||||
"qualityType": "adaptive", // 清晰度:adaptive(自适应)、low、medium、high(默认adaptive)
|
||||
"videoExpire": 720000000 // Token有效期(毫秒,默认720000000)
|
||||
}
|
||||
```
|
||||
|
||||
### 返回数据
|
||||
```json
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 200,
|
||||
"data": {
|
||||
"sn": "1581F8HGX253U00A063U",
|
||||
"camera_index": "176-0-0",
|
||||
"url": "app_id=xxx&expire_time=xxx&room_id=xxx&token=xxx&user_id=xxx",
|
||||
"expire_ts": 1781155234,
|
||||
"url_type": "volc" // volc(火山引擎) 或 agora(声网)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 目录结构
|
||||
```
|
||||
lib/features/v2/device_list/
|
||||
├── domain/
|
||||
│ ├── entities/
|
||||
│ │ └── uav_video_stream_entity.dart # 实体类
|
||||
│ ├── repositories/
|
||||
│ │ ── drone_station_repository.dart # 仓库接口
|
||||
│ └── usecases/
|
||||
│ └── get_uav_video_stream_usecase.dart # 用例
|
||||
├── data/
|
||||
│ ├── datasources/
|
||||
│ │ ├── drone_station_datasource.dart # 数据源接口
|
||||
│ │ └── drone_station_datasource_impl.dart # 数据源实现
|
||||
│ └── repositories/
|
||||
│ └── drone_station_repository_impl.dart # 仓库实现
|
||||
── presentation/
|
||||
├── bloc/
|
||||
│ ├── drone_station_bloc.dart # BLoC状态管理
|
||||
│ ├── drone_station_event.dart # 事件定义
|
||||
│ └── drone_station_state.dart # 状态定义
|
||||
└── pages/
|
||||
── uav_live_video_page.dart # 示例页面
|
||||
```
|
||||
|
||||
### 核心组件
|
||||
|
||||
#### 1. 实体类 (Entity)
|
||||
**文件**: `domain/entities/uav_video_stream_entity.dart`
|
||||
|
||||
定义了枚举类型和实体类:
|
||||
- `UavLensType`: 镜头类型枚举(wide、zoom、ir)
|
||||
- `VideoQualityType`: 视频质量枚举(adaptive、low、medium、high)
|
||||
- `UavVideoStreamEntity`: 视频流实体,包含解析RTC参数的方法
|
||||
|
||||
#### 2. 数据源 (DataSource)
|
||||
**文件**: `data/datasources/drone_station_datasource_impl.dart`
|
||||
|
||||
实现了API调用逻辑:
|
||||
- 发送POST请求到 `/iot/UAV/changeLens`
|
||||
- 处理响应并转换为实体对象
|
||||
- 包含详细的日志输出用于调试
|
||||
|
||||
#### 3. 仓库 (Repository)
|
||||
**文件**: `data/repositories/drone_station_repository_impl.dart`
|
||||
|
||||
使用 `fpdart` 的 `Either` 类型处理错误:
|
||||
- 成功时返回 `Right(UavVideoStreamEntity)`
|
||||
- 失败时返回 `Left(Failure)`
|
||||
|
||||
#### 4. 用例 (UseCase)
|
||||
**文件**: `domain/usecases/get_uav_video_stream_usecase.dart`
|
||||
|
||||
封装业务逻辑,供BLoC调用。
|
||||
|
||||
#### 5. BLoC 状态管理
|
||||
**文件**: `presentation/bloc/drone_station_bloc.dart`
|
||||
|
||||
新增事件和状态:
|
||||
- **事件**: `UavVideoStreamLoad` - 加载视频流
|
||||
- **状态**:
|
||||
- `UavVideoStreamLoading` - 加载中
|
||||
- `UavVideoStreamLoaded` - 加载成功
|
||||
- `UavVideoStreamError` - 加载失败
|
||||
|
||||
#### 6. 依赖注入
|
||||
**文件**: `core/di/injection.dart`
|
||||
|
||||
已注册以下单例:
|
||||
```dart
|
||||
sl.registerLazySingleton<GetUavVideoStreamUseCase>(
|
||||
() => GetUavVideoStreamUseCase(sl()),
|
||||
);
|
||||
sl.registerFactory<DroneStationBloc>(
|
||||
() => DroneStationBloc(sl(), sl(), sl(), sl()),
|
||||
);
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式一:直接使用示例页面
|
||||
|
||||
```dart
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/uav_live_video_page.dart';
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UavLiveVideoPage(
|
||||
droneSn: '1581F8HGX253U00A063U',
|
||||
cameraIndex: '176-0-0',
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### 方式二:在现有页面中使用
|
||||
|
||||
#### 1. 导入必要的类
|
||||
```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';
|
||||
```
|
||||
|
||||
#### 2. 初始化 BLoC
|
||||
```dart
|
||||
late DroneStationBloc _bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
// 加载视频流
|
||||
_loadVideoStream(UavLensType.wide);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 加载视频流
|
||||
```dart
|
||||
void _loadVideoStream(UavLensType lensType) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
_bloc.add(
|
||||
UavVideoStreamLoad(
|
||||
sn: widget.droneSn,
|
||||
cameraIndex: widget.cameraIndex,
|
||||
lensType: lensType,
|
||||
qualityType: VideoQualityType.adaptive,
|
||||
videoExpire: 720000000,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 监听状态变化
|
||||
```dart
|
||||
BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||
listener: (context, state) {
|
||||
if (state is UavVideoStreamLoaded) {
|
||||
final videoStream = state.videoStream;
|
||||
debugPrint('URL Type: ${videoStream.urlType}');
|
||||
debugPrint('AppId: ${videoStream.appId}');
|
||||
debugPrint('RoomId: ${videoStream.roomId}');
|
||||
debugPrint('UserId: ${videoStream.userId}');
|
||||
|
||||
// TODO: 根据 urlType 初始化对应的 RTC 引擎
|
||||
if (videoStream.urlType.toLowerCase() == 'volc') {
|
||||
// 使用火山引擎 RTC SDK
|
||||
} else if (videoStream.urlType.toLowerCase() == 'agora') {
|
||||
// 使用声网 RTC SDK
|
||||
}
|
||||
} else if (state is UavVideoStreamError) {
|
||||
setState(() {
|
||||
_errorMessage = state.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
// 根据状态渲染UI
|
||||
if (state is UavVideoStreamLoading) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
if (state is UavVideoStreamLoaded) {
|
||||
// 显示视频画面
|
||||
return Container();
|
||||
}
|
||||
|
||||
if (state is UavVideoStreamError) {
|
||||
return Text('错误: ${state.message}');
|
||||
}
|
||||
|
||||
return Container();
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
#### 5. 切换镜头类型
|
||||
```dart
|
||||
// 切换到广角镜头
|
||||
_loadVideoStream(UavLensType.wide);
|
||||
|
||||
// 切换到变焦镜头
|
||||
_loadVideoStream(UavLensType.zoom);
|
||||
|
||||
// 切换到红外镜头
|
||||
_loadVideoStream(UavLensType.ir);
|
||||
```
|
||||
|
||||
## RTC 集成
|
||||
|
||||
获取到视频流后,需要根据 `urlType` 选择对应的 RTC SDK:
|
||||
|
||||
### 火山引擎 (volc)
|
||||
```dart
|
||||
final appId = videoStream.appId;
|
||||
final roomId = videoStream.roomId;
|
||||
final token = videoStream.token;
|
||||
final userId = videoStream.userId;
|
||||
|
||||
// 使用 volc_engine_rtc SDK
|
||||
final engine = await RTCEngine.createRTCEngine(
|
||||
RTCVideoContext(appId: appId, eventHandler: handler),
|
||||
);
|
||||
final room = await engine.createRTCRoom(roomId);
|
||||
await room.joinRoom(token: token, userId: userId);
|
||||
```
|
||||
|
||||
### 声网 (agora)
|
||||
```dart
|
||||
final appId = videoStream.appId;
|
||||
final channelId = videoStream.roomId;
|
||||
final token = videoStream.token;
|
||||
final uid = int.tryParse(videoStream.userId) ?? 0;
|
||||
|
||||
// 使用 agora_rtc_engine SDK
|
||||
final engine = createAgoraRtcEngine();
|
||||
await engine.initialize(RtcEngineContext(appId: appId));
|
||||
await engine.joinChannel(
|
||||
token: token,
|
||||
channelId: channelId,
|
||||
uid: uid,
|
||||
options: ChannelMediaOptions(...),
|
||||
);
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **可扩展性**: 本模块采用清洁架构设计,所有业务逻辑与UI分离,便于在其他页面复用。
|
||||
|
||||
2. **错误处理**: 使用 `Either<Failure, T>` 模式统一处理错误,确保异常不会直接抛出。
|
||||
|
||||
3. **日志记录**: 数据源层包含详细的日志输出,方便调试和问题排查。
|
||||
|
||||
4. **默认值**:
|
||||
- `lensType` 可为空,后端会根据实际情况选择默认镜头
|
||||
- `qualityType` 默认为 `adaptive`(自适应)
|
||||
- `videoExpire` 默认为 `720000000` 毫秒
|
||||
|
||||
5. **Token 有效期**: `videoExpire` 参数单位为毫秒,建议设置较长的有效期以避免频繁刷新。
|
||||
|
||||
6. **多镜头支持**: 通过 `UavLensType` 枚举可以轻松扩展更多镜头类型。
|
||||
|
||||
## 后续工作
|
||||
|
||||
1. [ ] 集成 RTC SDK 显示实际视频画面
|
||||
2. [ ] 添加视频录制功能
|
||||
3. [ ] 添加截图功能
|
||||
4. [ ] 优化视频加载超时处理
|
||||
5. [ ] 添加视频质量切换功能
|
||||
6. [ ] 支持多路视频同时观看
|
||||
|
||||
## 相关文件清单
|
||||
|
||||
### 新增文件
|
||||
- `lib/features/v2/device_list/domain/entities/uav_video_stream_entity.dart`
|
||||
- `lib/features/v2/device_list/domain/usecases/get_uav_video_stream_usecase.dart`
|
||||
- `lib/features/v2/device_list/presentation/pages/uav_live_video_page.dart`
|
||||
|
||||
### 修改文件
|
||||
- `lib/core/consts/http_api_consts.dart` - 添加 API 常量
|
||||
- `lib/features/v2/device_list/data/datasources/drone_station_datasource.dart` - 添加接口方法
|
||||
- `lib/features/v2/device_list/data/datasources/drone_station_datasource_impl.dart` - 实现接口
|
||||
- `lib/features/v2/device_list/data/repositories/drone_station_repository_impl.dart` - 实现仓库
|
||||
- `lib/features/v2/device_list/domain/repositories/drone_station_repository.dart` - 添加仓库接口
|
||||
- `lib/features/v2/device_list/presentation/bloc/drone_station_event.dart` - 添加事件
|
||||
- `lib/features/v2/device_list/presentation/bloc/drone_station_state.dart` - 添加状态
|
||||
- `lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart` - 添加事件处理
|
||||
- `lib/core/di/injection.dart` - 注册依赖
|
||||
298
docs/UAV_VIDEO_PAGE_INTEGRATION.md
Normal file
298
docs/UAV_VIDEO_PAGE_INTEGRATION.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# 无人机视频页面集成完成
|
||||
|
||||
## 📋 更新内容
|
||||
|
||||
已将新的无人机实时视频接口集成到现有的**无人机视频回传/远程控制**页面中。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 集成位置
|
||||
|
||||
### 1. 无人机详情页面 → 无人机状态卡片
|
||||
|
||||
**文件**: `lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart`
|
||||
|
||||
**修改内容**:
|
||||
```dart
|
||||
// 点击"无人机状态"卡片时,传递设备序列号和摄像头索引
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DroneVideoControlPage(
|
||||
droneSn: detail.deviceSn, // ✅ 传递设备序列号
|
||||
cameraIndex: detail.gatewayCameraList != null &&
|
||||
detail.gatewayCameraList!.isNotEmpty
|
||||
? detail.gatewayCameraList!.first.cameraIndex
|
||||
: '176-0-0', // 默认值
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 无人机视频控制页面
|
||||
|
||||
**文件**: `lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart`
|
||||
|
||||
**新增功能**:
|
||||
|
||||
#### ✅ 接收参数
|
||||
```dart
|
||||
class DroneVideoControlPage extends StatefulWidget {
|
||||
final String droneSn; // 无人机设备序列号
|
||||
final String cameraIndex; // 摄像头编号
|
||||
|
||||
const DroneVideoControlPage({
|
||||
super.key,
|
||||
required this.droneSn,
|
||||
required this.cameraIndex,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ 自动加载视频流
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
// 默认加载广角镜头的视频流
|
||||
_loadVideoStream(UavLensType.wide);
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ 镜头切换功能
|
||||
在视频播放器右下角添加了镜头类型选择器:
|
||||
- **广角** (wide)
|
||||
- **变焦** (zoom)
|
||||
- **红外** (ir)
|
||||
|
||||
点击可快速切换不同镜头的视频流。
|
||||
|
||||
#### ✅ 加载状态显示
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ │
|
||||
│ ⏳ 正在加载... │
|
||||
│ │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
#### ✅ 错误处理和重试
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ │
|
||||
│ ❌ 加载失败 │
|
||||
│ 错误信息: xxxxx │
|
||||
│ │
|
||||
│ [ 重试 ] │
|
||||
│ │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 工作流程
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[无人机详情页] --> B[点击无人机状态卡片]
|
||||
B --> C[打开视频控制页面]
|
||||
C --> D[自动调用changeLens接口]
|
||||
D --> E{加载结果}
|
||||
E -->|成功| F[显示视频画面]
|
||||
E -->|失败| G[显示错误+重试按钮]
|
||||
F --> H[用户切换镜头]
|
||||
H --> D
|
||||
G -->|点击重试| D
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 UI 展示
|
||||
|
||||
### 视频播放器区域
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ ● REC 00:12:36 │
|
||||
│ │
|
||||
│ │
|
||||
│ [视频画面/加载状态] │
|
||||
│ │
|
||||
│ │
|
||||
│ ┌────────┐ │
|
||||
│ │广角 ▼│ │ ← 镜头切换
|
||||
│ └────────┘ │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 镜头切换菜单
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ 广角 │ ← 当前选中
|
||||
│ 变焦 │
|
||||
│ 红外 │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 1. BLoC 状态管理
|
||||
```dart
|
||||
// 监听视频流状态
|
||||
BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||
listener: (context, state) {
|
||||
if (state is UavVideoStreamLoaded) {
|
||||
// 获取视频流数据
|
||||
_videoStream = state.videoStream;
|
||||
|
||||
// TODO: 初始化 RTC 引擎并显示视频
|
||||
debugPrint('AppId: ${state.videoStream.appId}');
|
||||
debugPrint('RoomId: ${state.videoStream.roomId}');
|
||||
debugPrint('UserId: ${state.videoStream.userId}');
|
||||
} else if (state is UavVideoStreamError) {
|
||||
// 显示错误信息
|
||||
_errorMessage = state.message;
|
||||
}
|
||||
},
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
### 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 镜头切换
|
||||
```dart
|
||||
PopupMenuButton<UavLensType>(
|
||||
icon: Text(_getLensTypeName(_currentLensType)),
|
||||
onSelected: (UavLensType lensType) {
|
||||
if (lensType != _currentLensType) {
|
||||
_loadVideoStream(lensType); // 重新加载视频流
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: UavLensType.wide, child: Text('广角')),
|
||||
PopupMenuItem(value: UavLensType.zoom, child: Text('变焦')),
|
||||
PopupMenuItem(value: UavLensType.ir, child: Text('红外')),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已完成的功能
|
||||
|
||||
- ✅ 从无人机详情页传递设备序列号和摄像头索引
|
||||
- ✅ 自动调用 `changeLens` 接口获取视频流
|
||||
- ✅ 支持三种镜头类型切换(广角/变焦/红外)
|
||||
- ✅ 加载状态显示
|
||||
- ✅ 错误处理和重试机制
|
||||
- ✅ 打印视频流参数(AppId、RoomId、UserId等)
|
||||
- ✅ 清洁架构设计,易于扩展和维护
|
||||
|
||||
---
|
||||
|
||||
## 🔜 下一步工作(TODO)
|
||||
|
||||
### 1. 集成 RTC SDK 显示实际视频
|
||||
|
||||
在 `_buildVideoPlayer()` 方法中,当 `_videoStream != null` 时:
|
||||
|
||||
```dart
|
||||
if (_videoStream != null) {
|
||||
// 根据 urlType 选择对应的 RTC SDK
|
||||
if (_videoStream!.urlType == 'volc') {
|
||||
// 使用火山引擎 RTC SDK
|
||||
return VolcEngineRtcView(
|
||||
appId: _videoStream!.appId,
|
||||
roomId: _videoStream!.roomId,
|
||||
token: _videoStream!.token,
|
||||
userId: _videoStream!.userId,
|
||||
);
|
||||
} else if (_videoStream!.urlType == 'agora') {
|
||||
// 使用声网 RTC SDK
|
||||
return AgoraRtcView(
|
||||
appId: _videoStream!.appId,
|
||||
channelId: _videoStream!.roomId,
|
||||
token: _videoStream!.token,
|
||||
uid: int.parse(_videoStream!.userId),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 参考现有实现
|
||||
|
||||
可以参考 `drone_station_detail_page.dart` 中的以下方法:
|
||||
- `_initFloatingVolcEngine()` - 火山引擎初始化
|
||||
- `_initFloatingAgoraEngine()` - 声网引擎初始化
|
||||
- `_disposeVolcEngine()` / `_disposeAgoraEngine()` - 资源释放
|
||||
|
||||
### 3. 添加更多控制功能
|
||||
|
||||
- 云台控制(上下左右)
|
||||
- 变焦倍数调节
|
||||
- 拍照/录像
|
||||
- AI识别结果展示
|
||||
|
||||
---
|
||||
|
||||
## 📝 测试步骤
|
||||
|
||||
1. **进入无人机详情页**
|
||||
- 确保无人机在线且有摄像头列表
|
||||
|
||||
2. **点击"无人机状态"卡片**
|
||||
- 应该跳转到视频控制页面
|
||||
|
||||
3. **观察加载过程**
|
||||
- 看到 "正在加载视频流..." 提示
|
||||
- 成功后显示视频画面(当前为占位图)
|
||||
|
||||
4. **测试镜头切换**
|
||||
- 点击右下角的镜头类型按钮
|
||||
- 选择"广角"、"变焦"或"红外"
|
||||
- 观察是否重新加载并切换成功
|
||||
|
||||
5. **测试错误处理**
|
||||
- 断开网络连接
|
||||
- 应该显示错误信息和重试按钮
|
||||
- 点击重试应该重新加载
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
✅ **集成完成!**
|
||||
|
||||
现在无人机视频控制页面已经:
|
||||
- 正确接收设备参数
|
||||
- 自动调用新的视频流接口
|
||||
- 支持多镜头切换
|
||||
- 完善的加载和错误处理
|
||||
|
||||
只需最后一步:**集成 RTC SDK 显示实际视频画面**,即可完整实现无人机实时视频监控功能!
|
||||
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