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:
MeowOS committed 2026-08-31 16:21:11 +08:00
1 parent 6c78a88b8c
commit fbdd77a72e
8 files changed
+333 -7

No files matched your search

+4
View File
@@ -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>
+8
View File
@@ -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{
+91 -1
View File
@@ -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();
// 首次进入探鉴权状态:启用且无有效会话 → 登录页;否则直接进应用