feat: 增加分辨率修改

This commit is contained in:
2026-07-21 11:05:40 +08:00
parent 9e5ac91e46
commit 341dce249f
25 changed files with 646 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
<script setup>
import { store, sendKey } from '../store/controllerStore';
import { ref, watch } from 'vue';
import { store, sendKey, sendResolutionChange } from '../store/controllerStore';
// Android KeyEvent 键值(与被控端 SystemInputUtils 注入一致)
const keys = [
@@ -12,14 +13,60 @@ const keys = [
{ label: '电源', code: 26 }, // KEYCODE_POWER
];
// 分辨率预设width 为长边像素0 表示被控端原生分辨率;
// height 传 0由被控端按屏幕宽高比自动计算fps 传 0沿用当前帧率
const resolutionPresets = [
{ label: '原始画质', width: 0, height: 0, fps: 0 },
{ label: '1080P', width: 1920, height: 0, fps: 0 },
{ label: '720P', width: 1280, height: 0, fps: 0 },
{ label: '480P', width: 854, height: 0, fps: 0 },
];
// 注意:<option :value="p.width"> 的 value 是数字Vue 的 v-model 会按数字写入,
// 因此这里也必须用数字(不能用字符串 '0'),否则默认选中项与比较逻辑会类型不一致。
const selectedResolution = ref(0);
function press(code) {
if (!store.dataChannelOpen) return;
sendKey(code);
}
function onResolutionChange() {
if (!store.dataChannelOpen) return;
// 统一按数值匹配v-model 写入的是数字,不能用 String() 再与数字比较。
const preset = resolutionPresets.find((p) => p.width === Number(selectedResolution.value));
if (preset) {
sendResolutionChange(preset.width, preset.height, preset.fps);
}
}
// 被控端上报当前实际采集分辨率时,把下拉框同步到对应预设(避免 UI 与实际不一致)。
watch(() => store.currentResolution, (res) => {
if (!res) return;
const longEdge = Math.max(res.width, res.height);
const preset = resolutionPresets.find((p) => p.width === longEdge);
if (preset) selectedResolution.value = preset.width;
});
</script>
<template>
<div class="control-bar">
<div class="resolution-row">
<label class="resolution-label">分辨率:</label>
<select
class="resolution-select"
v-model="selectedResolution"
:disabled="!store.dataChannelOpen"
@change="onResolutionChange"
>
<option v-for="p in resolutionPresets" :key="p.label" :value="p.width">
{{ p.label }}
</option>
</select>
<span class="resolution-current" v-if="store.currentResolution">
当前: {{ store.currentResolution.width }}×{{ store.currentResolution.height }}
</span>
</div>
<button
v-for="k in keys"
:key="k.code"

View File

@@ -13,6 +13,8 @@ export const Action = {
KEY: 3,
LONG_PRESS: 4,
MOTION_EVENT: 5,
SET_RESOLUTION: 6,
REPORT_RESOLUTION: 7,
};
export async function loadProto() {
@@ -29,3 +31,9 @@ export function encodeControlMessage(fields) {
// 返回 Uint8Array可直接通过 RTCDataChannel.send 发送(二进制)。
return ControlMessage.encode(message).finish();
}
/** 解析被控端经 DataChannel 发来的二进制 protobuf如分辨率上报。返回消息对象。 */
export function decodeControlMessage(bytes) {
if (!ControlMessage) throw new Error('protobuf 尚未加载,请先调用 loadProto()');
return ControlMessage.decode(bytes);
}

View File

@@ -1,4 +1,4 @@
import { encodeControlMessage } from '../proto/controlMessage';
import { encodeControlMessage, decodeControlMessage, Action } from '../proto/controlMessage';
// 对应 Android 端 WebRtcClient / Flutter webrtc_controller.dart。
// 作为 OFFER 方仅接收远端视频recvonly+ 创建控制用 DataChannel
@@ -6,7 +6,7 @@ import { encodeControlMessage } from '../proto/controlMessage';
const DATA_CHANNEL_LABEL = 'control_channel';
export class WebRtcController {
constructor({ iceServers, deviceId, targetDeviceId, signaling, onIceState, onDataChannelState, onStream, onStats, onError }) {
constructor({ iceServers, deviceId, targetDeviceId, signaling, onIceState, onDataChannelState, onStream, onStats, onError, onResolutionReport }) {
this.iceServers = iceServers;
this.deviceId = deviceId;
this.targetDeviceId = targetDeviceId;
@@ -16,6 +16,7 @@ export class WebRtcController {
this.onStream = onStream;
this.onStats = onStats;
this.onError = onError;
this.onResolutionReport = onResolutionReport;
this.pc = null;
this.dataChannel = null;
@@ -96,7 +97,22 @@ export class WebRtcController {
this.dataChannel = dc;
dc.onopen = () => this.onDataChannelState && this.onDataChannelState(true);
dc.onclose = () => this.onDataChannelState && this.onDataChannelState(false);
dc.onmessage = (e) => console.debug('[DataChannel] 收到消息', e.data);
dc.onmessage = (e) => {
try {
// 被控端发来的是二进制 protobuf目前仅 REPORT_RESOLUTION 上报)。
const bytes = e.data instanceof ArrayBuffer ? new Uint8Array(e.data) : e.data;
const msg = decodeControlMessage(bytes);
if (msg && msg.action === Action.REPORT_RESOLUTION) {
this.onResolutionReport && this.onResolutionReport({
width: msg.width | 0,
height: msg.height | 0,
fps: msg.fps | 0,
});
}
} catch (err) {
console.debug('[DataChannel] 解码消息失败(非分辨率上报或格式异常)', err);
}
};
}
async handleAnswer(sdp) {
@@ -147,6 +163,7 @@ export class WebRtcController {
sendKey(keyCode) { return this.sendControlMessage({ action: 3, keyCode, keyAction: 0 }); } // KEY
sendLongPress(x, y) { return this.sendControlMessage({ action: 4, x, y }); } // LONG_PRESS
sendMotionEvent(action, x, y) { return this.sendControlMessage({ action: 5, motionAction: action, x, y }); } // MOTION_EVENT
sendResolutionChange(width, height, fps) { return this.sendControlMessage({ action: 6, width, height, fps }); } // SET_RESOLUTION
_startStats() {
this._statsTimer = setInterval(async () => {

View File

@@ -35,6 +35,8 @@ export const store = reactive({
controlledDevices: [],
stats: null,
remoteStream: null,
// 被控端上报的当前实际采集分辨率(宽/高/帧率),用于与分辨率下拉框保持一致。
currentResolution: null,
});
let signaling = null;
@@ -142,6 +144,8 @@ export async function connectToDevice(targetId) {
onStream: (stream) => { store.remoteStream = markRaw(stream); },
onStats: (stats) => { store.stats = stats; },
onError: (msg) => { if (msg) store.error = msg; },
// 被控端上报当前实际采集分辨率,同步到 store 供 UI 展示/匹配预设。
onResolutionReport: (res) => { store.currentResolution = res; },
});
await webrtc.createOffer();
} catch (e) {
@@ -160,6 +164,7 @@ export async function disconnectDevice() {
store.dataChannelOpen = false;
store.remoteStream = null;
store.stats = null;
store.currentResolution = null;
store.statusText = store.signalingConnected ? '已断开设备连接' : '未连接';
}
@@ -177,3 +182,5 @@ export function sendSwipe(x1, y1, x2, y2, duration) { return webrtc && webrtc.se
export function sendLongPress(x, y) { return webrtc && webrtc.sendLongPress(x, y); }
export function sendMotionEvent(action, x, y) { return webrtc && webrtc.sendMotionEvent(action, x, y); }
export function sendKey(keyCode) { return webrtc && webrtc.sendKey(keyCode); }
// 请求被控端切换屏幕采集分辨率width<=0 表示原生分辨率height<=0 时按 width 长边依宽高比缩放)
export function sendResolutionChange(width, height, fps) { return webrtc && webrtc.sendResolutionChange(width, height, fps); }

View File

@@ -161,6 +161,26 @@ body {
.key-btn:hover { border-color: var(--accent); }
.key-btn:active { background: var(--accent); }
.resolution-row {
display: flex;
align-items: center;
gap: 8px;
}
.resolution-label { font-size: 13px; color: var(--text); font-weight: 600; }
.resolution-select {
padding: 9px 11px;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 13px;
outline: none;
cursor: pointer;
}
.resolution-select:focus { border-color: var(--accent); }
.resolution-select:disabled { opacity: .5; cursor: not-allowed; }
.resolution-current { font-size: 12px; color: var(--muted); }
.footer { background: var(--panel); border-top: 1px solid var(--border); }
.stats-bar {
display: flex;