路径规划孔洞和 远程遥控视频切换

This commit is contained in:
mmc
2026-03-20 11:20:09 +08:00
parent bb22de2e32
commit 95c7d02415
3 changed files with 41 additions and 119 deletions

View File

@@ -1729,8 +1729,9 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
isStartWork = false;
});
_saveDataToLocal();
if (isStopWork || !isStartWork) return;
_stopWork();
//if (isStopWork || !isStartWork) return;
},
icon: const Icon(Icons.close, color: Colors.grey, size: 20),
padding: EdgeInsets.zero,

View File

@@ -82,6 +82,7 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
streamUrl: _videoStreamUrl,
showLeftPip: state.showLeftPip, // 从 Cubit 状态中读取
showRightPip: state.showRightPip, // 从 Cubit 状态中读取
isFrontMain: context.watch<RemoteControlCubit>().state.controlEntity.originY>= 0.5,
);
},
),
@@ -192,21 +193,21 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
color: Colors.black54,
child: AlertDialog(
title: const Text("权限变更"),
// content: Text("${state.permissionPlatform}端正请求控制权,同意释放吗?"),
// content: Text("${state.permissionPlatform}端正请求控制权,同意释放吗?"),
content: Text("web端正请求控制权,同意释放吗?"),
actions: [
TextButton(onPressed: () => context.read<RemoteControlCubit>().respondPermission(false,deviceId), child: const Text("拒绝")),
TextButton(onPressed: () => context.read<RemoteControlCubit>().respondPermission(false, deviceId), child: const Text("拒绝")),
TextButton(
onPressed: () {
context.read<RemoteControlCubit>().respondPermission(true, deviceId);
// 🔥 用户同意,延迟跳转到首页
// Future.delayed(const Duration(milliseconds: 1000), () {
// if (context.mounted) {
// context.go(RoutePaths.home);
// }
// });
},
child: const Text("同意"),
onPressed: () {
context.read<RemoteControlCubit>().respondPermission(true, deviceId);
// 🔥 用户同意,延迟跳转到首页
// Future.delayed(const Duration(milliseconds: 1000), () {
// if (context.mounted) {
// context.go(RoutePaths.home);
// }
// });
},
child: const Text("同意"),
),
],
),

View File

