fix: 修复 WebRTC 控制稳定性与输入事件重复问题
- 修复安卓端 401 拦截器线程问题,确保 Toast 在主线程执行 - 修复 Web 端按键/触摸重复触发:增加按下状态跟踪,防止悬停触发动作和长按重复 - 修复 Web 端指针取消时未释放触摸状态导致被控端卡死 - 修复 TURN 未启用时返回 401 导致客户端异常登出,改为返回 enabled=false - 修复后台管理端 401 后路由守卫跳转失败 - 增加控制指令调试日志用于排查问题
This commit is contained in:
@@ -29,11 +29,19 @@ const selectedResolution = ref(0);
|
||||
const fpsOptions = ref([15, 24, 30, 60]);
|
||||
const selectedFps = ref(0); // 0 表示尚未同步到被控端当前帧率
|
||||
|
||||
// 记录当前处于按下状态的按键:pointerleave/pointerup 在指针未按下时也会触发,
|
||||
// 若无此跟踪,鼠标仅扫过按钮就会向被控端发送孤立的 KEY UP。
|
||||
const pressedButtons = new Set();
|
||||
|
||||
function pressDown(code) {
|
||||
if (!store.dataChannelOpen) return;
|
||||
if (pressedButtons.has(code)) return;
|
||||
pressedButtons.add(code);
|
||||
sendKeyDown(code);
|
||||
}
|
||||
function pressUp(code) {
|
||||
if (!pressedButtons.has(code)) return;
|
||||
pressedButtons.delete(code);
|
||||
if (!store.dataChannelOpen) return;
|
||||
sendKeyUp(code);
|
||||
}
|
||||
@@ -71,18 +79,26 @@ function lookupKeyCode(e) {
|
||||
}
|
||||
|
||||
// 固定按键:按下/抬起分离(如电源键需要真实按下-抬起配对)
|
||||
// pressedKeys 跟踪已按下的物理键,避免长按自动重复触发重复 DOWN、
|
||||
// 以及无对应 DOWN 的孤立 UP(如在窗口外按下、回到窗口内才抬起)。
|
||||
const pressedKeys = new Set();
|
||||
|
||||
function onKeyCaptureDown(e) {
|
||||
if (!store.dataChannelOpen) return;
|
||||
const m = lookupKeyCode(e);
|
||||
if (!m) return;
|
||||
e.preventDefault();
|
||||
if (pressedKeys.has(m.code)) return; // 浏览器长按自动重复,忽略
|
||||
pressedKeys.add(m.code);
|
||||
sendKeyDown(m.code);
|
||||
}
|
||||
function onKeyCaptureUp(e) {
|
||||
if (!store.dataChannelOpen) return;
|
||||
const m = lookupKeyCode(e);
|
||||
if (!m) return;
|
||||
e.preventDefault();
|
||||
if (!pressedKeys.has(m.code)) return;
|
||||
pressedKeys.delete(m.code);
|
||||
if (!store.dataChannelOpen) return;
|
||||
sendKeyUp(m.code);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ const TOUCH_SLOP = 0.02;
|
||||
const SAMPLE_MS = 16;
|
||||
|
||||
let startX = 0, startY = 0, startTime = 0, isLongPressed = false, longTimer = null, lastMoveTs = 0;
|
||||
// 指针是否处于按下状态:未按下时 pointermove/pointerup 不得产生任何指令,
|
||||
// 否则鼠标仅悬停划过画面就会向被控端发送大量 ACTION_MOVE。
|
||||
let isDown = false;
|
||||
|
||||
function clearLong() {
|
||||
if (longTimer) { clearTimeout(longTimer); longTimer = null; }
|
||||
@@ -53,6 +56,7 @@ function clearLong() {
|
||||
function onDown(e) {
|
||||
if (!active.value) return;
|
||||
overlayRef.value.setPointerCapture && overlayRef.value.setPointerCapture(e.pointerId);
|
||||
isDown = true;
|
||||
isLongPressed = false;
|
||||
const p = mapRelative(e.clientX, e.clientY);
|
||||
startX = p.x; startY = p.y; startTime = Date.now(); lastMoveTs = 0;
|
||||
@@ -65,7 +69,7 @@ function onDown(e) {
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
if (!active.value) return;
|
||||
if (!active.value || !isDown) return;
|
||||
const p = mapRelative(e.clientX, e.clientY);
|
||||
if (Math.abs(p.x - startX) > TOUCH_SLOP || Math.abs(p.y - startY) > TOUCH_SLOP) clearLong();
|
||||
const now = Date.now();
|
||||
@@ -76,7 +80,8 @@ function onMove(e) {
|
||||
}
|
||||
|
||||
function onUp(e) {
|
||||
if (!active.value) return;
|
||||
if (!active.value || !isDown) return;
|
||||
isDown = false;
|
||||
clearLong();
|
||||
const p = mapRelative(e.clientX, e.clientY);
|
||||
sendMotionEvent(1, p.x, p.y); // ACTION_UP
|
||||
@@ -88,6 +93,15 @@ function onUp(e) {
|
||||
else sendSwipe(startX, startY, p.x, p.y, Math.max(dur, 1));
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
clearLong();
|
||||
// 按下过程中指针被取消(如系统手势接管):补发 ACTION_UP 释放,避免被控端卡在按下态。
|
||||
if (isDown && active.value) {
|
||||
isDown = false;
|
||||
sendMotionEvent(1, startX, startY); // ACTION_UP
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -101,7 +115,7 @@ function onUp(e) {
|
||||
@pointerdown="onDown"
|
||||
@pointermove="onMove"
|
||||
@pointerup="onUp"
|
||||
@pointercancel="clearLong"
|
||||
@pointercancel="onCancel"
|
||||
></div>
|
||||
|
||||
<div v-if="!hasStream" class="placeholder">
|
||||
|
||||
@@ -222,6 +222,7 @@ export class WebRtcController {
|
||||
if (!this.dataChannel || this.dataChannel.readyState !== 'open') return false;
|
||||
const bytes = encodeControlMessage(fields);
|
||||
this.dataChannel.send(bytes);
|
||||
console.debug('[控制指令]', describeControlMessage(fields), `bytes=${bytes.length}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -327,6 +328,26 @@ export class WebRtcController {
|
||||
}
|
||||
}
|
||||
|
||||
// 将待发送的控制指令格式化为可读日志文本(坐标保留 4 位小数)。
|
||||
function describeControlMessage(f) {
|
||||
const pt = (x, y) => `(${Number(x).toFixed(4)}, ${Number(y).toFixed(4)})`;
|
||||
switch (f.action) {
|
||||
case 1: return `模拟点击 TOUCH ${pt(f.x, f.y)}`;
|
||||
case 2: return `模拟滑动 SWIPE ${pt(f.x1, f.y1)} -> ${pt(f.x2, f.y2)} duration=${f.duration}ms`;
|
||||
case 3: {
|
||||
const ka = f.keyAction === 0 ? 'DOWN' : f.keyAction === 1 ? 'UP' : 'CLICK';
|
||||
return `模拟按键 KEY keyCode=${f.keyCode} keyAction=${ka}`;
|
||||
}
|
||||
case 4: return `模拟长按 LONG_PRESS ${pt(f.x, f.y)}`;
|
||||
case 5: {
|
||||
const ma = f.motionAction === 0 ? 'DOWN' : f.motionAction === 1 ? 'UP' : f.motionAction === 2 ? 'MOVE' : String(f.motionAction);
|
||||
return `触摸轨迹 MOTION ${ma} ${pt(f.x, f.y)}`;
|
||||
}
|
||||
case 6: return `切换分辨率 SET_RESOLUTION ${f.width}x${f.height}@${f.fps}fps`;
|
||||
default: return `未知指令 action=${f.action}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 将字节/秒格式化为可读速率
|
||||
function formatSpeed(bps) {
|
||||
if (!bps || bps <= 0) return '0 B/s';
|
||||
|
||||
Reference in New Issue
Block a user