1更新无人机的摄像头视频显示的条件为在离线情况下的自动刷新+下拉刷新的机制。
2更新优化去除原有路径规划的初始化阶段的获取设备的接口请求,改为使用新的 targetDevice。 3修改无人机机场的在线状态简化为在线和离线。去掉机场二字。 4.更新同一账号同时段登录,被强制下线的友好提示!
This commit is contained in:
@@ -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 结果
|
||||
await switchDeviceUseCase.deviceRepository.switchDevice(
|
||||
"app",
|
||||
deviceName,
|
||||
);
|
||||
// 🔥 修复:使用 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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"}');
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F7F7),
|
||||
body: MapPageEnterprise(),
|
||||
);
|
||||
// 检查是否有选中的设备
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,24 +181,36 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
Widget _buildContent(UAVDetailEntity detail) {
|
||||
_detail = detail; // 保存详情数据供其他方法使用
|
||||
_droneSn = detail.deviceSn; // 保存无人机序列号
|
||||
return Stack(
|
||||
children: [
|
||||
ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildAirportStatusCard(detail),
|
||||
const SizedBox(height: 12),
|
||||
_buildMonitorCard(),
|
||||
const SizedBox(height: 12),
|
||||
_buildDroneStatusCard(detail),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActions(),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
_buildFloatingMonitor(),
|
||||
],
|
||||
return RefreshIndicator(
|
||||
onRefresh: _handleRefresh,
|
||||
color: const Color(0xFF165DFF),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildAirportStatusCard(detail),
|
||||
const SizedBox(height: 12),
|
||||
_buildMonitorCard(),
|
||||
const SizedBox(height: 12),
|
||||
_buildDroneStatusCard(detail),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActions(),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 处理下拉刷新
|
||||
Future<void> _handleRefresh() async {
|
||||
// 重新加载无人机详情
|
||||
_bloc.add(
|
||||
UAVDetailLoad(
|
||||
gatewaySn: widget.station.gatewaySn,
|
||||
deviceSn: widget.station.deviceSn,
|
||||
),
|
||||
);
|
||||
// 重置轮询计时器,使用新的状态
|
||||
_scheduleDroneStatusPoll();
|
||||
}
|
||||
|
||||
Widget _buildMonitorCard() {
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user