优化和路径规划新适配

This commit is contained in:
2026-07-11 16:46:53 +08:00
parent 2124f3bc02
commit 1a4d0aedfd
43 changed files with 2454 additions and 685 deletions

View File

@@ -79,6 +79,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
String? _cacheBattery;
String? _cacheCtrlMode;
int? _cachePing;
DateTime? _lastStatusPushTime; // 最后一次收到设备状态推送的时间
void _initDeviceStatusListener() {
_deviceStatusSub?.cancel();
@@ -97,6 +98,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
_cacheBattery = battery.toString();
_cacheCtrlMode = controlMode;
_cachePing = c;
_lastStatusPushTime = DateTime.now(); // 记录最后一次推送时间
// 500ms节流,不到时间不刷新UI
final now = DateTime.now();
@@ -117,6 +119,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
),
battery: int.tryParse(_cacheBattery ?? '') ?? 0,
ping: _cachePing,
hasReceivedStatusPush: true, // 标记已收到设备状态推送
// 🔥 标记为设备状态更新
updateType: 'device_status',
),
@@ -677,6 +680,22 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
void toggleRightPip() =>
emit(state.copyWith(showRightPip: !state.showRightPip));
// 🔥 切换前后视角(双击屏幕触发)
void toggleCameraView() {
final currentOriginY = state.controlEntity.originY;
// 🔥 originY >= 0 表示前视角,< 0 表示后视角
// 切换逻辑:如果当前是前视角(>=0),切换到后视角(-1);反之亦然
final newOriginY = currentOriginY >= 0 ? -1 : 1;
debugPrint('📷 [RemoteControlCubit] 切换前后视角: $currentOriginY -> $newOriginY');
final updatedEntity = state.controlEntity.copyWith(y: newOriginY);
emit(state.copyWith(controlEntity: updatedEntity));
// 🔥 发送 TCP 指令到设备
_repository.sendControlMachineCmd(updatedEntity);
}
@override
Future<void> close() {
_timer?.cancel();
@@ -926,6 +945,14 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
emit(state.copyWith(topRightIsExpanded: !state.topRightIsExpanded));
}
/// 检查设备状态推送是否活跃(5秒内有推送视为活跃)
bool isStatusPushActive() {
if (!state.hasReceivedStatusPush) return false;
if (_lastStatusPushTime == null) return false;
// 超过5秒未收到新推送,视为推送已中断
return DateTime.now().difference(_lastStatusPushTime!) < const Duration(seconds: 5);
}
Future<int> getNetworkDelay() async {
try {
// 直接 Ping 你的服务器IP
@@ -956,6 +983,10 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
debugPrint('🎯 [RemoteControl] 设备名称: ${device.deviceName}');
debugPrint('🎯 [RemoteControl] 当前TCP状态: ${tcpClient.isConnected ? "已连接" : "未连接"}');
// 🔥 关键修复:切换设备时清空所有缓存,防止显示上一个设备的数据
_clearAllCache();
debugPrint('✅ [RemoteControl] 已清空所有缓存数据');
debugPrint('📦 [RemoteControl] 更新targetDevice状态');
emit(state.copyWith(targetDevice: device));
debugPrint('✅ [RemoteControl] targetDevice状态已更新');
@@ -997,6 +1028,11 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
void clearTargetDevice() {
// debugPrint('🧹 [RemoteControl] 清除待控制设备');
// _logger.logWithLevel('🧹 [RemoteControl] 清除待控制设备');
// 🔥 关键修复:退出页面时清空所有缓存,防止数据滞留
_clearAllCache();
debugPrint('✅ [RemoteControl] 已清空所有缓存数据');
emit(state.copyWith(targetDevice: null));
}
@@ -1006,5 +1042,32 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
return state.targetDevice;
}
/// 🔥 清空所有缓存数据(解决数据滞留和跨设备数据显示问题)
void _clearAllCache() {
_cacheVoltage = null;
_cacheBattery = null;
_cacheCtrlMode = null;
_cachePing = null;
_lastUiUpdateTime = null;
_lastStatusPushTime = null; // 重置推送时间
// 🔥 同时重置 UI 状态为初始值
if (!isClosed) {
emit(
state.copyWith(
runningStatusModel: state.runningStatusModel.copyWith(
voltage: '--',
battery: '--',
controlMode: '--',
),
battery: 0,
ping: null,
hasReceivedStatusPush: false, // 重置推送接收标志
),
);
}
debugPrint('🗑️ [RemoteControl] 缓存已清空 - voltage, battery, controlMode, ping');
}
}

