project: 增加web和adb控制,未测试
This commit is contained in:
66
WebRTCControllerWeb/src/components/ConnectionPanel.vue
Normal file
66
WebRTCControllerWeb/src/components/ConnectionPanel.vue
Normal 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>
|
||||
33
WebRTCControllerWeb/src/components/ControlBar.vue
Normal file
33
WebRTCControllerWeb/src/components/ControlBar.vue
Normal 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>
|
||||
112
WebRTCControllerWeb/src/components/RemoteScreen.vue
Normal file
112
WebRTCControllerWeb/src/components/RemoteScreen.vue
Normal 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>
|
||||
15
WebRTCControllerWeb/src/components/StatsBar.vue
Normal file
15
WebRTCControllerWeb/src/components/StatsBar.vue
Normal 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>
|
||||
Reference in New Issue
Block a user