Codex. Cover support, updates for Music Box

This commit is contained in:
Jakub Zych
2026-04-27 21:10:13 +02:00
parent e961d264c0
commit 4c818e327b
10 changed files with 2248 additions and 129 deletions

View File

@@ -45,41 +45,6 @@
user-select: none;
}
/* ───────── Top HUD ───────── */
.hud {
position: absolute;
top: var(--pad-y);
left: var(--pad-x);
right: var(--pad-x);
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
font-family: var(--mono);
font-size: 20px;
font-weight: 700;
color: var(--hud);
letter-spacing: 0.08em;
text-shadow: 0 0 8px rgba(7, 8, 13, 0.9), 0 0 4px rgba(7, 8, 13, 0.9);
}
.hud .group { display: flex; gap: 24px; align-items: center; }
.hud > .group:nth-child(1) { justify-self: start; }
.hud > .group:nth-child(2) { justify-self: center; }
.hud > .group:nth-child(3) { justify-self: end; }
.hud .dim { color: var(--hud-dim); }
.hud .group > span { display: inline-flex; align-items: center; gap: 10px; }
.hud .led {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px var(--accent);
margin-top: -0.08em;
animation: rec-blink 1.6s ease-in-out infinite;
}
#signal {
display: inline-block;
transform: translateY(-0.10em);
}
@keyframes rec-blink {
0%, 55% { opacity: 1; }
65%, 100% { opacity: 0.2; }
@@ -186,6 +151,7 @@
.status-line { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.status-line + .status-line { margin-top: 8px; }
.status-line .sep { color: var(--hud-dim); }
.status-line .spacer { flex: 1 1 auto; }
.live-block {
display: flex; align-items: center; gap: 10px;
@@ -201,13 +167,27 @@
box-shadow: 0 0 10px var(--accent);
animation: rec-blink 1.6s ease-in-out infinite;
}
/* The "DESKTOP" label fills the slot the Game HUD uses for game name. */
/* OPHI-118 // DESKTOP — the rig identifier moved here from the old top HUD. */
.scene-name {
color: var(--ink);
font-weight: 700;
font-size: 22px;
letter-spacing: 0.10em;
}
/* HUD tag — TRANSMISSION/SIGNAL/UTC labels formerly in the top HUD,
restyled to sit cleanly in the bottom bar (cyan, mono, smaller). */
.hud-tag {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--hud);
font-weight: 700;
font-size: 16px;
letter-spacing: 0.08em;
text-shadow: 0 0 6px rgba(7, 8, 13, 0.9);
}
.hud-tag .dim { color: var(--hud-dim); }
#signal { display: inline-block; transform: translateY(-0.10em); }
.meta-line {
color: var(--ink-dim);
font-size: 16px;
@@ -217,21 +197,6 @@
</head>
<body>
<header class="hud">
<div class="group">
<span><span class="led"></span>TRANSMISSION</span>
<span>OPHI-118 // DESKTOP</span>
</div>
<div class="group">
<span class="dim">SIGNAL</span>
<span id="signal">▮▮▮▯▯</span>
</div>
<div class="group">
<span class="dim">UTC</span>
<span id="clock">--:--:--</span>
</div>
</header>
<div class="screen-frame" id="scr">
<span class="tick tl"></span>
<span class="tick tr"></span>
@@ -264,7 +229,11 @@
<span>LIVE</span>
</div>
<span class="sep">·</span>
<span class="scene-name">DESKTOP</span>
<span class="scene-name">OPHI-118 // DESKTOP</span>
<span class="spacer"></span>
<span class="hud-tag"><span class="dim">SIGNAL</span><span id="signal">▮▮▮▯▯</span></span>
<span class="sep">·</span>
<span class="hud-tag"><span class="dim">UTC</span><span id="clock">--:--:--</span></span>
</div>
<div class="status-line meta-line">
<span class="icon"></span>
@@ -311,12 +280,18 @@
tickElapsed(); setInterval(tickElapsed, 1000);
// ── OBS WS: live frame sync for camera + screen capture ──
// URL params let you rename the sources / scene without editing JS:
// ?camera=Webcam&screen=Display&scene=Desktop
// URL params:
// ?camera=Webcam override the source name to track for camera
// ?screen=Display override the source name to track for screen
// ?scene=Desktop LOCK the HUD to a specific scene (optional)
// When ?scene= is omitted (the default) the HUD follows whichever scene is
// currently program — so the same browser source can be referenced in both
// Desktop and Desktop (No Cam): switching scenes rebinds to the items in
// the now-active scene, and the camera/screen frames toggle/move with it.
const params = new URLSearchParams(location.search);
const CAM_SOURCE = params.get('camera') || 'Camera';
const SCR_SOURCE = params.get('screen') || 'Screen Capture (PipeWire)';
const SCENE = params.get('scene') || 'Desktop';
const SCENE_LOCK = params.get('scene') || null;
const camEl = document.getElementById('cam');
const scrEl = document.getElementById('scr');
@@ -336,10 +311,12 @@
};
}
// Bind a frame element to a scene item: live-sync size/position into the
// --{prefix}-x/y/w/h CSS vars and toggle .off on enable changes.
// Frame binding for one tracked source. Holds mutable {sceneName, itemId}
// so the event filter reads through to the *current* binding — that lets
// rebind() swap scenes without leaking listeners or stale itemIds.
// Assumes alignment 5 (top-left) — OBS's default for newly added items.
async function syncSourceFrame(obs, { sceneName, sourceName, frameEl, varPrefix }) {
function makeFrame(obs, sourceName, frameEl, varPrefix) {
const state = { sceneName: null, itemId: null };
const setVisible = (v) => frameEl.classList.toggle('off', !v);
const setTransform = (t) => {
if (!t) return;
@@ -350,30 +327,39 @@
rootStyle.setProperty(`--${varPrefix}-w`, `${w}px`);
rootStyle.setProperty(`--${varPrefix}-h`, `${h}px`);
};
let itemId = null;
try {
const r = await obs.call('GetSceneItemId', { sceneName, sourceName });
itemId = r.sceneItemId;
const e = await obs.call('GetSceneItemEnabled',
{ sceneName, sceneItemId: itemId });
setVisible(e.sceneItemEnabled);
const t = await obs.call('GetSceneItemTransform',
{ sceneName, sceneItemId: itemId });
setTransform(t.sceneItemTransform);
} catch (err) {
console.warn(`[desktop] frame sync failed (${sceneName}/${sourceName}):`, err.message);
return;
}
obs.addEventListener('event', (ev) => {
const d = ev.detail || {};
if (d.eventData?.sceneName !== sceneName
|| d.eventData?.sceneItemId !== itemId) return;
if (d.eventType === 'SceneItemEnableStateChanged') {
setVisible(d.eventData.sceneItemEnabled);
} else if (d.eventType === 'SceneItemTransformChanged') {
setTransform(d.eventData.sceneItemTransform);
}
});
return {
async rebind(sceneName) {
state.sceneName = sceneName;
state.itemId = null;
if (!sceneName) { setVisible(false); return; }
try {
const r = await obs.call('GetSceneItemId',
{ sceneName, sourceName });
state.itemId = r.sceneItemId;
const e = await obs.call('GetSceneItemEnabled',
{ sceneName, sceneItemId: state.itemId });
setVisible(e.sceneItemEnabled);
const t = await obs.call('GetSceneItemTransform',
{ sceneName, sceneItemId: state.itemId });
setTransform(t.sceneItemTransform);
} catch {
// Source isn't in this scene (or scene doesn't exist) — hide
// the frame outright. This is what makes "Desktop (No Cam)"
// work cleanly even if Camera were fully removed (rather than
// just disabled).
setVisible(false);
}
},
handleEvent(d) {
if (d.eventData?.sceneName !== state.sceneName
|| d.eventData?.sceneItemId !== state.itemId) return;
if (d.eventType === 'SceneItemEnableStateChanged') {
setVisible(d.eventData.sceneItemEnabled);
} else if (d.eventType === 'SceneItemTransformChanged') {
setTransform(d.eventData.sceneItemTransform);
}
},
};
}
async function connectOBS() {
@@ -381,14 +367,34 @@
try {
const obs = new OBSWSMini(window.__OBSWS.url, window.__OBSWS.password);
await obs.connect();
obs.addEventListener('close', () => {
setTimeout(connectOBS, 2500);
obs.addEventListener('close', () => setTimeout(connectOBS, 2500));
const camFrame = makeFrame(obs, CAM_SOURCE, camEl, 'cam');
const scrFrame = makeFrame(obs, SCR_SOURCE, scrEl, 'scr');
// Resolve the scene to track. With ?scene= explicit, lock to it;
// otherwise follow whatever's currently program. Modern obs-websocket
// returns currentProgramSceneName; older builds returned sceneName —
// accept both.
let scene = SCENE_LOCK;
if (!scene) {
try {
const r = await obs.call('GetCurrentProgramScene');
scene = r.currentProgramSceneName || r.sceneName || null;
} catch { /* leave null; rebind on first scene change */ }
}
await Promise.all([camFrame.rebind(scene), scrFrame.rebind(scene)]);
obs.addEventListener('event', async (ev) => {
const d = ev.detail || {};
if (!SCENE_LOCK && d.eventType === 'CurrentProgramSceneChanged') {
const next = d.eventData?.sceneName;
await Promise.all([camFrame.rebind(next), scrFrame.rebind(next)]);
return;
}
camFrame.handleEvent(d);
scrFrame.handleEvent(d);
});
// Run both syncs in parallel — independent items, independent failures.
await Promise.all([
syncSourceFrame(obs, { sceneName: SCENE, sourceName: CAM_SOURCE, frameEl: camEl, varPrefix: 'cam' }),
syncSourceFrame(obs, { sceneName: SCENE, sourceName: SCR_SOURCE, frameEl: scrEl, varPrefix: 'scr' }),
]);
} catch (e) {
// OBS WS optional — silently retry; defaults stand meanwhile.
setTimeout(connectOBS, 2500);