feat(meowpanel): 阶段C收尾——实时画面+远程遥控 (agent-224) — /api/screen.jpeg(fb0活动帧→自研最小baseline JPEG编码器,480x720≈24KB/帧,无libm零依赖) + /api/input/tap(坐标注入,BTN_TOUCH先行时序,本机触摸优先5s仲裁); 前端「屏幕」页(1fps可调,点击映射,息屏提示)
meowpanel→e98aec66 已部署; 真机验证: jpeg非全黑+注入tap拉起Files(日志+截图双证) 注: 注入走 write event1(实测本机可投递,未用uinput)
This commit is contained in:
1 parent
6c78a88b8c
commit
fbdd77a72e
8 files changed
+333
-7
No files matched your search
@@ -45,6 +45,10 @@
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="7" y="3" width="10" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="15" r="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M10 7h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>
|
||||
<span class="nav-label">远程</span>
|
||||
</button>
|
||||
<button class="nav-item" data-route="screen" title="实时画面 / 点击遥控">
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M8 21h8M12 17v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="8.5" cy="11" r="1.2" fill="currentColor"/><path d="M12 11l3.5 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>
|
||||
<span class="nav-label">屏幕</span>
|
||||
</button>
|
||||
<button class="nav-item" data-route="system" title="系统信息 / 升级">
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="6" y="3" width="12" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M9 8h6M9 12h6M9 16h3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>
|
||||
<span class="nav-label">系统</span>
|
||||
|
||||
@@ -236,6 +236,14 @@ input[type=range]{ width:100%; accent-color:var(--primary); }
|
||||
.app-card .app-meta{ font-size:12px; color:var(--on-surface-variant); }
|
||||
.app-card .app-actions{ display:flex; gap:6px; margin-top:auto; flex-wrap:wrap; }
|
||||
|
||||
/* 实时屏幕(屏幕页) */
|
||||
.screen-frame{ display:flex; justify-content:center; max-width:480px; margin:0 auto; }
|
||||
.screen-frame img{
|
||||
width:100%; max-width:480px; height:auto; display:block;
|
||||
border-radius:var(--radius-m); background:#000; cursor:crosshair;
|
||||
aspect-ratio:480/720; object-fit:contain; border:var(--outline-w) solid var(--outline-variant);
|
||||
}
|
||||
|
||||
/* 远程按键区 */
|
||||
.key-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:12px; }
|
||||
.key-btn{
|
||||
|
||||
@@ -805,6 +805,94 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 10. 实时屏幕 / 点击遥控 ---------------- */
|
||||
let screenTimer = null, screenBusy = false, screenOn = false;
|
||||
let screenObjUrl = null, screenFps = 1;
|
||||
function stopScreenPoll() {
|
||||
if (screenTimer) { clearInterval(screenTimer); screenTimer = null; }
|
||||
screenOn = false;
|
||||
}
|
||||
function restartScreenPoll() {
|
||||
if (screenTimer) { clearInterval(screenTimer); screenTimer = null; }
|
||||
const img = $('#screen-img'); if (!img) return;
|
||||
screenOn = true;
|
||||
const ms = Math.max(500, Math.round(1000 / screenFps));
|
||||
screenTimer = setInterval(fetchScreen, ms);
|
||||
}
|
||||
async function fetchScreen() {
|
||||
if (screenBusy) return;
|
||||
screenBusy = true;
|
||||
try {
|
||||
const headers = {};
|
||||
const token = storageGet('meowpanel.token');
|
||||
if (token) headers['X-MeowToken'] = token;
|
||||
const res = await fetch('/api/screen.jpeg?t=' + Date.now() + '&q=70',
|
||||
Object.keys(headers).length ? { headers: headers } : undefined);
|
||||
if (!res.ok) { $('#screen-status').textContent = '获取失败(HTTP ' + res.status + ')'; screenBusy = false; return; }
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = $('#screen-img');
|
||||
if (!img) { URL.revokeObjectURL(url); screenBusy = false; return; }
|
||||
img.src = url;
|
||||
if (screenObjUrl) URL.revokeObjectURL(screenObjUrl);
|
||||
screenObjUrl = url;
|
||||
$('#screen-status').textContent = '已更新 · ' + new Date().toLocaleTimeString();
|
||||
} catch (e) {
|
||||
$('#screen-status').textContent = '后端未连接';
|
||||
}
|
||||
screenBusy = false;
|
||||
}
|
||||
async function screenTap(e) {
|
||||
const img = $('#screen-img'); if (!img) return;
|
||||
const r = img.getBoundingClientRect();
|
||||
if (!r.width || !r.height) return;
|
||||
// 前端按 480x720(设备原生分辨率)换算
|
||||
const nx = Math.round((e.clientX - r.left) / r.width * 480);
|
||||
const ny = Math.round((e.clientY - r.top) / r.height * 720);
|
||||
const nxCL = Math.max(0, Math.min(479, nx)), nyCL = Math.max(0, Math.min(719, ny));
|
||||
try {
|
||||
const d = await api('POST', '/api/input/tap', { x: nxCL, y: nyCL });
|
||||
if (d && d.dispatched) toast('已注入 (' + nxCL + ',' + nyCL + ')');
|
||||
else if (d && d.reason === 'local_touch_active') toast('本机触摸中,注入已暂停');
|
||||
else toast('注入未派发');
|
||||
} catch (err) { toast('注入失败:' + errText(err)); }
|
||||
}
|
||||
function renderScreen() {
|
||||
screenFps = parseFloat(storageGet('screen.fps') || '1');
|
||||
if (!(screenFps >= 0.2 && screenFps <= 2)) screenFps = 1;
|
||||
let h = '<div class="page" id="screen-page">';
|
||||
h += '<div class="card"><h3 class="card-title">实时画面</h3>' +
|
||||
'<p class="muted">点击画面 = 在设备上点击该处。本机有人触摸时,注入自动暂停 5 秒;设备息屏请先点亮。</p>' +
|
||||
'<div class="screen-frame">' +
|
||||
'<img id="screen-img" alt="设备实时画面">' +
|
||||
'</div>' +
|
||||
'<div class="field-row" style="margin-top:12px">' +
|
||||
'<label class="muted" for="screen-fps">刷新率</label>' +
|
||||
'<input type="range" id="screen-fps" min="0.2" max="2" step="0.1" value="' + screenFps + '">' +
|
||||
'<span class="muted" id="screen-fps-lbl" style="min-width:56px">' + screenFps.toFixed(1) + ' fps</span>' +
|
||||
'</div>' +
|
||||
'<p class="muted" id="screen-status">载入中…</p>' +
|
||||
'</div></div>';
|
||||
$('#content').innerHTML = h;
|
||||
const slider = $('#screen-fps');
|
||||
slider.addEventListener('input', function () {
|
||||
const v = parseFloat(slider.value);
|
||||
screenFps = v; storageSet('screen.fps', String(v));
|
||||
$('#screen-fps-lbl').textContent = v.toFixed(1) + ' fps';
|
||||
restartScreenPoll();
|
||||
});
|
||||
$('#screen-img').addEventListener('click', screenTap);
|
||||
restartScreenPoll();
|
||||
fetchScreen();
|
||||
}
|
||||
function bindScreen() {
|
||||
// 页面隐藏(切到其它标签页)时停刷;回到本页继续
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.hidden) stopScreenPoll();
|
||||
else if (state.route === 'screen') restartScreenPoll();
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 路由 ---------------- */
|
||||
const ROUTES = {
|
||||
perf: { title: '实时性能', render: renderPerf, bind: null },
|
||||
@@ -813,6 +901,7 @@
|
||||
apps: { title: 'MeowAPPs 管理', render: renderApps, bind: bindApps },
|
||||
settings: { title: '设置镜像', render: renderSettings, bind: bindSettings },
|
||||
remote: { title: '远程控制', render: renderRemote, bind: bindRemote },
|
||||
screen: { title: '实时屏幕', render: renderScreen, bind: bindScreen },
|
||||
system: { title: '系统信息 / 升级', render: renderSystem, bind: bindSystem },
|
||||
logs: { title: '系统日志', render: renderLogs, bind: bindLogs },
|
||||
notif: { title: '通知中心', render: renderNotif, bind: bindNotif }
|
||||
@@ -828,6 +917,7 @@
|
||||
const r = ROUTES[id] || ROUTES.perf;
|
||||
state.route = id;
|
||||
stopPerf();
|
||||
stopScreenPoll();
|
||||
$$('.nav-item').forEach(b => b.classList.toggle('active', b.dataset.route === id));
|
||||
$('#page-title').textContent = r.title;
|
||||
r.render();
|
||||
@@ -846,7 +936,7 @@
|
||||
bindNav();
|
||||
bindThemeUi();
|
||||
bindSlider();
|
||||
bindFiles(); bindApps(); bindSettings(); bindRemote(); bindSystem(); bindLogs(); bindNotif();
|
||||
bindFiles(); bindApps(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif();
|
||||
bindLogin();
|
||||
bindPwDlg();
|
||||
// 首次进入探鉴权状态:启用且无有效会话 → 登录页;否则直接进应用
|
||||
|
||||
Reference in new issue
Block a user