@@ -9,11 +9,15 @@ class WebRTCLocalPlayer extends StatefulWidget {
final bool showLeftPip;
final bool showRightPip;
// 🔥 新增:外部传入是否显示前视角
final bool isFrontMain;
const WebRTCLocalPlayer({
super.key,
required this.streamUrl,
this.showLeftPip = true,
this.showRightPip = true,
required this.isFrontMain, // 🔥 必传
});
@override
@@ -25,15 +29,10 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
RTCPeerConnection? _peerConnection;
bool _isInitialized = false;
bool _isFrontMain = true;
// 使用 ValueNotifier 配合局部刷新,提升拖拽性能
final ValueNotifier<Offset> _leftPosNotifier = ValueNotifier(
const Offset(20, 80),
);
final ValueNotifier<Offset> _rightPosNotifier = ValueNotifier(
const Offset(200, 80),
);
final ValueNotifier<Offset> _leftPosNotifier = ValueNotifier(const Offset(20, 80));
final ValueNotifier<Offset> _rightPosNotifier = ValueNotifier(const Offset(200, 80));
bool _isPosInitialized = false;
@override
@@ -73,32 +72,21 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
};
try {
RTCSessionDescription offer = await _peerConnection!.createOffer({
'offerToReceiveVideo': 1,
});
RTCSessionDescription offer = await _peerConnection!.createOffer({'offerToReceiveVideo': 1});
await _peerConnection!.setLocalDescription(offer);
Uri uri = Uri.parse(
widget.streamUrl.replaceFirst('webrtc://', 'http://'),
);
Uri uri = Uri.parse(widget.streamUrl.replaceFirst('webrtc://', 'http://'));
String apiUrl = "http://${uri.host}:1985/rtc/v1/play/";
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);
await _peerConnection!.setRemoteDescription(
RTCSessionDescription(data['sdp'], 'answer'),
);
await _peerConnection!.setRemoteDescription(RTCSessionDescription(data['sdp'], 'answer'));
}
} catch (e) {
debugPrint("信令错误: $e");
@@ -117,8 +105,7 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
// 四分屏裁剪视图
Widget _buildQuadrantView({required Alignment alignment}) {
if (!_isInitialized || _renderer.srcObject == null)
return Container(color: Colors.black);
if (!_isInitialized || _renderer.srcObject == null) return Container(color: Colors.black);
return RepaintBoundary(
child: ClipRect(
@@ -126,11 +113,7 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
widthFactor: 2.0,
heightFactor: 2.0,
alignment: alignment,
child: RTCVideoView(
_renderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
mirror: false,
),
child: RTCVideoView(_renderer, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: false),
),
),
);
@@ -143,12 +126,7 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.white,
Colors.white,
Colors.transparent,
],
colors: [Colors.transparent, Colors.white, Colors.white, Colors.transparent],
stops: [0.0, 0.15, 0.85, 1.0],
).createShader(bounds);
},
@@ -158,19 +136,12 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
return const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.transparent,
Colors.white,
Colors.white,
Colors.transparent,
],
colors: [Colors.transparent, Colors.white, Colors.white, Colors.transparent],
stops: [0.0, 0.05, 0.95, 1.0],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: _buildQuadrantView(
alignment: _isFrontMain ? Alignment.topLeft : Alignment.topRight,
),
child: _buildQuadrantView(alignment: widget.isFrontMain ? Alignment.topLeft : Alignment.topRight),
),
);
}
@@ -199,10 +170,7 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
if (!_isPosInitialized) {
_leftPosNotifier.value = const Offset(20, 80);
_rightPosNotifier.value = Offset(
constraints.maxWidth - pipW - 20,
80,
);
_rightPosNotifier.value = Offset(constraints.maxWidth - pipW - 20, 80);
_isPosInitialized = true;
}
@@ -212,11 +180,7 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
Positioned.fill(
child: Stack(
children: [
_buildQuadrantView(
alignment: _isFrontMain
? Alignment.topLeft
: Alignment.topRight,
),
_buildQuadrantView(alignment: widget.isFrontMain ? Alignment.topLeft : Alignment.topRight),
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
@@ -227,35 +191,14 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
),
),
// ② 主视频层
// ② 主视频层(🔥 去掉双击,交给外部控制)
Center(
child: GestureDetector(
onDoubleTap: () =>
setState(() => _isFrontMain = !_isFrontMain),
child: AspectRatio(
aspectRatio: 16 / 9,
child: _buildMainViewWithFeathering(),
),
),
child: AspectRatio(aspectRatio: 16 / 9, child: _buildMainViewWithFeathering()),
),
// ③ 悬浮小窗 - 解决阻尼感的 1:1 稳定拖拽
if (widget.showLeftPip)
_buildFastPip(
_leftPosNotifier,
Alignment.bottomLeft,
constraints,
pipW,
pipH,
),
if (widget.showRightPip)
_buildFastPip(
_rightPosNotifier,
Alignment.bottomRight,
constraints,
pipW,
pipH,
),
if (widget.showLeftPip) _buildFastPip(_leftPosNotifier, Alignment.bottomLeft, constraints, pipW, pipH),
if (widget.showRightPip) _buildFastPip(_rightPosNotifier, Alignment.bottomRight, constraints, pipW, pipH),
],
);
},
@@ -263,12 +206,7 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
);
}
void _correctPosition(
ValueNotifier<Offset> notifier,
BoxConstraints constraints,
double w,
double h,
) {
void _correctPosition(ValueNotifier<Offset> notifier, BoxConstraints constraints, double w, double h) {
double maxX = (constraints.maxWidth - w).clamp(0.0, double.infinity);
double maxY = (constraints.maxHeight - h).clamp(0.0, double.infinity);
double newX = notifier.value.dx.clamp(0.0, maxX);
@@ -279,13 +217,7 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
}
// 极致丝滑的局部刷新组件
Widget _buildFastPip(
ValueNotifier<Offset> notifier,
Alignment align,
BoxConstraints constraints,
double w,
double h,
) {
Widget _buildFastPip(ValueNotifier<Offset> notifier, Alignment align, BoxConstraints constraints, double w, double h) {
return ValueListenableBuilder<Offset>(
valueListenable: notifier,
builder: (context, pos, child) {
@@ -296,14 +228,8 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
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),
);
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);
},
child: child!,
@@ -315,13 +241,7 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
height: h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(0, 8))],
),
clipBehavior: Clip.antiAlias,
child: _buildQuadrantView(alignment: align),