Files
lhns--steam-frame-nix/modules/vr-keyboard-controllers/relay.mjs
T
Pierre Kisters 2442527514 VR keyboard: cleanup of the controller bridge, gesture input and docs
- gesture-input.js: drop the unused mode getter and release().
- relay.mjs: both sides have a binding; no condition.
- hub.js: the frame cadence is documented once (bridge-patch.js); errors
  listed.
- docs: the bridge's idle cost and "one word at a time" stated once;
  twoHanded's README row points to the docs; rewrapped lines.
2026-10-01 04:01:00 +02:00

74 lines
3.4 KiB
JavaScript

// relay.mjs (user service vr-keyboard-controllers-relay): carries the
// controller frames from SteamVR's systemui page (bridge-patch.js, 8087; CDP
// binding __sfuiCtlOut) to Steam's SharedJSContext (hub.js, 8080:
// __sfuiControllers.frame(frame)). Frames are rebuilt from their numbers
// (nothing else passes). If the systemui page goes away: lost(). Back from
// Steam (hub.js demand(ms), CDP binding __sfuiCtlIn): { fast: ms } ->
// __sfuiCtl.demand(ms) in systemui.
const SIDES = {
steam: { url: 'http://127.0.0.1:8080/json/list', title: 'SharedJSContext', binding: '__sfuiCtlIn', forward: demand },
vr: { url: 'http://127.0.0.1:8087/json/list', title: 'systemui', binding: '__sfuiCtlOut', forward },
};
const live = {}; // side -> evaluate(expression)
const num = (x) => (Number.isFinite(x) ? +x : null);
const point = (p, keys) => {
if (!p || typeof p !== 'object') return null;
const o = {};
for (const k of keys) { o[k] = num(p[k]); if (o[k] === null) return null; }
return o;
};
const hand = (h) => (h && typeof h === 'object'
? { tip: point(h.tip, ['u', 'v', 'd']), ray: point(h.ray, ['u', 'v', 'dist']), trigger: num(h.trigger) }
: null);
function clean(m) {
if (!m || typeof m !== 'object' || !Number.isFinite(m.seq) || !Number.isFinite(m.t)) return null;
const f = { seq: m.seq, t: m.t, moving: !!m.moving, keyboard: point(m.keyboard, ['width']), hands: { left: hand(m.hands?.left), right: hand(m.hands?.right) } };
for (const h of ['left', 'right']) if (f.hands[h] && !f.hands[h].tip) f.hands[h] = null;
return f;
}
function forward(payload) {
let m;
try { m = clean(JSON.parse(payload)); } catch { return; }
if (m) live.steam?.(`window.__sfuiControllers?.frame(${JSON.stringify(m)})`);
}
function demand(payload) {
let ms;
try { ms = num(JSON.parse(payload)?.fast); } catch { return; }
if (ms !== null) live.vr?.(`window.__sfuiCtl?.demand?.(${Math.max(0, Math.min(5000, ms))})`);
}
async function connect(side) {
const cfg = SIDES[side];
const list = await (await fetch(cfg.url, { signal: AbortSignal.timeout(3000) })).json();
const t = list.find((x) => x.title === cfg.title && x.webSocketDebuggerUrl);
if (!t) throw new Error(`${cfg.title} not found`);
const ws = new WebSocket(t.webSocketDebuggerUrl);
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
let id = 0;
const send = (method, params = {}) => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ id: ++id, method, params })); };
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.method === 'Runtime.bindingCalled' && m.params.name === cfg.binding) cfg.forward(m.params.payload);
};
const closed = new Promise((res) => { ws.onclose = res; });
// No Runtime.enable: bindings work without it, and it would stream the
// page's console and context events here.
send('Runtime.addBinding', { name: cfg.binding });
live[side] = (expression) => send('Runtime.evaluate', { expression });
console.log(`${side}: connected`);
await closed;
delete live[side];
if (side === 'vr') live.steam?.('window.__sfuiControllers?.lost()');
console.log(`${side}: disconnected`);
}
for (const side of Object.keys(SIDES)) {
(async () => {
for (let failed = false; ; await new Promise((r) => setTimeout(r, 3000))) {
try { await connect(side); failed = false; } catch (e) { if (!failed) console.error(`${side}: ${e.message}`); failed = true; }
}
})();
}