16 KiB
16 KiB
无人机实时视频接口使用示例
快速开始
本指南展示如何在你的项目中快速集成无人机实时视频功能。
1. 基础用法 - 直接导航到视频页面
最简单的方式是直接导航到 UavLiveVideoPage:
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
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: 实现加载视频流方法
/// 加载视频流
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 并监听状态变化
@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 引擎(可选)
/// 初始化 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: 从列表页跳转到视频页
// 在设备列表中点击某个设备
void _onDeviceTap(Device device) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UavLiveVideoPage(
droneSn: device.sn,
cameraIndex: device.cameraIndex,
),
),
);
}
场景 2: 支持多个摄像头切换
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 过期时)
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. 最佳实践
✅ 推荐做法
-
始终检查错误状态
if (state is UavVideoStreamError) { // 显示友好的错误提示 showSnackBar(context, '视频加载失败: ${state.message}'); } -
提供重试机制
ElevatedButton( onPressed: () => _loadVideoStream(_currentLensType!), child: const Text('重试'), ) -
记录关键日志
debugPrint('视频流加载成功: URL Type=${videoStream.urlType}'); -
合理设置 Token 有效期
videoExpire: 720000000, // 约8天,避免频繁刷新 -
及时释放资源
@override void dispose() { _bloc.close(); super.dispose(); }
❌ 避免的做法
-
不要在页面外直接调用 API
// 错误:绕过 BLoC 直接调用 final result = await repository.getUavVideoStream(...); // ✅ 正确:通过 BLoC 管理状态 _bloc.add(UavVideoStreamLoad(...)); -
不要忘记处理加载状态
// ❌ 错误:没有加载指示器 if (state is UavVideoStreamLoaded) { ... } // ✅ 正确:显示加载状态 if (state is UavVideoStreamLoading) { return CircularProgressIndicator(); } -
不要硬编码参数
// 错误:硬编码 sn: '1581F8HGX253U00A063U', // ✅ 正确:使用变量 sn: widget.droneSn,
5. 故障排查
问题 1: 视频流加载失败
可能原因:
- 网络问题
- 设备序列号错误
- 摄像头编号错误
- Token 过期
解决方案:
- 检查网络连接
- 验证
droneSn和cameraIndex是否正确 - 查看控制台日志输出
- 尝试重新加载
问题 2: 视频画面不显示
可能原因:
- RTC SDK 未正确初始化
- RTC 参数解析失败
- SDK 版本不兼容
解决方案:
- 检查
urlType字段,确认使用正确的 SDK - 验证 AppId、RoomId、Token、UserId 是否正确解析
- 查看 RTC SDK 的日志输出
- 参考
drone_station_detail_page.dart中的实现
问题 3: 镜头切换无效
可能原因:
- 后端不支持该镜头类型
- 摄像头不支持指定的镜头
解决方案:
- 检查后端返回的错误信息
- 尝试其他镜头类型
- 联系后端确认支持的镜头类型
6. 扩展功能
添加视频录制功能
// TODO: 集成 RTC SDK 的录制功能
Future<void> _startRecording() async {
// 根据使用的 RTC SDK 调用相应的录制 API
}
Future<void> _stopRecording() async {
// 停止录制并保存文件
}
添加截图功能
// TODO: 集成 RTC SDK 的截图功能
Future<void> _takeScreenshot() async {
// 根据使用的 RTC SDK 调用相应的截图 API
// 保存截图到相册
}
添加视频质量切换
void _changeQuality(VideoQualityType quality) {
_bloc.add(
UavVideoStreamLoad(
sn: widget.droneSn,
cameraIndex: widget.cameraIndex,
lensType: _currentLensType,
qualityType: quality, // 切换清晰度
),
);
}
7. 相关文档
8. 技术支持
如遇到问题,请:
- 查看控制台日志输出
- 参考示例代码
uav_live_video_page.dart - 查阅集成指南文档
- 联系开发团队