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

16 KiB
Raw Blame History

无人机实时视频接口使用示例

快速开始

本指南展示如何在你的项目中快速集成无人机实时视频功能。

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. 最佳实践

✅ 推荐做法

  1. 始终检查错误状态

    if (state is UavVideoStreamError) {
      // 显示友好的错误提示
      showSnackBar(context, '视频加载失败: ${state.message}');
    }
    
  2. 提供重试机制

    ElevatedButton(
      onPressed: () => _loadVideoStream(_currentLensType!),
      child: const Text('重试'),
    )
    
  3. 记录关键日志

    debugPrint('视频流加载成功: URL Type=${videoStream.urlType}');
    
  4. 合理设置 Token 有效期

    videoExpire: 720000000,  // 约8天,避免频繁刷新
    
  5. 及时释放资源

    @override
    void dispose() {
      _bloc.close();
      super.dispose();
    }
    

❌ 避免的做法

  1. 不要在页面外直接调用 API

    //  错误:绕过 BLoC 直接调用
    final result = await repository.getUavVideoStream(...);
    
    // ✅ 正确:通过 BLoC 管理状态
    _bloc.add(UavVideoStreamLoad(...));
    
  2. 不要忘记处理加载状态

    // ❌ 错误:没有加载指示器
    if (state is UavVideoStreamLoaded) { ... }
    
    // ✅ 正确:显示加载状态
    if (state is UavVideoStreamLoading) {
      return CircularProgressIndicator();
    }
    
  3. 不要硬编码参数

    //  错误:硬编码
    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. 扩展功能

添加视频录制功能

// 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. 技术支持

如遇到问题,请:

  1. 查看控制台日志输出
  2. 参考示例代码 uav_live_video_page.dart
  3. 查阅集成指南文档
  4. 联系开发团队