一次大的提交

This commit is contained in:
2026-08-07 08:49:29 +08:00
parent bf2fc49c19
commit db70665ced
181 changed files with 23060 additions and 2804 deletions

View 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] 添加调试日志

View 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 即可显示实际视频画面。

View 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` - 注册依赖

View 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 显示实际视频画面**,即可完整实现无人机实时视频监控功能!

View 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. 联系开发团队