project: 增加web和adb控制,未测试

This commit is contained in:
2026-07-17 19:17:10 +08:00
parent 5fb7b58fc3
commit b8edd5eff5
49 changed files with 5061 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
<script setup>
import { ref } from 'vue';
import { store, connectSignaling, disconnectSignaling, refreshDevices, connectToDevice, disconnectDevice } from '../store/controllerStore';
const selected = ref('');
function onConnectDevice() {
connectToDevice(selected.value);
}
</script>
<template>
<div class="conn-panel">
<div class="field">
<label>信令服务器地址 (WebSocket)</label>
<input class="input" v-model="store.serverUrl" placeholder="ws://host:port/ws/signal" />
</div>
<div class="field">
<label>本机设备 ID (CONTROLLER)</label>
<input class="input" v-model="store.deviceId" placeholder="例如 web-controller-001" />
</div>
<button class="btn" v-if="!store.signalingConnected" @click="connectSignaling">连接信令服务器</button>
<button class="btn danger" v-else @click="disconnectSignaling">断开信令</button>
<div class="section-title">被控端设备 (CONTROLLED)</div>
<button class="btn secondary" style="margin-bottom:12px" @click="refreshDevices" :disabled="!store.signalingConnected">
刷新设备列表
</button>
<div v-if="store.controlledDevices.length === 0" class="hint">
暂无在线被控端请确认 Android 被控端 (WebRTCControlled) 已启动并连接到同一信令服务器
</div>
<div
v-for="id in store.controlledDevices"
:key="id"
class="device-item"
:class="{ active: store.targetDeviceId === id }"
@click="selected = id"
>
<span>{{ id }}</span>
<span style="color:var(--accent-2)">在线</span>
</div>
<div class="field" style="margin-top:14px">
<select class="select" v-model="selected">
<option value="">选择目标设备</option>
<option v-for="id in store.controlledDevices" :key="id" :value="id">{{ id }}</option>
</select>
</div>
<button class="btn" @click="onConnectDevice" :disabled="!selected || store.rtcConnected">发起远程控制</button>
<button class="btn danger" v-if="store.rtcConnected" @click="disconnectDevice">结束控制</button>
<div v-if="store.error" class="error">{{ store.error }}</div>
<div class="section-title">说明</div>
<div class="hint">
本页面作为 <b>控制端 (CONTROLLER)</b>对应原 Android / Flutter 控制端<br />
连接后可在右侧屏幕区域触摸/滑动来操控被控设备<br />
控制指令以 protobuf DataChannel 发送与原 Android 被控端完全兼容
</div>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup>
import { store, sendKey } from '../store/controllerStore';
// Android KeyEvent 键值(与被控端 SystemInputUtils 注入一致)
const keys = [
{ label: '主页', code: 3 }, // KEYCODE_HOME
{ label: '返回', code: 4 }, // KEYCODE_BACK
{ label: '多任务', code: 187 }, // KEYCODE_APP_SWITCH
{ label: '菜单', code: 82 }, // KEYCODE_MENU
{ label: '音量 +', code: 24 }, // KEYCODE_VOLUME_UP
{ label: '音量 -', code: 25 }, // KEYCODE_VOLUME_DOWN
{ label: '电源', code: 26 }, // KEYCODE_POWER
];
function press(code) {
if (!store.dataChannelOpen) return;
sendKey(code);
}
</script>
<template>
<div class="control-bar">
<button
v-for="k in keys"
:key="k.code"
class="key-btn"
:disabled="!store.dataChannelOpen"
@click="press(k.code)"
>
{{ k.label }}
</button>
</div>
</template>

View File

