fix: 修复 WebRTC 控制稳定性与输入事件重复问题

- 修复安卓端 401 拦截器线程问题,确保 Toast 在主线程执行
- 修复 Web 端按键/触摸重复触发:增加按下状态跟踪,防止悬停触发动作和长按重复
- 修复 Web 端指针取消时未释放触摸状态导致被控端卡死
- 修复 TURN 未启用时返回 401 导致客户端异常登出,改为返回 enabled=false
- 修复后台管理端 401 后路由守卫跳转失败
- 增加控制指令调试日志用于排查问题
This commit is contained in:
TongTongStudio
2026-08-04 02:40:23 +08:00
parent 1bd0318d35
commit 9b0e105647
7 changed files with 86 additions and 13 deletions

View File

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

View File

@@ -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">

View File

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