screen: add match-size — RFB SetDesktopSize to resize the remote desktop
This commit is contained in:
parent
317d580405
commit
f510a321df
2 changed files with 94 additions and 1 deletions
|
|
@ -549,7 +549,12 @@ shaped).
|
||||||
- `GET /screen` — VNC viewer page (minimal RFB-over-WebSocket
|
- `GET /screen` — VNC viewer page (minimal RFB-over-WebSocket
|
||||||
renderer). Only accessible when `hyperhive.gui.enable = true`
|
renderer). Only accessible when `hyperhive.gui.enable = true`
|
||||||
in the agent's `agent.nix`; the harness shows a 🖥 screen link
|
in the agent's `agent.nix`; the harness shows a 🖥 screen link
|
||||||
in the state row when `gui_vnc_port` is present.
|
in the state row when `gui_vnc_port` is present. Toolbar:
|
||||||
|
`⤢ fit` CSS-downscales the canvas to the window; `⤡ match size`
|
||||||
|
sends an RFB `SetDesktopSize` request so the server (weston)
|
||||||
|
changes its real output resolution to the window dimensions —
|
||||||
|
enabled once the server advertises the `ExtendedDesktopSize`
|
||||||
|
pseudo-encoding (issue #133).
|
||||||
- `GET /screen/ws` — raw RFB byte relay: proxies WebSocket
|
- `GET /screen/ws` — raw RFB byte relay: proxies WebSocket
|
||||||
frames to the weston VNC server at `127.0.0.1:<vnc_port>`.
|
frames to the weston VNC server at `127.0.0.1:<vnc_port>`.
|
||||||
Transparent to any RFB variant. VNC port comes from
|
Transparent to any RFB variant. VNC port comes from
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ html, body { height: 100%; background: var(--base); color: var(--text);
|
||||||
border: 1px solid var(--surface1); border-radius: 4px; cursor: pointer;
|
border: 1px solid var(--surface1); border-radius: 4px; cursor: pointer;
|
||||||
}
|
}
|
||||||
.tbtn.active { color: var(--green); border-color: var(--green); }
|
.tbtn.active { color: var(--green); border-color: var(--green); }
|
||||||
|
.tbtn:disabled { opacity: 0.4; cursor: default; }
|
||||||
#status { margin-left: auto; font-size: 0.75rem; color: var(--subtext0); }
|
#status { margin-left: auto; font-size: 0.75rem; color: var(--subtext0); }
|
||||||
#status.connected { color: var(--green); }
|
#status.connected { color: var(--green); }
|
||||||
#status.error { color: var(--red); }
|
#status.error { color: var(--red); }
|
||||||
|
|
@ -83,6 +84,7 @@ canvas { display: block; cursor: default; }
|
||||||
<strong>🖥 screen</strong>
|
<strong>🖥 screen</strong>
|
||||||
<a href="/" title="back to agent page">← agent</a>
|
<a href="/" title="back to agent page">← agent</a>
|
||||||
<button id="fit-toggle" class="tbtn" title="Toggle fit-to-window scaling">⤢ fit</button>
|
<button id="fit-toggle" class="tbtn" title="Toggle fit-to-window scaling">⤢ fit</button>
|
||||||
|
<button id="match-toggle" class="tbtn" title="Resize the remote desktop to fit this window" disabled>⤡ match size</button>
|
||||||
<button id="debug-toggle" class="tbtn" title="Toggle RFB debug log">debug</button>
|
<button id="debug-toggle" class="tbtn" title="Toggle RFB debug log">debug</button>
|
||||||
<span id="status">connecting…</span>
|
<span id="status">connecting…</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -109,6 +111,7 @@ canvas { display: block; cursor: default; }
|
||||||
const debugLog = document.getElementById('debug-log');
|
const debugLog = document.getElementById('debug-log');
|
||||||
const debugBtn = document.getElementById('debug-toggle');
|
const debugBtn = document.getElementById('debug-toggle');
|
||||||
const fitBtn = document.getElementById('fit-toggle');
|
const fitBtn = document.getElementById('fit-toggle');
|
||||||
|
const matchBtn = document.getElementById('match-toggle');
|
||||||
const canvasWrap = document.getElementById('canvas-wrap');
|
const canvasWrap = document.getElementById('canvas-wrap');
|
||||||
|
|
||||||
// --- Debug log ---
|
// --- Debug log ---
|
||||||
|
|
@ -160,6 +163,23 @@ canvas { display: block; cursor: default; }
|
||||||
window.addEventListener('resize', relayoutCanvas);
|
window.addEventListener('resize', relayoutCanvas);
|
||||||
applyFitMode();
|
applyFitMode();
|
||||||
|
|
||||||
|
// --- Match-size: resize the remote desktop to this window ---
|
||||||
|
// Sends an RFB SetDesktopSize request so the VNC server (weston)
|
||||||
|
// changes its actual output resolution to match the browser
|
||||||
|
// viewport — sharper than fit-mode's CSS downscale. The button is
|
||||||
|
// enabled only once the server has advertised the ExtendedDesktopSize
|
||||||
|
// pseudo-encoding (a -308 rect). (issue #133)
|
||||||
|
let extDesktopSupported = false;
|
||||||
|
let screenId = 1; // captured from the server's ExtendedDesktopSize advert
|
||||||
|
matchBtn.addEventListener('click', () => {
|
||||||
|
if (!extDesktopSupported) return;
|
||||||
|
// Even dimensions — some servers reject odd ones.
|
||||||
|
const w = Math.max(2, canvasWrap.clientWidth & ~1);
|
||||||
|
const h = Math.max(2, canvasWrap.clientHeight & ~1);
|
||||||
|
dbg('→ request desktop resize to ' + w + 'x' + h, 'send');
|
||||||
|
sendSetDesktopSize(w, h);
|
||||||
|
});
|
||||||
|
|
||||||
function hex(bytes) {
|
function hex(bytes) {
|
||||||
return Array.from(bytes).map(b => b.toString(16).padStart(2,'0')).join(' ');
|
return Array.from(bytes).map(b => b.toString(16).padStart(2,'0')).join(' ');
|
||||||
}
|
}
|
||||||
|
|
@ -216,6 +236,9 @@ canvas { display: block; cursor: default; }
|
||||||
let fbW = 0, fbH = 0;
|
let fbW = 0, fbH = 0;
|
||||||
let pixelFormat = null; // set after ServerInit
|
let pixelFormat = null; // set after ServerInit
|
||||||
let updateRects = 0;
|
let updateRects = 0;
|
||||||
|
// ExtendedDesktopSize pseudo-encoding (-308), as the unsigned 32-bit
|
||||||
|
// value the rect-header encoding field is read as.
|
||||||
|
const EXT_DESKTOP_SIZE_U32 = (-308) >>> 0;
|
||||||
|
|
||||||
// Drain bytes from the queue into a flat buffer view
|
// Drain bytes from the queue into a flat buffer view
|
||||||
function drainTo(n) {
|
function drainTo(n) {
|
||||||
|
|
@ -544,6 +567,9 @@ canvas { display: block; cursor: default; }
|
||||||
canvas.height = fbH;
|
canvas.height = fbH;
|
||||||
relayoutCanvas();
|
relayoutCanvas();
|
||||||
setStatus('connected', 'connected');
|
setStatus('connected', 'connected');
|
||||||
|
// Advertise Raw + the ExtendedDesktopSize pseudo-encoding so the
|
||||||
|
// server reports (and accepts) desktop-size changes. (issue #133)
|
||||||
|
sendSetEncodings([0, -308]);
|
||||||
// Request full framebuffer update
|
// Request full framebuffer update
|
||||||
requestUpdate(0, 0, 0, fbW, fbH);
|
requestUpdate(0, 0, 0, fbW, fbH);
|
||||||
state = 'normal';
|
state = 'normal';
|
||||||
|
|
@ -583,6 +609,27 @@ canvas { display: block; cursor: default; }
|
||||||
const pixels = drainTo(bytes);
|
const pixels = drainTo(bytes);
|
||||||
if (!pixels) { chunks.unshift(b); totalBytes += 12; return false; }
|
if (!pixels) { chunks.unshift(b); totalBytes += 12; return false; }
|
||||||
drawRaw(x, y, w, h, pixels);
|
drawRaw(x, y, w, h, pixels);
|
||||||
|
} else if (enc === EXT_DESKTOP_SIZE_U32) {
|
||||||
|
// ExtendedDesktopSize: w,h carry the new desktop dimensions;
|
||||||
|
// the rect body is nScreens(1) + pad(3) + nScreens×16. The
|
||||||
|
// header's x = change reason, y = request status. (issue #133)
|
||||||
|
const nScreens = peekByte();
|
||||||
|
if (nScreens < 0) { chunks.unshift(b); totalBytes += 12; return false; }
|
||||||
|
const body = drainTo(4 + nScreens * 16);
|
||||||
|
if (!body) { chunks.unshift(b); totalBytes += 12; return false; }
|
||||||
|
if (nScreens > 0) screenId = u32be(body, 4); // reuse the server's screen id
|
||||||
|
if (!extDesktopSupported) {
|
||||||
|
extDesktopSupported = true;
|
||||||
|
matchBtn.disabled = false;
|
||||||
|
}
|
||||||
|
if (w && h && (w !== fbW || h !== fbH)) {
|
||||||
|
dbg('← desktop resized to ' + w + 'x' + h
|
||||||
|
+ ' (reason ' + x + ', status ' + y + ')', 'ok');
|
||||||
|
fbW = w; fbH = h;
|
||||||
|
canvas.width = w; canvas.height = h;
|
||||||
|
relayoutCanvas();
|
||||||
|
requestUpdate(0, 0, 0, fbW, fbH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateRects--;
|
updateRects--;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -623,6 +670,47 @@ canvas { display: block; cursor: default; }
|
||||||
send(b);
|
send(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetEncodings (msg type 2): advertise the encodings we understand.
|
||||||
|
// Negative values are pseudo-encodings (e.g. -308 ExtendedDesktopSize).
|
||||||
|
function sendSetEncodings(encs) {
|
||||||
|
const b = new Uint8Array(4 + encs.length * 4);
|
||||||
|
b[0] = 2; // message-type
|
||||||
|
b[1] = 0; // padding
|
||||||
|
b[2] = encs.length >> 8; b[3] = encs.length & 0xff;
|
||||||
|
let o = 4;
|
||||||
|
for (const e of encs) {
|
||||||
|
const v = e >>> 0; // two's-complement for negatives
|
||||||
|
b[o++] = (v>>24)&0xff; b[o++] = (v>>16)&0xff;
|
||||||
|
b[o++] = (v>>8)&0xff; b[o++] = v&0xff;
|
||||||
|
}
|
||||||
|
send(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDesktopSize (msg type 251): ask the server to change the desktop
|
||||||
|
// resolution. One screen at the origin, sized to the request. (#133)
|
||||||
|
function sendSetDesktopSize(w, h) {
|
||||||
|
const b = new Uint8Array(24);
|
||||||
|
b[0] = 251; b[1] = 0; // message-type + padding
|
||||||
|
b[2] = w>>8; b[3] = w&0xff;
|
||||||
|
b[4] = h>>8; b[5] = h&0xff;
|
||||||
|
b[6] = 1; b[7] = 0; // number-of-screens + padding
|
||||||
|
// screen: id(4) x(2) y(2) width(2) height(2) flags(4)
|
||||||
|
b[8] = (screenId>>>24)&0xff; b[9] = (screenId>>>16)&0xff;
|
||||||
|
b[10] = (screenId>>>8)&0xff; b[11] = screenId&0xff;
|
||||||
|
b[12] = 0; b[13] = 0; // x-position
|
||||||
|
b[14] = 0; b[15] = 0; // y-position
|
||||||
|
b[16] = w>>8; b[17] = w&0xff;
|
||||||
|
b[18] = h>>8; b[19] = h&0xff;
|
||||||
|
b[20] = 0; b[21] = 0; b[22] = 0; b[23] = 0; // flags
|
||||||
|
send(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek the first unconsumed byte without draining it. -1 when empty.
|
||||||
|
function peekByte() {
|
||||||
|
for (const c of chunks) { if (c.length) return c[0]; }
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Input forwarding ---
|
// --- Input forwarding ---
|
||||||
canvas.addEventListener('mousemove', sendPointer);
|
canvas.addEventListener('mousemove', sendPointer);
|
||||||
canvas.addEventListener('mousedown', sendPointer);
|
canvas.addEventListener('mousedown', sendPointer);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue