远程遥控视频的显示bug修复;恢复远程遥控中的前置推送检查和动态实时信息的展示。
This commit is contained in:
@@ -35,7 +35,8 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
||||
final NetMessageDispatcher dispatcher;
|
||||
final DeviceStatusBloc deviceStatusBloc; // 🔥 注入 DeviceStatusBloc
|
||||
StreamSubscription? _deviceStatusSub; // 🔥 订阅 DeviceStatusBloc 的状态流
|
||||
StreamSubscription? _deviceStatusSub; // 🔥 订阅 DeviceStatusBloc 的状态流(MQTT,已停用)
|
||||
StreamSubscription? _tcpStatusSub; // 🔥 订阅 TCP 0x02 机器状态推送(门槛 + 电压/电量/控制模式)
|
||||
static const platform = MethodChannel('com.maibu.satabot/ping');
|
||||
int _currentPing = 50;
|
||||
final ILoggerService _logger = GetIt.I<ILoggerService>();
|
||||
@@ -70,7 +71,11 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
),
|
||||
) {
|
||||
_initPacketListener();
|
||||
_initDeviceStatusListener(); // 🔥 改为订阅 DeviceStatusBloc
|
||||
// 🔥 门槛(hasReceivedStatusPush)与电压/电量/控制模式改由 TCP 0x02 驱动,
|
||||
// 与 MQTT 定位链路解耦;MQTT(DeviceStatusBloc) 仅继续负责定位。
|
||||
// 回滚:注释下行、改回 _initDeviceStatusListener() 即可。
|
||||
_initTcpStatusListener();
|
||||
// _initDeviceStatusListener(); // 🔥 旧:订阅 DeviceStatusBloc(MQTT),已停用
|
||||
}
|
||||
|
||||
// 🔥 新增:订阅 DeviceStatusBloc 的状态流,而不是直接监听TCP
|
||||
@@ -82,6 +87,9 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
int? _cachePing;
|
||||
DateTime? _lastStatusPushTime; // 最后一次收到设备状态推送的时间
|
||||
|
||||
// 🔥 已停用:门槛与电压/电量/控制模式改由 TCP 0x02 驱动(见 _initTcpStatusListener)。
|
||||
// 保留本方法用于回滚——恢复构造函数中的调用即可重新走 MQTT(DeviceStatusUpdated)。
|
||||
// ignore: unused_element
|
||||
void _initDeviceStatusListener() {
|
||||
_deviceStatusSub?.cancel();
|
||||
|
||||
@@ -129,6 +137,77 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
});
|
||||
}
|
||||
|
||||
/// 🔥 门槛 + 运行状态专用监听:直接监听 TCP 0x02 机器状态推送。
|
||||
///
|
||||
/// 与 MQTT 定位链路彻底解耦:
|
||||
/// - 门槛 hasReceivedStatusPush / _lastStatusPushTime 由 TCP 0x02 驱动;
|
||||
/// - 电压/电量/控制模式 直接由 0x02 报文的 fromFields 解析(fields[0]/[21]/[20]);
|
||||
/// - MQTT(DeviceStatusBloc) 仅继续负责定位,不再影响本页门槛与三个值。
|
||||
void _initTcpStatusListener() {
|
||||
_tcpStatusSub?.cancel();
|
||||
|
||||
_tcpStatusSub = tcpClient.packetStream
|
||||
.where((p) => p.command == 0x02)
|
||||
.listen((packet) async {
|
||||
try {
|
||||
final message = utf8.decode(packet.payload, allowMalformed: true);
|
||||
if (message.trim().isEmpty) return;
|
||||
|
||||
final fields = message.trim().split(',');
|
||||
if (fields.length < 18) {
|
||||
debugPrint('⚠️ [TCP-0x02门槛] 字段不足:${fields.length},期望≥18,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
final status = RunningStatusEntity.fromFields(fields);
|
||||
final controlMode = status.controlMode == '3' ? '远程模式' : '本地模式';
|
||||
|
||||
// 🔥 门槛:收到 TCP 0x02 即视为推送活跃(节流前刷新,轻量无阻塞)
|
||||
_lastStatusPushTime = DateTime.now();
|
||||
|
||||
_cacheVoltage = status.voltage.toString();
|
||||
_cacheBattery = status.battery;
|
||||
_cacheCtrlMode = controlMode;
|
||||
|
||||
// 500ms节流,不到时间不刷新UI
|
||||
final now = DateTime.now();
|
||||
if (_lastUiUpdateTime != null &&
|
||||
now.difference(_lastUiUpdateTime!) <
|
||||
const Duration(milliseconds: 500)) {
|
||||
// 节流期间仍需保证门槛标志已置位(否则首帧恰逢节流会导致门槛漏置)
|
||||
if (!state.hasReceivedStatusPush && !isClosed) {
|
||||
emit(state.copyWith(hasReceivedStatusPush: true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
_lastUiUpdateTime = now;
|
||||
|
||||
// 🔥 ping 只在真正刷新UI时测(≤2Hz):0x02 原始推送可能高频,
|
||||
// 若每帧 await getNetworkDelay 会造成大量并发 ping,移到节流后规避。
|
||||
final c = await getNetworkDelay();
|
||||
_cachePing = c;
|
||||
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
runningStatusModel: state.runningStatusModel.copyWith(
|
||||
voltage: _cacheVoltage,
|
||||
battery: _cacheBattery,
|
||||
controlMode: _cacheCtrlMode,
|
||||
),
|
||||
battery: int.tryParse(_cacheBattery ?? '') ?? 0,
|
||||
ping: _cachePing,
|
||||
hasReceivedStatusPush: true, // 🔥 门槛:TCP 0x02 到达即置位
|
||||
updateType: 'device_status',
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [TCP-0x02门槛] 解析异常:$e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 🔥 超简单方法:传入 IP,得到 ping 值
|
||||
|
||||
// 🔥 模拟设备状态更新 - 用于测试
|
||||
@@ -701,6 +780,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
Future<void> close() {
|
||||
_timer?.cancel();
|
||||
_deviceStatusSub?.cancel(); // 🔥 取消订阅 DeviceStatusBloc
|
||||
_tcpStatusSub?.cancel(); // 🔥 取消订阅 TCP 0x02 门槛监听
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -198,16 +198,20 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
.targetDevice;
|
||||
final deviceId = targetDevice?.deviceName;
|
||||
|
||||
// 🔥 从 RemoteControlCubit 获取 token(如果有的话)
|
||||
// 注意:如果 token 不在 RemoteControlCubit 中,需要从其他地方获取
|
||||
// 这里假设 token 是有效的,直接构建 URL
|
||||
if (deviceId != null && deviceId.isNotEmpty) {
|
||||
// 🔥 从 AppUserCubit 获取登录 token:SRS on_play 会校验,缺失会被拒绝拉流
|
||||
final token = context.read<AppUserCubit>().state.user?.token;
|
||||
if (deviceId != null &&
|
||||
deviceId.isNotEmpty &&
|
||||
token != null &&
|
||||
token.isNotEmpty) {
|
||||
_videoStreamUrl =
|
||||
"webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId";
|
||||
"webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=$token";
|
||||
debugPrint('🎬 [WebRTC] URL构建成功: $_videoStreamUrl');
|
||||
} else {
|
||||
_videoStreamUrl = '';
|
||||
debugPrint('❌ [WebRTC] URL构建失败 - deviceId: $deviceId');
|
||||
debugPrint(
|
||||
'❌ [WebRTC] URL构建失败 - deviceId: $deviceId, hasToken: ${token != null}',
|
||||
);
|
||||
}
|
||||
final int originY = context
|
||||
.watch<RemoteControlCubit>()
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:cc_ui_kit/cc_ui_kit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/components/capsule_toast.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
@@ -115,6 +116,13 @@ class CenterControlArea extends StatelessWidget {
|
||||
void _handleSliderAction(String deviceKey, String action, BuildContext context) {
|
||||
debugPrint('🎯 [Slider 业务] deviceKey: $deviceKey, action: $action');
|
||||
|
||||
// 🔥 门槛检查:与摇杆一致,未收到 TCP 0x02 推送时禁止操作并提示。
|
||||
// 胶囊 Toast 全局去重、自动消失,适配 StatelessWidget 无实例状态。
|
||||
if (!context.read<RemoteControlCubit>().isStatusPushActive()) {
|
||||
CapsuleToast.show('暂未收到该机器的推送,不能控制', showCheck: false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 使用deviceKey进行判断,不依赖显示文本
|
||||
switch (deviceKey) {
|
||||
case 'chassis':
|
||||
|
||||
@@ -71,6 +71,11 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
||||
void _updateLoadState(WebRTCLoadState state) {
|
||||
if (_loadState == state) return;
|
||||
_loadState = state;
|
||||
// 🔥 自身状态变化必须触发重建:未传 onLoadingStateChanged 的调用方
|
||||
// (如远程遥控页)否则无法从"视频加载中"切到 playing/error 界面
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
widget.onLoadingStateChanged?.call(state);
|
||||
}
|
||||
|
||||
@@ -404,19 +409,34 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
||||
// 🔥 在真正渲染出视频帧(playing 态)之前,始终显示加载指示器
|
||||
// 避免出现"track 已收到但画面是黑的"黑屏情况
|
||||
if (_loadState != WebRTCLoadState.playing) {
|
||||
// 🔥 区分 loading 与 error:error 态显示明确失败提示,不再一直转圈
|
||||
final bool isError = _loadState == WebRTCLoadState.error;
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(color: Colors.white),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'视频加载中...',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||
),
|
||||
],
|
||||
children: isError
|
||||
? [
|
||||
const Icon(
|
||||
Icons.videocam_off,
|
||||
size: 48,
|
||||
color: Colors.white38,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'视频加载失败,请检查设备是否在线或重试',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||
),
|
||||
]
|
||||
: [
|
||||
const CircularProgressIndicator(color: Colors.white),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'视频加载中...',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user