1更新无人机的摄像头视频显示的条件为在离线情况下的自动刷新+下拉刷新的机制。

2更新优化去除原有路径规划的初始化阶段的获取设备的接口请求,改为使用新的 targetDevice。
3修改无人机机场的在线状态简化为在线和离线。去掉机场二字。
4.更新同一账号同时段登录,被强制下线的友好提示!
This commit is contained in:
2026-06-17 13:46:30 +08:00
parent 71aea48d9f
commit 443ce601a0
6 changed files with 192 additions and 666 deletions

View File

@@ -6,6 +6,7 @@ import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/features/devices/data/repositories/route_planning_repository_impl.dart';
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_cubit.dart';
import '../../../features/auth/data/datasources/auth_tcp_datasource.dart';
import '../../../features/devices/data/models/route_plan_send_entity.dart';
@@ -273,11 +274,40 @@ class TcpClient {
return;
}
_reconnectTimer = Timer(const Duration(seconds: 5), () {
_reconnectTimer = Timer(const Duration(seconds: 5), () async {
_reconnectTimer = null;
//debugPrint('⏰ 被动-定时器触发,开始执行重连...');
_logger.logWithLevel('⏰ 被动-定时器触发,开始执行重连...', shouldLog: true);
connectBySwitch(host: _lastHost!, port: _lastPort!, deviceName: devname);
// 🔥 修复:重连时使用 RemoteControlCubit.targetDevice,而不是记住的设备名
try {
final remoteControlCubit = GetIt.I<RemoteControlCubit>();
final targetDevice = remoteControlCubit.state.targetDevice;
if (targetDevice != null) {
_logger.logWithLevel('🔄 [TCP] 重连使用 targetDevice: ${targetDevice.deviceName}', shouldLog: true);
connectBySwitch(
host: _lastHost!,
port: _lastPort!,
deviceName: targetDevice.deviceName,
);
} else {
// 如果 targetDevice 为 null,使用传入的设备名作为兜底
_logger.logWithLevel('⚠️ [TCP] targetDevice 为 null,使用传入的设备名: $devname', shouldLog: true);
connectBySwitch(
host: _lastHost!,
port: _lastPort!,
deviceName: devname,
);
}
} catch (e) {
_logger.logWithLevel('❌ [TCP] 获取 targetDevice 失败,使用传入的设备名: $devname, 错误: $e', shouldLog: true);
connectBySwitch(
host: _lastHost!,
port: _lastPort!,
deviceName: devname,
);
}
});
await _sendAuthPacketBySwitch(devname);
}
@@ -808,11 +838,24 @@ class TcpClient {
_logger.logWithLevel('🔑 被动[TCP] 已发送认证包 (0x03): $authString');
try {
// 1. 获取 Either 结果
// 🔥 修复:使用 RemoteControlCubit.targetDevice,而不是传入的 deviceName
final remoteControlCubit = GetIt.I<RemoteControlCubit>();
final targetDevice = remoteControlCubit.state.targetDevice;
if (targetDevice != null) {
debugPrint('🔀 [TCP] 使用 targetDevice 进行切换: ${targetDevice.deviceName}');
await switchDeviceUseCase.deviceRepository.switchDevice(
"app",
targetDevice.deviceName,
);
} else {
// 兜底:如果 targetDevice 为 null,使用传入的 deviceName
debugPrint('⚠️ [TCP] targetDevice 为 null,使用传入的 deviceName: $deviceName');
await switchDeviceUseCase.deviceRepository.switchDevice(
"app",
deviceName,
);
}
} catch (e) {
//debugPrint('❌ 被动[AuthTcp] 设备订阅流程异常:$e');
_logger.logWithLevel('❌ 被动[AuthTcp] 设备订阅流程异常:$e');

View File

@@ -9,6 +9,7 @@ import 'package:maibu_satabot_v2/core/network/tcp/tcp_client.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_state.dart'; // 👈 必须加
import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/remote_control_repository.dart';
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_cubit.dart';
import '../../../devices/domain/entities/device_entity.dart';
import '../bloc/permission_request_bloc.dart';
@@ -44,6 +45,17 @@ class _HomePageState extends State<HomePage> {
final devicesCubit = context.read<DevicesCubit>();
// 🔥 修复:检查是否已有 targetDevice,如果有则不需要加载设备列表
final remoteControlState = context.read<RemoteControlCubit>().state;
if (remoteControlState.targetDevice != null) {
debugPrint('✅ [HomePage] 已有 targetDevice: ${remoteControlState.targetDevice!.deviceName},跳过设备列表加载');
setState(() {
_isDevicesLoading = false;
_hasLoadedDevices = true;
});
return;
}
// 🔥 关键:等待设备加载完成
await devicesCubit.fetchAllDevices(username);

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/app/app_user_cubit.dart';
import '../../../devices/presentation/bloc/devices_cubit.dart';
import '../../../remote_control/presentation/bloc/remote_control_cubit.dart';
import '../widgets/map/test_amap_page.dart';
import '../widgets/map/testmap_pages.dart';
@@ -11,17 +11,87 @@ class RoutePlanPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 同时监听用户和设备状态
final userState = context.read<AppUserCubit>().state;
final deviceState = context.read<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
try {
// 🔥 修复:使用 RemoteControlCubit 的 targetDevice 作为唯一设备数据源
final userState = context.watch<AppUserCubit>().state;
final remoteControlState = context.watch<RemoteControlCubit>().state;
final targetDevice = remoteControlState.targetDevice;
if (currentDevice == null)
return const Scaffold(body: Center(child: Text("加载中...")));
debugPrint('🔍 [RoutePlanPage] targetDevice: ${targetDevice?.deviceName ?? "null"}');
// 检查是否有选中的设备
if (targetDevice == null) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.devices_other,
size: 64,
color: Color(0xFF86909C),
),
const SizedBox(height: 16),
const Text(
'请先选择设备',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1D2129),
),
),
const SizedBox(height: 8),
Text(
'在设备列表或场站中选择要控制的设备',
style: TextStyle(
fontSize: 12,
color: Color(0xFF86909C),
),
),
],
),
),
);
}
return Scaffold(
backgroundColor: const Color(0xFFF7F7F7),
body: MapPageEnterprise(),
);
} catch (e, stackTrace) {
debugPrint('❌ [RoutePlanPage] 构建错误: $e');
debugPrint('堆栈跟踪: $stackTrace');
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
const Text(
'页面加载失败',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1D2129),
),
),
const SizedBox(height: 8),
Text(
'错误: $e',
style: const TextStyle(
fontSize: 12,
color: Colors.red,
),
),
],
),
),
);
}
}
}

