优化和路径规划新适配
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user