远程遥控视频的显示bug修复;恢复远程遥控中的前置推送检查和动态实时信息的展示。

This commit is contained in:
2026-09-04 15:02:43 +08:00
parent db81816b78
commit b468e577a6
4 changed files with 128 additions and 16 deletions

View File

@@ -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();
}

View File

@@ -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>()

View File

@@ -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':

View File

@@ -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),
),
],
),
),
);