@@ -0,0 +1,112 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { store, sendTouch, sendSwipe, sendLongPress, sendMotionEvent } from '../store/controllerStore';
const videoRef = ref(null);
const overlayRef = ref(null);
const hasStream = ref(false);
const active = computed(() => store.dataChannelOpen);
watch(
() => store.remoteStream,
(stream) => {
if (videoRef.value && stream) {
videoRef.value.srcObject = stream;
hasStream.value = true;
} else {
hasStream.value = false;
}
}
);
// 将指针坐标映射到视频内容区相对坐标 (0~1),兼容 object-fit: contain 的黑边。
function mapRelative(clientX, clientY) {
const v = videoRef.value;
const rect = v.getBoundingClientRect();
const vw = v.videoWidth || rect.width;
const vh = v.videoHeight || rect.height;
const scale = Math.min(rect.width / vw, rect.height / vh) || 1;
const dispW = vw * scale;
const dispH = vh * scale;
const offX = (rect.width - dispW) / 2;
const offY = (rect.height - dispH) / 2;
let x = (clientX - rect.left - offX) / dispW;
let y = (clientY - rect.top - offY) / dispH;
x = Math.max(0, Math.min(1, x));
y = Math.max(0, Math.min(1, y));
return { x, y };
}
const LONG_PRESS_MS = 400;
const TOUCH_SLOP = 0.02;
const SAMPLE_MS = 16;
let startX = 0, startY = 0, startTime = 0, isLongPressed = false, longTimer = null, lastMoveTs = 0;
function clearLong() {
if (longTimer) { clearTimeout(longTimer); longTimer = null; }
}
function onDown(e) {
if (!active.value) return;
overlayRef.value.setPointerCapture && overlayRef.value.setPointerCapture(e.pointerId);
isLongPressed = false;
const p = mapRelative(e.clientX, e.clientY);
startX = p.x; startY = p.y; startTime = Date.now(); lastMoveTs = 0;
sendMotionEvent(0, p.x, p.y); // ACTION_DOWN
clearLong();
longTimer = setTimeout(() => {
isLongPressed = true;
sendLongPress(p.x, p.y);
}, LONG_PRESS_MS);
}
function onMove(e) {
if (!active.value) 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();
if (now - lastMoveTs >= SAMPLE_MS) {
lastMoveTs = now;
sendMotionEvent(2, p.x, p.y); // ACTION_MOVE
}
}
function onUp(e) {
if (!active.value) return;
clearLong();
const p = mapRelative(e.clientX, e.clientY);
sendMotionEvent(1, p.x, p.y); // ACTION_UP
if (!isLongPressed) {
const dx = Math.abs(p.x - startX);
const dy = Math.abs(p.y - startY);
const dur = Date.now() - startTime;
if (dur < 200 && dx < TOUCH_SLOP && dy < TOUCH_SLOP) sendTouch(startX, startY);
else sendSwipe(startX, startY, p.x, p.y, Math.max(dur, 1));
}
}
</script>
<template>
<div class="screen-wrap">
<video ref="videoRef" class="remote-video" autoplay playsinline muted></video>
<div
ref="overlayRef"
class="touch-overlay"
:class="{ active }"
@pointerdown="onDown"
@pointermove="onMove"
@pointerup="onUp"
@pointercancel="clearLong"
></div>
<div v-if="!hasStream" class="placeholder">
<div class="big">📱</div>
<div v-if="!store.signalingConnected">请先在左侧连接信令服务器</div>
<div v-else-if="!store.targetDeviceId">请选择并连接一个被控端设备</div>
<div v-else>正在等待被控端画面</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import { store } from '../store/controllerStore';
</script>
<template>
<div class="stats-bar">
<span>分辨率: <b>{{ store.stats ? store.stats.width + '×' + store.stats.height : '-' }}</b></span>
<span>帧率: <b>{{ store.stats ? store.stats.fps : '-' }}</b></span>
<span>解码: <b>{{ store.stats ? store.stats.codec : '-' }}</b></span>
<span>下载: <b>{{ store.stats ? store.stats.downSpeed : '-' }}</b></span>
<span>时长: <b>{{ store.stats ? store.stats.duration : '-' }}</b></span>
<span>控制通道: <b>{{ store.dataChannelOpen ? '已连接' : '未连接' }}</b></span>
<span>目标: <b>{{ store.targetDeviceId || '-' }}</b></span>
</div>
</template>