View File

@@ -30,6 +30,7 @@ class RemoteControlState extends Equatable {
final String obstacleFlag; //障碍物标志位
final RunningStatusModel runningStatusModel;
final DeviceEntity? targetDevice;
final bool hasReceivedStatusPush; // 是否收到过设备状态推送
// 🔥 状态更新类型标记 - 用于区分是设备状态更新还是弹窗状态更新
final String? updateType;
@@ -53,6 +54,7 @@ class RemoteControlState extends Equatable {
this.showRightPip = true,
this.obstacleFlag = '',
required this.runningStatusModel,
this.hasReceivedStatusPush = false,
this.topRightIsExpanded = false,
this.obstacleRecognitionFlag = true,
this.targetDevice,
@@ -79,6 +81,7 @@ class RemoteControlState extends Equatable {
bool? showRightPip,
RunningStatusModel? runningStatusModel,
String? obstacleFlag,
bool? hasReceivedStatusPush,
bool? topRightIsExpanded,
bool? obstacleRecognitionFlag,
DeviceEntity? targetDevice,
@@ -104,6 +107,7 @@ class RemoteControlState extends Equatable {
showRightPip: showRightPip ?? this.showRightPip,
runningStatusModel: runningStatusModel ?? this.runningStatusModel,
obstacleFlag: obstacleFlag ?? this.obstacleFlag,
hasReceivedStatusPush: hasReceivedStatusPush ?? this.hasReceivedStatusPush,
topRightIsExpanded: topRightIsExpanded ?? this.topRightIsExpanded,
obstacleRecognitionFlag:
obstacleRecognitionFlag ?? this.obstacleRecognitionFlag,
@@ -132,6 +136,7 @@ class RemoteControlState extends Equatable {
showRightPip,
runningStatusModel,
obstacleFlag,
hasReceivedStatusPush,
topRightIsExpanded,
obstacleRecognitionFlag,
targetDevice,

View File

@@ -146,17 +146,16 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
@override
Widget build(BuildContext context) {
// 监听全局状态(这些通常不随摇杆频繁变化)
final userState = context.watch<AppUserCubit>().state;
final deviceState = context.watch<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
// 🔥 只监听 RemoteControlCubit,避免其他状态变化导致频繁 rebuild
final remoteCubit = context.read<RemoteControlCubit>();
final targetDevice = remoteCubit.state.targetDevice;
final hasPermission = remoteCubit.state.hasPermission;
///context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
// 🔥 调试日志:检查设备状态
debugPrint('🔍 [RemoteControlPage] build - targetDevice: ${targetDevice?.deviceName}');
if (currentDevice == null) {
if (targetDevice == null) {
debugPrint('❌ [RemoteControlPage] targetDevice 为空,显示离线页面');
return _buildOfflineScaffold();
}
@@ -195,25 +194,23 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
p.showRightPip != c.showRightPip,
builder: (context, state) {
// 🔥 只从 targetDevice 获取 deviceId
// 🔥 从 targetDevice 获取 deviceId
final targetDevice = context
.watch<RemoteControlCubit>()
.state
.targetDevice;
final deviceId = targetDevice?.deviceName;
// debugPrint('🔍 [WebRTC检查] targetDevice: $targetDevice, deviceId: $deviceId');
// debugPrint('🔍 [WebRTC检查] user: ${userState.user != null}, token: ${userState.user?.token != null}');
if (deviceId != null &&
userState.user != null &&
userState.user!.token != null) {
// 🔥 从 RemoteControlCubit 获取 token(如果有的话)
// 注意:如果 token 不在 RemoteControlCubit 中,需要从其他地方获取
// 这里假设 token 是有效的,直接构建 URL
if (deviceId != null && deviceId.isNotEmpty) {
_videoStreamUrl =
"webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
// debugPrint('🎬 [WebRTC] URL构建成功: $_videoStreamUrl');
"webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId";
debugPrint('🎬 [WebRTC] URL构建成功: $_videoStreamUrl');
} else {
_videoStreamUrl = '';
// debugPrint('❌ [WebRTC] URL构建失败 - deviceId: $deviceId, hasUser: ${userState.user != null}, hasToken: ${userState.user?.token != null}');
debugPrint('❌ [WebRTC] URL构建失败 - deviceId: $deviceId');
}
final int originY = context
.watch<RemoteControlCubit>()
@@ -222,18 +219,16 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
.originY;
// debugPrint("${originY},originY");
return WebRTCLocalPlayer(
// 这里的 URL 拼接根据你的后端规则
// streamUrl: "webrtc://${TCPConsts.TCP_IP}/live/livestream/${currentDevice.deviceName}?token=${userState.user!.token}",
streamUrl: _videoStreamUrl,
showLeftPip: state.showLeftPip, // 从 Cubit 状态中读取
showRightPip: state.showRightPip, // 从 Cubit 状态中读取
isFrontMain:
context
.watch<RemoteControlCubit>()
.state
.controlEntity
.originY >=
0,
showLeftPip: state.showLeftPip,
showRightPip: state.showRightPip,
isFrontMain: originY >= 0,
onDoubleTap: () {
debugPrint('👆 [RemoteControlPage] 双击屏幕,切换前后视角');
// 🔥 通过 TCP 发送切换视角指令
// 这里需要调用 RemoteControlCubit 的方法来切换视角
context.read<RemoteControlCubit>().toggleCameraView();
},
);
},
),

View File

@@ -24,6 +24,49 @@ class LeftJoystickArea extends StatefulWidget {
class _LeftJoystickAreaState extends State<LeftJoystickArea> {
int _lastSentY = 0;
bool _isTouching = false;
bool _hasShownNoPushDialog = false; // 防止重复弹窗
/// 检查是否可以控制,不能则弹出提示
bool _checkCanControl(BuildContext context) {
final cubit = context.read<RemoteControlCubit>();
if (!cubit.isStatusPushActive()) {
if (!_hasShownNoPushDialog) {
_hasShownNoPushDialog = true;
_showNoStatusPushDialog(context);
}
return false;
}
_hasShownNoPushDialog = false;
return true;
}
/// 显示暂未收到推送的提示弹窗(小圆角)
void _showNoStatusPushDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: const Color(0xE5333333),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'暂未收到该机器的推送,不能控制',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
),
),
),
);
// 1.5秒后自动关闭
Future.delayed(const Duration(milliseconds: 1500), () {
Navigator.of(context).pop();
});
}
@override
Widget build(BuildContext context) {
@@ -39,6 +82,7 @@ class _LeftJoystickAreaState extends State<LeftJoystickArea> {
radius: widget.width,
axisHint: AxisHint.forwardBackward,
onValueChanged: (value) {
if (!_checkCanControl(context)) return;
// 标记:正在触摸
_isTouching = true;
@@ -50,11 +94,13 @@ class _LeftJoystickAreaState extends State<LeftJoystickArea> {
}
},
onPress: () {
if (!_checkCanControl(context)) return;
_isTouching = true;
debugPrint('onPress');
_triggerVibration();
},
onPanEnd: () async {
_hasShownNoPushDialog = false; // 松手重置,下次触摸重新检查
// 🔥 松手 100% 归零
debugPrint('onPanEnd');
await _stopJoystick();

View File

@@ -23,6 +23,49 @@ class RightJoystickArea extends StatefulWidget {
class _RightJoystickAreaState extends State<RightJoystickArea> {
int _lastX = 0;
bool _hasShownNoPushDialog = false; // 防止重复弹窗
/// 检查是否可以控制,不能则弹出提示
bool _checkCanControl(BuildContext context) {
final cubit = context.read<RemoteControlCubit>();
if (!cubit.isStatusPushActive()) {
if (!_hasShownNoPushDialog) {
_hasShownNoPushDialog = true;
_showNoStatusPushDialog(context);
}
return false;
}
_hasShownNoPushDialog = false;
return true;
}
/// 显示暂未收到推送的提示弹窗(小圆角)
void _showNoStatusPushDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: const Color(0xE5333333),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'暂未收到该机器的推送,不能控制',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
),
),
);
// 1.5秒后自动关闭
Future.delayed(const Duration(milliseconds: 1500), () {
Navigator.of(context).pop();
});
}
@override
Widget build(BuildContext context) {
@@ -38,6 +81,7 @@ class _RightJoystickAreaState extends State<RightJoystickArea> {
radius: widget.width,
axisHint: AxisHint.leftRight,
onValueChanged: (value) {
if (!_checkCanControl(context)) return;
int currentX = value.x.toInt();
if (currentX == _lastX) return;
_lastX = currentX;
@@ -46,8 +90,12 @@ class _RightJoystickAreaState extends State<RightJoystickArea> {
debugPrint('右摇杆的数据- x: $finalX, y: 0');
context.read<RemoteControlCubit>().updateOriginX(finalX);
},
onPress: _triggerVibration,
onPress: () {
if (!_checkCanControl(context)) return;
_triggerVibration();
},
onPanEnd: () async {
_hasShownNoPushDialog = false; // 松手重置,下次触摸重新检查
debugPrint('🕹️ [右摇杆] 松手,强制 X=0, Y=0');
_lastX = 0;

View File

@@ -8,16 +8,18 @@ class WebRTCLocalPlayer extends StatefulWidget {
final String streamUrl;
final bool showLeftPip;
final bool showRightPip;
// 🔥 新增:外部传入是否显示前视角
final bool isFrontMain;
final Alignment? mainViewAlignment; // 🔥 主视角对齐方式(指定后覆盖 isFrontMain)
final VoidCallback? onDoubleTap; // 🔥 添加双击回调
const WebRTCLocalPlayer({
super.key,
required this.streamUrl,
this.showLeftPip = true,
this.showRightPip = true,
required this.isFrontMain, // 🔥 必传
this.isFrontMain = true,
this.mainViewAlignment, // 🔥 可选,指定要显示的象限
this.onDoubleTap, // 🔥 可选回调
});
@override
@@ -25,12 +27,16 @@ class WebRTCLocalPlayer extends StatefulWidget {
}
class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
// 🔥 显示器:只初始化一次,终生使用
final RTCVideoRenderer _renderer = RTCVideoRenderer();
// 🔥 信号线:可以更换,但必须先拔后插
RTCPeerConnection? _peerConnection;
// 🔥 URL 缓存:用于判断是否需要换线
String? _currentStreamUrl;
bool _isInitialized = false;
// 使用 ValueNotifier 配合局部刷新,提升拖拽性能
// 画中画位置管理
final ValueNotifier<Offset> _leftPosNotifier = ValueNotifier(const Offset(20, 80));
final ValueNotifier<Offset> _rightPosNotifier = ValueNotifier(const Offset(200, 80));
bool _isPosInitialized = false;
@@ -38,47 +44,76 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
@override
void initState() {
super.initState();
_prepareAndConnect();
_initRenderer(); // 🔥 显示器只初始化一次
}
Future<void> _prepareAndConnect() async {
/// 🔥 初始化显示器(只调用一次)
Future<void> _initRenderer() async {
try {
await _renderer.initialize();
if (!mounted) return;
await _initWebRTCConnection();
setState(() => _isInitialized = true);
if (mounted) {
_connectSignal(); // 🔥 显示器就绪后,插上第一根信号线
}
} catch (e) {
debugPrint("初始化失败: $e");
debugPrint("❌ [WebRTC] 显示器初始化失败: $e");
}
}
Future<void> _initWebRTCConnection() async {
// 🔥 安全检查: URL 为空时不初始化
@override
void didUpdateWidget(WebRTCLocalPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
// 🔥 只有 URL 真正变化时才换线
if (oldWidget.streamUrl != widget.streamUrl && widget.streamUrl.isNotEmpty) {
debugPrint('🔄 [WebRTC] URL 变化,准备换线');
_connectSignal();
}
}
/// 🔥 换线逻辑:先拔后插
Future<void> _connectSignal() async {
// 1️⃣ 如果 URL 为空,不插线
if (widget.streamUrl.isEmpty) {
debugPrint('⚠️ [WebRTC] streamUrl 为空,跳过初始化');
setState(() => _isInitialized = true); // 🔥 标记为已初始化,避免卡loading
debugPrint('⚠️ [WebRTC] URL 为空,跳过连接');
return;
}
_peerConnection = await createPeerConnection({
"sdpSemantics": "unified-plan",
"iceServers": [
{"urls": "stun:stun.l.google.com:19302"},
],
});
await _peerConnection!.addTransceiver(
kind: RTCRtpMediaType.RTCRtpMediaTypeVideo,
init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly),
);
_peerConnection!.onTrack = (RTCTrackEvent event) {
if (event.track.kind == 'video' && event.streams.isNotEmpty) {
if (mounted) setState(() => _renderer.srcObject = event.streams[0]);
}
};
// 2️⃣ 如果 URL 没变,不换线(防止重复连接)
if (_currentStreamUrl == widget.streamUrl && _peerConnection != null) {
debugPrint('✅ [WebRTC] URL 未变化,保持当前连接');
return;
}
// 3️⃣ 拔掉旧线(彻底释放旧连接)
await _disconnectSignal();
// 4️⃣ 插上新线(创建新连接)
try {
debugPrint('🔌 [WebRTC] 建立新连接...');
_peerConnection = await createPeerConnection({
"sdpSemantics": "unified-plan",
"iceServers": [
{"urls": "stun:stun.l.google.com:19302"},
],
});
await _peerConnection!.addTransceiver(
kind: RTCRtpMediaType.RTCRtpMediaTypeVideo,
init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly),
);
// 🔥 收到视频流时,直接连接到显示器
_peerConnection!.onTrack = (RTCTrackEvent event) {
if (event.track.kind == 'video' && event.streams.isNotEmpty) {
debugPrint('✅ [WebRTC] 收到视频流,连接到显示器');
if (mounted) {
setState(() => _renderer.srcObject = event.streams[0]);
}
}
};
// 🔥 SDP 协商
RTCSessionDescription offer = await _peerConnection!.createOffer({'offerToReceiveVideo': 1});
await _peerConnection!.setLocalDescription(offer);
@@ -88,39 +123,53 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
final response = await http.post(
Uri.parse(apiUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({"api": apiUrl, "streamurl": widget.streamUrl, "clientip": null, "sdp": offer.sdp}),
body: jsonEncode({
"api": apiUrl,
"streamurl": widget.streamUrl,
"clientip": null,
"sdp": offer.sdp
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final sdp = data['sdp'];
// 🔥 安全检查: SDP 不能为 null
if (sdp != null && sdp is String && sdp.isNotEmpty) {
await _peerConnection!.setRemoteDescription(RTCSessionDescription(sdp, 'answer'));
debugPrint('✅ [WebRTC] setRemoteDescription 成功');
} else {
debugPrint('❌ [WebRTC] SDP 为空或无效,跳过 setRemoteDescription');
_currentStreamUrl = widget.streamUrl; // 🔥 记录当前 URL
debugPrint('✅ [WebRTC] 连接成功');
}
}
} catch (e) {
debugPrint("信令错误: $e");
debugPrint("❌ [WebRTC] 连接失败: $e");
}
}
/// 🔥 拔线逻辑:彻底释放旧连接
Future<void> _disconnectSignal() async {
if (_peerConnection != null) {
debugPrint('🗑️ [WebRTC] 释放旧连接...');
await _peerConnection?.close();
await _peerConnection?.dispose();
_peerConnection = null;
_renderer.srcObject = null; // 🔥 清空显示器的信号源
debugPrint('✅ [WebRTC] 旧连接已释放');
}
}
@override
void dispose() {
_peerConnection?.dispose();
_disconnectSignal(); // 🔥 拔掉信号线
_renderer.srcObject = null;
_renderer.dispose();
_renderer.dispose(); // 🔥 销毁显示器
_leftPosNotifier.dispose();
_rightPosNotifier.dispose();
super.dispose();
}
// 四分屏裁剪视图
Widget _buildQuadrantView({required Alignment alignment}) {
if (!_isInitialized || _renderer.srcObject == null) return Container(color: Colors.black);
if (_renderer.srcObject == null) return Container(color: Colors.black);
return RepaintBoundary(
child: ClipRect(
@@ -156,30 +205,17 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: _buildQuadrantView(alignment: widget.isFrontMain ? Alignment.topLeft : Alignment.topRight),
child: _buildQuadrantView(alignment: widget.mainViewAlignment ?? (widget.isFrontMain ? Alignment.topLeft : Alignment.topRight)),
),
);
}
@override
Widget build(BuildContext context) {
// 🔥 如果 URL 为空且未初始化,返回黑色占位符
if (!_isInitialized && widget.streamUrl.isEmpty) {
// 🔥 如果正在初始化,显示加载指示器
if (_peerConnection == null && _currentStreamUrl == null) {
return Container(
color: Colors.black, // 🔥 使用黑色背景
child: const Center(
child: Text(
'无视频信号',
style: TextStyle(color: Colors.white54, fontSize: 14),
),
),
);
}
// 🔥 如果正在初始化但有 URL,显示加载指示器(使用黑色背景)
if (!_isInitialized) {
return Container(
color: Colors.black, // 🔥 使用黑色背景
color: Colors.black,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -197,13 +233,12 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
}
return Scaffold(
backgroundColor: Colors.black, // 🔥 使用黑色背景
backgroundColor: Colors.black,
body: LayoutBuilder(
builder: (context, constraints) {
final double pipW = constraints.maxWidth / 5;
final double pipH = pipW * 9 / 16;
// 核心:处理 Windows 窗口大小变化时的坐标修正
Future.microtask(() {
if (!mounted) return;
_correctPosition(_leftPosNotifier, constraints, pipW, pipH);
@@ -218,12 +253,15 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
return Stack(
children: [
// 🔥 已移除背景虚化层,避免遮挡上方内容
// 主视频层的羽化效果由 _buildMainViewWithFeathering() 处理
// ② 主视频层(🔥 去掉双击,交给外部控制)
// ② 主视频层(🔥 恢复双击切换前后视角)
Center(
child: AspectRatio(aspectRatio: 16 / 9, child: _buildMainViewWithFeathering()),
child: GestureDetector(
onDoubleTap: () {
debugPrint('👆 [WebRTC] 双击屏幕,切换前后视角');
widget.onDoubleTap?.call(); // 🔥 调用父组件回调
},
child: AspectRatio(aspectRatio: 16 / 9, child: _buildMainViewWithFeathering()),
),
),
// ③ 悬浮小窗 - 解决阻尼感的 1:1 稳定拖拽
@@ -246,7 +284,6 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
}
}
// 极致丝滑的局部刷新组件
Widget _buildFastPip(ValueNotifier<Offset> notifier, Alignment align, BoxConstraints constraints, double w, double h) {
return ValueListenableBuilder<Offset>(
valueListenable: notifier,
@@ -257,7 +294,6 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: (details) {
// 关键修正:使用 notifier.value.dx 替换 pos.dx,消除位移丢失导致的阻尼感
final double nextX = (notifier.value.dx + details.delta.dx).clamp(0.0, (constraints.maxWidth - w).clamp(0.0, double.infinity));
final double nextY = (notifier.value.dy + details.delta.dy).clamp(0.0, (constraints.maxHeight - h).clamp(0.0, double.infinity));
notifier.value = Offset(nextX, nextY);