View File

@@ -192,10 +192,14 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
@override
void initState() {
super.initState();
debugPrint('🚀 [MapPage] initState 开始');
_currentStation = _stationList.first;
_traceManager = TracePoint<PlotPoint>();
_loadSavedData();
// 🔥 修复:异步加载数据,避免阻塞
_loadSavedData().catchError((e) {
debugPrint('❌ [MapPage] 加载本地数据失败: $e');
});
// 示例:切换到导航模式
_traceManager.setMode(TPMode.LOCATION);
@@ -204,7 +208,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 🔥 核心修复:使用 addPostFrameCallback 延迟获取地图中心(渲染完成后执行)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _mapController.camera != null) {
if (!mounted) return;
debugPrint('🗺️ [MapPage] addPostFrameCallback 执行');
if (_mapController.camera != null) {
final originalCenter = _mapController.camera!.center;
final highPrecisionCenter = LatLng(
originalCenter.latitude,
@@ -242,6 +249,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
}
}
});
debugPrint('✅ [MapPage] initState 完成');
}
// 🔥 核心修复:监听路径规划应答流
@@ -2835,7 +2844,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
}
return BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
final devicesState = context.watch<DevicesCubit>().state;
// 🔥 修复:使用 read 而非 watch,避免不必要的重建和接口调用
final devicesState = context.read<DevicesCubit>().state;
bool isFinishWork = devicesState.isFinshWork ?? false;
double? arriLatitude = devicesState.arriLatitude;
double? arriLongitude = devicesState.arriLongitude;

View File

