Files
feature-next-arch/docs/UAV_VIDEO_USAGE_EXAMPLES.md
2026-08-07 08:49:29 +08:00

643 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 无人机实时视频接口使用示例
## 快速开始
本指南展示如何在你的项目中快速集成无人机实时视频功能。
## 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. 联系开发团队