feat: 增加分辨率修改
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user