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:flutter/cupertino.dart';
|
||||||
import 'package:get_it/get_it.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/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/auth/data/datasources/auth_tcp_datasource.dart';
|
||||||
import '../../../features/devices/data/models/route_plan_send_entity.dart';
|
import '../../../features/devices/data/models/route_plan_send_entity.dart';
|
||||||
@@ -273,11 +274,40 @@ class TcpClient {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_reconnectTimer = Timer(const Duration(seconds: 5), () {
|
_reconnectTimer = Timer(const Duration(seconds: 5), () async {
|
||||||
_reconnectTimer = null;
|
_reconnectTimer = null;
|
||||||
//debugPrint('⏰ 被动-定时器触发,开始执行重连...');
|
//debugPrint('⏰ 被动-定时器触发,开始执行重连...');
|
||||||
_logger.logWithLevel('⏰ 被动-定时器触发,开始执行重连...', shouldLog: true);
|
_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);
|
await _sendAuthPacketBySwitch(devname);
|
||||||
}
|
}
|
||||||
@@ -808,11 +838,24 @@ class TcpClient {
|
|||||||
_logger.logWithLevel('🔑 被动[TCP] 已发送认证包 (0x03): $authString');
|
_logger.logWithLevel('🔑 被动[TCP] 已发送认证包 (0x03): $authString');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 获取 Either 结果
|
// 🔥 修复:使用 RemoteControlCubit.targetDevice,而不是传入的 deviceName
|
||||||
await switchDeviceUseCase.deviceRepository.switchDevice(
|
final remoteControlCubit = GetIt.I<RemoteControlCubit>();
|
||||||
"app",
|
final targetDevice = remoteControlCubit.state.targetDevice;
|
||||||
deviceName,
|
|
||||||
);
|
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) {
|
} catch (e) {
|
||||||
//debugPrint('❌ 被动[AuthTcp] 设备订阅流程异常:$e');
|
//debugPrint('❌ 被动[AuthTcp] 设备订阅流程异常:$e');
|
||||||
_logger.logWithLevel('❌ 被动[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_cubit.dart';
|
||||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_state.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/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 '../../../devices/domain/entities/device_entity.dart';
|
||||||
import '../bloc/permission_request_bloc.dart';
|
import '../bloc/permission_request_bloc.dart';
|
||||||
@@ -44,6 +45,17 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
final devicesCubit = context.read<DevicesCubit>();
|
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);
|
await devicesCubit.fetchAllDevices(username);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../../core/app/app_user_cubit.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/test_amap_page.dart';
|
||||||
import '../widgets/map/testmap_pages.dart';
|
import '../widgets/map/testmap_pages.dart';
|
||||||
|
|
||||||
@@ -11,17 +11,87 @@ class RoutePlanPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 同时监听用户和设备状态
|
try {
|
||||||
final userState = context.read<AppUserCubit>().state;
|
// 🔥 修复:使用 RemoteControlCubit 的 targetDevice 作为唯一设备数据源
|
||||||
final deviceState = context.read<DevicesCubit>().state;
|
final userState = context.watch<AppUserCubit>().state;
|
||||||
final currentDevice = deviceState.selectedDevice;
|
final remoteControlState = context.watch<RemoteControlCubit>().state;
|
||||||
|
final targetDevice = remoteControlState.targetDevice;
|
||||||
|
|
||||||
if (currentDevice == null)
|
debugPrint('🔍 [RoutePlanPage] targetDevice: ${targetDevice?.deviceName ?? "null"}');
|
||||||
return const Scaffold(body: Center(child: Text("加载中...")));
|
|
||||||
|
|
||||||
return Scaffold(
|
// 检查是否有选中的设备
|
||||||
backgroundColor: const Color(0xFFF7F7F7),
|
if (targetDevice == null) {
|
||||||
body: MapPageEnterprise(),
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
debugPrint('🚀 [MapPage] initState 开始');
|
||||||
_currentStation = _stationList.first;
|
_currentStation = _stationList.first;
|
||||||
_traceManager = TracePoint<PlotPoint>();
|
_traceManager = TracePoint<PlotPoint>();
|
||||||
|
|
||||||
_loadSavedData();
|
// 🔥 修复:异步加载数据,避免阻塞
|
||||||
|
_loadSavedData().catchError((e) {
|
||||||
|
debugPrint('❌ [MapPage] 加载本地数据失败: $e');
|
||||||
|
});
|
||||||
|
|
||||||
// 示例:切换到导航模式
|
// 示例:切换到导航模式
|
||||||
_traceManager.setMode(TPMode.LOCATION);
|
_traceManager.setMode(TPMode.LOCATION);
|
||||||
@@ -204,7 +208,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
|||||||
|
|
||||||
// 🔥 核心修复:使用 addPostFrameCallback 延迟获取地图中心(渲染完成后执行)
|
// 🔥 核心修复:使用 addPostFrameCallback 延迟获取地图中心(渲染完成后执行)
|
||||||
WidgetsBinding.instance.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 originalCenter = _mapController.camera!.center;
|
||||||
final highPrecisionCenter = LatLng(
|
final highPrecisionCenter = LatLng(
|
||||||
originalCenter.latitude,
|
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>(
|
return BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final devicesState = context.watch<DevicesCubit>().state;
|
// 🔥 修复:使用 read 而非 watch,避免不必要的重建和接口调用
|
||||||
|
final devicesState = context.read<DevicesCubit>().state;
|
||||||
bool isFinishWork = devicesState.isFinshWork ?? false;
|
bool isFinishWork = devicesState.isFinshWork ?? false;
|
||||||
double? arriLatitude = devicesState.arriLatitude;
|
double? arriLatitude = devicesState.arriLatitude;
|
||||||
double? arriLongitude = devicesState.arriLongitude;
|
double? arriLongitude = devicesState.arriLongitude;
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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 '../../../../../core/di/injection.dart';
|
||||||
import '../../domain/entities/drone_station_entity.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_bloc.dart';
|
||||||
import '../bloc/drone_station_event.dart';
|
import '../bloc/drone_station_event.dart';
|
||||||
import '../bloc/drone_station_state.dart';
|
import '../bloc/drone_station_state.dart';
|
||||||
@@ -14,9 +11,6 @@ import 'drone_mission_control_page.dart';
|
|||||||
import 'drone_monitor_page.dart';
|
import 'drone_monitor_page.dart';
|
||||||
import '../widgets/flight_task_selector_modal.dart';
|
import '../widgets/flight_task_selector_modal.dart';
|
||||||
|
|
||||||
// SDK 类型枚举
|
|
||||||
enum RtcSdkType { volcengine, agora }
|
|
||||||
|
|
||||||
class DroneStationDetailPage extends StatefulWidget {
|
class DroneStationDetailPage extends StatefulWidget {
|
||||||
final DroneStationEntity station;
|
final DroneStationEntity station;
|
||||||
|
|
||||||
@@ -33,30 +27,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
UAVDetailEntity? _detail;
|
UAVDetailEntity? _detail;
|
||||||
String? _droneSn;
|
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;
|
Timer? _droneStatusPollingTimer;
|
||||||
@@ -76,35 +47,11 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
_startDroneStatusPolling();
|
_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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_bloc.close();
|
_bloc.close();
|
||||||
_destroyFloatingRtcEngine();
|
|
||||||
_floatingLoadingTimer?.cancel();
|
|
||||||
_droneStatusPollingTimer?.cancel(); // 停止轮询
|
_droneStatusPollingTimer?.cancel(); // 停止轮询
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -123,8 +70,8 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
final currentState = _bloc.state;
|
final currentState = _bloc.state;
|
||||||
if (currentState is UAVDetailLoaded) {
|
if (currentState is UAVDetailLoaded) {
|
||||||
if (currentState.detail.droneOnlineStatus == 1) {
|
if (currentState.detail.droneOnlineStatus == 1) {
|
||||||
// 无人机在线时,每15秒轮询一次
|
// 无人机在线时,每30秒轮询一次
|
||||||
interval = const Duration(seconds: 15);
|
interval = const Duration(seconds: 30);
|
||||||
} else {
|
} else {
|
||||||
// 无人机离线时,每60秒轮询一次(降低频率)
|
// 无人机离线时,每60秒轮询一次(降低频率)
|
||||||
interval = const Duration(seconds: 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();
|
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -215,19 +121,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: BlocConsumer<DroneStationBloc, DroneStationState>(
|
body: BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {},
|
||||||
// 监听视频流加载状态
|
|
||||||
if (state is VideoStreamLoaded) {
|
|
||||||
_floatingLoadingTimer?.cancel();
|
|
||||||
setState(() {
|
|
||||||
_floatingVideoStream = state.videoStream;
|
|
||||||
});
|
|
||||||
_initFloatingRtcEngine();
|
|
||||||
} else if (state is VideoStreamError) {
|
|
||||||
_floatingLoadingTimer?.cancel();
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is UAVDetailLoading) {
|
if (state is UAVDetailLoading) {
|
||||||
return const Center(
|
return const Center(
|
||||||
@@ -274,8 +168,6 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state is UAVDetailLoaded) {
|
if (state is UAVDetailLoaded) {
|
||||||
// 检查是否自动打开悬浮窗
|
|
||||||
_checkAndShowFloatingMonitor(state.detail);
|
|
||||||
return _buildContent(state.detail);
|
return _buildContent(state.detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,24 +181,36 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
Widget _buildContent(UAVDetailEntity detail) {
|
Widget _buildContent(UAVDetailEntity detail) {
|
||||||
_detail = detail; // 保存详情数据供其他方法使用
|
_detail = detail; // 保存详情数据供其他方法使用
|
||||||
_droneSn = detail.deviceSn; // 保存无人机序列号
|
_droneSn = detail.deviceSn; // 保存无人机序列号
|
||||||
return Stack(
|
return RefreshIndicator(
|
||||||
children: [
|
onRefresh: _handleRefresh,
|
||||||
ListView(
|
color: const Color(0xFF165DFF),
|
||||||
padding: const EdgeInsets.all(16),
|
child: ListView(
|
||||||
children: [
|
padding: const EdgeInsets.all(16),
|
||||||
_buildAirportStatusCard(detail),
|
children: [
|
||||||
const SizedBox(height: 12),
|
_buildAirportStatusCard(detail),
|
||||||
_buildMonitorCard(),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
_buildMonitorCard(),
|
||||||
_buildDroneStatusCard(detail),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
_buildDroneStatusCard(detail),
|
||||||
_buildQuickActions(),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 20),
|
_buildQuickActions(),
|
||||||
],
|
const SizedBox(height: 20),
|
||||||
),
|
],
|
||||||
_buildFloatingMonitor(),
|
),
|
||||||
],
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理下拉刷新
|
||||||
|
Future<void> _handleRefresh() async {
|
||||||
|
// 重新加载无人机详情
|
||||||
|
_bloc.add(
|
||||||
|
UAVDetailLoad(
|
||||||
|
gatewaySn: widget.station.gatewaySn,
|
||||||
|
deviceSn: widget.station.deviceSn,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
// 重置轮询计时器,使用新的状态
|
||||||
|
_scheduleDroneStatusPoll();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMonitorCard() {
|
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({
|
Widget _buildQuickActionButton({
|
||||||
required IconData icon,
|
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) {
|
Widget _buildCameraListRow(String label, List<CameraInfo> cameras) {
|
||||||
String value = 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(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: isOnline
|
colors: isOnline
|
||||||
@@ -257,7 +257,7 @@ class DroneStationItemCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
Text(
|
Text(
|
||||||
'$label\n${isOnline ? '在线' : '离线'}',
|
'${isOnline ? '在线' : '离线'}',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
|||||||
Reference in New Issue
Block a user