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