Codex. Cover support, updates for Music Box
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user