@@ -1,11 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:volc_engine_rtc/volc_engine_rtc.dart' as volc;
import 'package:agora_rtc_engine/agora_rtc_engine.dart' as agora;
import '../../../../../core/di/injection.dart';
import '../../domain/entities/drone_station_entity.dart';
import '../../domain/entities/video_stream_entity.dart';
import '../bloc/drone_station_bloc.dart';
import '../bloc/drone_station_event.dart';
import '../bloc/drone_station_state.dart';
@@ -14,9 +11,6 @@ import 'drone_mission_control_page.dart';
import 'drone_monitor_page.dart';
import '../widgets/flight_task_selector_modal.dart';
// SDK 类型枚举
enum RtcSdkType { volcengine, agora }
class DroneStationDetailPage extends StatefulWidget {
final DroneStationEntity station;
@@ -33,30 +27,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
UAVDetailEntity? _detail;
String? _droneSn;
// 悬浮视频监控状态
bool showFloatingMonitor = false;
bool isFloatingIndoor = true;
Offset floatingPosition = const Offset(20, 200);
bool _isFloatingMonitorEnabled = true; // 悬浮窗默认开启
// 视频流状态
VideoStreamEntity? _floatingVideoStream;
bool _isFloatingLoading = false;
String? _floatingErrorMessage;
String? _floatingRemoteUserId;
bool _isFloatingAgora = false;
// 火山引擎 RTC
volc.RTCEngine? _floatingRtcEngine;
volc.RTCRoom? _floatingRtcRoom;
volc.RTCViewContext? _floatingRemoteRenderContext;
// Agora RTC
agora.RtcEngine? _floatingAgoraEngine;
// 加载超时计时器
Timer? _floatingLoadingTimer;
static const _floatingLoadingTimeout = Duration(seconds: 15);
// 无人机状态轮询计时器
Timer? _droneStatusPollingTimer;
@@ -76,35 +47,11 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
_startDroneStatusPolling();
}
/// 检查并自动打开悬浮窗
void _checkAndShowFloatingMonitor(UAVDetailEntity detail) {
debugPrint('🔍 检查悬浮窗条件:');
debugPrint(' - 开关状态: $_isFloatingMonitorEnabled');
debugPrint(' - 是否已显示: $showFloatingMonitor');
debugPrint(' - 无人机在线: ${detail.droneOnlineStatus}');
debugPrint(' - 机场摄像头: ${detail.gatewayCameraList?.length ?? 0}');
if (!_isFloatingMonitorEnabled || showFloatingMonitor) {
debugPrint('❌ 不满足条件,退出检查');
return; // 开关关闭或已显示,不执行
}
// 无人机在线且有机场摄像头,自动打开悬浮窗
if (detail.droneOnlineStatus == 1 &&
detail.gatewayCameraList != null &&
detail.gatewayCameraList!.isNotEmpty) {
debugPrint('✅ 检测到无人机在线,自动打开悬浮窗');
_loadFloatingVideoStream();
} else {
debugPrint('❌ 无人机离线或无摄像头');
}
}
@override
void dispose() {
_bloc.close();
_destroyFloatingRtcEngine();
_floatingLoadingTimer?.cancel();
_droneStatusPollingTimer?.cancel(); // 停止轮询
super.dispose();
}
@@ -123,8 +70,8 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
final currentState = _bloc.state;
if (currentState is UAVDetailLoaded) {
if (currentState.detail.droneOnlineStatus == 1) {
// 无人机在线时,每15秒轮询一次
interval = const Duration(seconds: 15);
// 无人机在线时,每30秒轮询一次
interval = const Duration(seconds: 30);
} else {
// 无人机离线时,每60秒轮询一次(降低频率)
interval = const Duration(seconds: 60);
@@ -143,53 +90,12 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
),
);
// 如果悬浮窗开启且无人机上线,自动显示悬浮窗
if (_isFloatingMonitorEnabled) {
final state = _bloc.state;
if (state is UAVDetailLoaded &&
state.detail.droneOnlineStatus == 1 &&
!showFloatingMonitor) {
debugPrint('✅ 无人机已上线,自动打开悬浮窗');
_loadFloatingVideoStream();
}
}
// 重新调度下一次轮询(动态周期)
_scheduleDroneStatusPoll();
});
}
// 销毁悬浮窗的 RTC 引擎
void _destroyFloatingRtcEngine() async {
// 销毁火山引擎 RTC
if (_floatingRtcRoom != null) {
try {
await _floatingRtcRoom?.leaveRoom();
} catch (_) {}
_floatingRtcRoom = null;
}
if (_floatingRtcEngine != null) {
try {
_floatingRtcEngine?.destroy();
} catch (_) {}
_floatingRtcEngine = null;
}
// 销毁 Agora RTC
if (_floatingAgoraEngine != null) {
try {
await _floatingAgoraEngine?.leaveChannel();
} catch (_) {}
try {
_floatingAgoraEngine?.release();
} catch (_) {}
_floatingAgoraEngine = null;
}
_floatingRemoteRenderContext = null;
_floatingRemoteUserId = null;
_isFloatingAgora = false;
}
@override
Widget build(BuildContext context) {
@@ -215,19 +121,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
centerTitle: true,
),
body: BlocConsumer<DroneStationBloc, DroneStationState>(
listener: (context, state) {
// 监听视频流加载状态
if (state is VideoStreamLoaded) {
_floatingLoadingTimer?.cancel();
setState(() {
_floatingVideoStream = state.videoStream;
});
_initFloatingRtcEngine();
} else if (state is VideoStreamError) {
_floatingLoadingTimer?.cancel();
setState(() {});
}
},
listener: (context, state) {},
builder: (context, state) {
if (state is UAVDetailLoading) {
return const Center(
@@ -274,8 +168,6 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
}
if (state is UAVDetailLoaded) {
// 检查是否自动打开悬浮窗
_checkAndShowFloatingMonitor(state.detail);
return _buildContent(state.detail);
}
@@ -289,9 +181,10 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
Widget _buildContent(UAVDetailEntity detail) {
_detail = detail; // 保存详情数据供其他方法使用
_droneSn = detail.deviceSn; // 保存无人机序列号
return Stack(
children: [
ListView(
return RefreshIndicator(
onRefresh: _handleRefresh,
color: const Color(0xFF165DFF),
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildAirportStatusCard(detail),
@@ -304,11 +197,22 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
const SizedBox(height: 20),
],
),
_buildFloatingMonitor(),
],
);
}
/// 处理下拉刷新
Future<void> _handleRefresh() async {
// 重新加载无人机详情
_bloc.add(
UAVDetailLoad(
gatewaySn: widget.station.gatewaySn,
deviceSn: widget.station.deviceSn,
),
);
// 重置轮询计时器,使用新的状态
_scheduleDroneStatusPoll();
}
Widget _buildMonitorCard() {
return GestureDetector(
onTap: _goToMonitor,
@@ -693,37 +597,6 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
),
),
),
// 悬浮观看功能已禁用
// GestureDetector(
// onTap: () {
// Navigator.pop(context);
// _loadFloatingVideoStream();
// },
// child: Container(
// padding: const EdgeInsets.all(16),
// decoration: BoxDecoration(
// color: const Color(0xFFF2F3F5),
// borderRadius: BorderRadius.circular(12),
// ),
// child: const Row(
// children: [
// Icon(Icons.picture_in_picture, color: Color(0xFF165DFF)),
// SizedBox(width: 16),
// Text(
// '悬浮观看',
// style: TextStyle(
// fontSize: 16,
// color: Color(0xFF1D2129),
// fontWeight: FontWeight.w500,
// ),
// ),
// Spacer(),
// Icon(Icons.arrow_forward_ios, color: Color(0xFF86909C)),
// ],
// ),
// ),
// ),
// const SizedBox(height: 16),
],
),
);
@@ -771,146 +644,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
);
}
// 悬浮视频监控组件
Widget _buildFloatingMonitor() {
if (!showFloatingMonitor) return const SizedBox();
return Positioned(
left: floatingPosition.dx,
top: floatingPosition.dy,
child: Draggable(
feedback: _monitorFloatCard(),
childWhenDragging: const SizedBox(),
onDragEnd: (details) {
setState(() {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
floatingPosition = Offset(
details.offset.dx.clamp(0, screenWidth - 280),
details.offset.dy.clamp(0, screenHeight - 200),
);
});
},
child: _monitorFloatCard(),
),
);
}
Widget _monitorFloatCard() {
return Container(
width: 280,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x20000000),
blurRadius: 12,
offset: Offset(0, 4),
),
],
),
child: Column(
children: [
// 视频区域
Container(
height: 140,
color: const Color(0xFF0D1117),
child: const Center(
child: Icon(
Icons.video_library,
size: 48,
color: Color(0xFF4E5969),
),
),
),
// 控制区域
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
// 室内/室外切换
Expanded(
child: GestureDetector(
onTap: () => setState(() => isFloatingIndoor = true),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 6,
horizontal: 12,
),
decoration: BoxDecoration(
color: isFloatingIndoor
? const Color(0xFF165DFF)
: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'室内',
style: TextStyle(
fontSize: 12,
color: isFloatingIndoor
? Colors.white
: const Color(0xFF86909C),
),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: GestureDetector(
onTap: () {
setState(() => isFloatingIndoor = false);
_loadFloatingVideoStream();
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 6,
horizontal: 12,
),
decoration: BoxDecoration(
color: !isFloatingIndoor
? const Color(0xFF165DFF)
: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'室外',
style: TextStyle(
fontSize: 12,
color: !isFloatingIndoor
? Colors.white
: const Color(0xFF86909C),
),
),
),
),
),
// 关闭按钮
GestureDetector(
onTap: () {
_destroyFloatingRtcEngine();
setState(() {
showFloatingMonitor = false;
_isFloatingMonitorEnabled = false; // 关闭开关
});
},
child: const Padding(
padding: EdgeInsets.all(4),
child: Icon(
Icons.close,
size: 18,
color: Color(0xFF86909C),
),
),
),
],
),
),
],
),
);
}
Widget _buildQuickActionButton({
required IconData icon,
@@ -1004,350 +738,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
);
}
// 加载悬浮视频流
void _loadFloatingVideoStream() {
_floatingLoadingTimer?.cancel();
// 从当前状态中获取摄像头索引
final currentState = _bloc.state;
String cameraIndex = '165-0-7'; // 默认值
if (currentState is UAVDetailLoaded &&
currentState.detail.gatewayCameraList != null &&
currentState.detail.gatewayCameraList!.isNotEmpty) {
cameraIndex = currentState.detail.gatewayCameraList!.first.cameraIndex;
}
setState(() {
_isFloatingLoading = true;
_floatingErrorMessage = null;
showFloatingMonitor = true;
_isFloatingMonitorEnabled = true; // 开启开关
});
_destroyFloatingRtcEngine();
// 设置加载超时计时器
_floatingLoadingTimer = Timer(_floatingLoadingTimeout, () {
if (!mounted) return;
if (_isFloatingLoading) {
debugPrint('⚠️ 悬浮窗视频加载超时');
setState(() {
_isFloatingLoading = false;
_floatingErrorMessage = '视频加载超时,请检查网络连接或点击刷新重试';
});
}
});
_bloc.add(
VideoStreamLoad(
sn: widget.station.gatewaySn,
cameraIndex: cameraIndex,
cameraPosition: isFloatingIndoor ? 'indoor' : 'outdoor',
),
);
}
// 初始化悬浮窗的 RTC 引擎
Future<void> _initFloatingRtcEngine() async {
if (_floatingVideoStream == null) return;
final appId = _floatingVideoStream!.appId;
final roomId = _floatingVideoStream!.roomId;
final token = _floatingVideoStream!.token;
final userId = _floatingVideoStream!.userId.isNotEmpty
? _floatingVideoStream!.userId
: 'user_${DateTime.now().millisecondsSinceEpoch}';
if (appId.isEmpty || roomId.isEmpty || token.isEmpty) {
setState(() {
_floatingErrorMessage = 'RTC 参数缺失';
_isFloatingLoading = false;
});
return;
}
debugPrint('=== 悬浮窗 RTC 初始化 ===');
debugPrint('AppId: $appId');
debugPrint('RoomId: $roomId');
debugPrint('UserId: $userId');
debugPrint('URL Type: ${_floatingVideoStream!.urlType}');
final sdkType = _floatingVideoStream!.urlType.toLowerCase() == 'agora'
? RtcSdkType.agora
: RtcSdkType.volcengine;
if (sdkType == RtcSdkType.agora) {
await _initFloatingAgoraEngine(appId, roomId, token, userId);
} else {
await _initFloatingVolcEngine(appId, roomId, token, userId);
}
}
// 初始化悬浮窗的 Agora 引擎
Future<void> _initFloatingAgoraEngine(
String appId,
String channelId,
String token,
String userId,
) async {
try {
debugPrint('=== Agora 悬浮窗初始化 ===');
_floatingAgoraEngine = agora.createAgoraRtcEngine();
await _floatingAgoraEngine!.initialize(
agora.RtcEngineContext(appId: appId),
);
debugPrint('Agora 引擎初始化成功');
// 启用视频模块
_floatingAgoraEngine!.enableVideo();
debugPrint('Agora 视频模块已启用');
_floatingAgoraEngine!.registerEventHandler(
agora.RtcEngineEventHandler(
onJoinChannelSuccess: (agora.RtcConnection connection, int elapsed) {
debugPrint('✅ 悬浮窗 Agora 加入频道成功: ${connection.channelId}');
},
onUserJoined: (agora.RtcConnection connection, int uid, int elapsed) {
debugPrint('✅ 悬浮窗 Agora 用户加入: uid=$uid');
setState(() {
_floatingRemoteUserId = uid.toString();
_isFloatingAgora = true;
_isFloatingLoading = false;
});
},
onUserOffline:
(
agora.RtcConnection connection,
int uid,
agora.UserOfflineReasonType reason,
) {
debugPrint('悬浮窗 Agora 用户离开: $uid');
if (uid.toString() == _floatingRemoteUserId) {
setState(() {
_floatingRemoteUserId = null;
});
}
},
onError: (agora.ErrorCodeType err, String msg) {
debugPrint('❌ 悬浮窗 Agora 错误: $err - $msg');
setState(() {
_floatingErrorMessage = 'Agora RTC 错误:$err';
_isFloatingLoading = false;
});
},
),
);
await _floatingAgoraEngine!.joinChannel(
token: token,
channelId: channelId,
uid: int.tryParse(userId) ?? 0,
options: agora.ChannelMediaOptions(
channelProfile:
agora.ChannelProfileType.channelProfileLiveBroadcasting,
clientRoleType: agora.ClientRoleType.clientRoleAudience,
autoSubscribeVideo: true,
autoSubscribeAudio: false,
),
);
} catch (e) {
debugPrint('❌ 悬浮窗 Agora RTC 初始化失败: $e');
setState(() {
_floatingErrorMessage = 'Agora RTC 初始化失败:$e';
_isFloatingLoading = false;
});
}
}
// 初始化悬浮窗的火山引擎
Future<void> _initFloatingVolcEngine(
String appId,
String roomId,
String token,
String userId,
) async {
try {
debugPrint('=== 悬浮窗 VolcEngine 初始化 ===');
final engineEventHandler = volc.IRTCEngineEventHandler(
onWarning: (volc.WarningCode code) {
debugPrint('Volc 悬浮窗 Warning: $code');
},
onError: (volc.ErrorCode code) {
debugPrint('Volc 悬浮窗 Error: $code');
setState(() {
_floatingErrorMessage = 'Volc RTC 错误:$code';
_isFloatingLoading = false;
});
},
onConnectionStateChanged: (state, reason) {
debugPrint(
'Volc 悬浮窗 Connection State Changed: $state, reason: $reason',
);
},
);
_floatingRtcEngine = await volc.RTCEngine.createRTCEngine(
volc.RTCVideoContext(appId: appId, eventHandler: engineEventHandler),
);
if (_floatingRtcEngine == null) {
debugPrint('Volc 悬浮窗引擎创建失败');
setState(() {
_floatingErrorMessage = 'Volc RTC 引擎创建失败';
_isFloatingLoading = false;
});
return;
}
_floatingRtcRoom = await _floatingRtcEngine!.createRTCRoom(roomId);
if (_floatingRtcRoom == null) {
debugPrint('Volc 悬浮窗房间创建失败');
setState(() {
_floatingErrorMessage = 'Volc RTC 房间创建失败';
_isFloatingLoading = false;
});
return;
}
final roomEventHandler = volc.IRTCRoomEventHandler(
onUserPublishStreamVideo:
(String userId, volc.StreamInfo streamInfo, bool isPublish) {
debugPrint('Volc 悬浮窗 远端用户 $userId 视频流状态: $isPublish');
setState(() {
if (isPublish) {
_floatingRemoteUserId = userId;
_isFloatingAgora = false;
_floatingRemoteRenderContext =
volc.RTCViewContext.remoteContext(
roomId: roomId,
userId: userId,
);
_isFloatingLoading = false;
} else {
if (userId == _floatingRemoteUserId) {
_floatingRemoteRenderContext = null;
_floatingRemoteUserId = null;
}
}
});
},
onUserLeave: (String userId, int reason) {
debugPrint('Volc 悬浮窗 用户离开: $userId');
if (userId == _floatingRemoteUserId) {
setState(() {
_floatingRemoteRenderContext = null;
_floatingRemoteUserId = null;
});
}
},
);
await _floatingRtcRoom?.setRTCRoomEventHandler(roomEventHandler);
await _floatingRtcRoom?.joinRoom(
token: token,
userInfo: volc.UserInfo(userId: userId, extraInfo: ''),
userVisibility: true,
roomConfig: volc.RoomConfig(
isPublishAudio: false,
isPublishVideo: false,
isAutoSubscribeAudio: false,
isAutoSubscribeVideo: true,
),
);
} catch (e) {
debugPrint('❌ 悬浮窗 Volc RTC 初始化失败: $e');
setState(() {
_floatingErrorMessage = 'Volc RTC 初始化失败:$e';
_isFloatingLoading = false;
});
}
}
// 构建悬浮窗视频内容
Widget _buildFloatingVideoContent() {
if (_isFloatingLoading) {
return Container(
height: 140,
color: const Color(0xFF0D1117),
child: const Center(
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
),
);
}
if (_floatingErrorMessage != null) {
return Container(
height: 140,
color: const Color(0xFF0D1117),
child: Center(
child: Text(
_floatingErrorMessage!,
style: const TextStyle(color: Colors.red, fontSize: 12),
textAlign: TextAlign.center,
),
),
);
}
if (_floatingRemoteUserId == null) {
return Container(
height: 140,
color: const Color(0xFF0D1117),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.video_camera_front, size: 32, color: Colors.grey),
SizedBox(height: 8),
Text(
'等待视频流...',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
),
);
}
if (_isFloatingAgora && _floatingAgoraEngine != null) {
return SizedBox(
height: 140,
child: agora.AgoraVideoView(
controller: agora.VideoViewController.remote(
rtcEngine: _floatingAgoraEngine!,
canvas: agora.VideoCanvas(uid: int.parse(_floatingRemoteUserId!)),
connection: agora.RtcConnection(
channelId: _floatingVideoStream?.roomId ?? '',
),
),
),
);
}
if (!_isFloatingAgora && _floatingRemoteRenderContext != null) {
return SizedBox(
height: 140,
child: volc.RTCSurfaceView(
context: _floatingRemoteRenderContext!,
renderMode: volc.VideoRenderMode.hidden,
),
);
}
return Container(
height: 140,
color: const Color(0xFF0D1117),
child: const Center(
child: Text(
'视频初始化中...',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
),
);
}
Widget _buildCameraListRow(String label, List<CameraInfo> cameras) {
String value = cameras

View File

@@ -95,7 +95,7 @@ class DroneStationItemCard extends StatelessWidget {
),
),
// 机场状态徽章
_buildStatusBadge('机场', station.isOnline),
_buildStatusBadge(station.isOnline),
],
),
),
@@ -224,9 +224,9 @@ class DroneStationItemCard extends StatelessWidget {
}
/// 状态徽章
Widget _buildStatusBadge(String label, bool isOnline) {
Widget _buildStatusBadge( bool isOnline) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isOnline
@@ -257,7 +257,7 @@ class DroneStationItemCard extends StatelessWidget {
),
const SizedBox(width: 5),
Text(
'$label\n${isOnline ? '在线' : '离线'}',
'${isOnline ? '在线' : '离线'}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,