mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 03:00:13 +02:00
The relay no longer enables the Runtime domain on systemui (bindings work without it; it streamed the page's console and context events). Full-rate frames only while a tip is within 10 cm of the keyboard or a trigger is pulled (not for a laser resting on it); idle frames every 1 s instead of 250 ms. Nothing while the keyboard is hidden.
66 lines
3.1 KiB
JavaScript
66 lines
3.1 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().
|
|
const SIDES = {
|
|
steam: { url: 'http://127.0.0.1:8080/json/list', title: 'SharedJSContext' },
|
|
vr: { url: 'http://127.0.0.1:8087/json/list', title: 'systemui', binding: '__sfuiCtlOut' },
|
|
};
|
|
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)})`);
|
|
}
|
|
|
|
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) 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.
|
|
if (cfg.binding) 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; }
|
|
}
|
|
})();
|
|
}
|