webapp: archive pre-migration scene dirs + bash scripts, update docs
Moves the seven HTML scene dirs (landing/, loading/, game/, desktop/,
goodbye/, music-box/, music/) and the superseded bash helpers (setup.sh,
loading.sh, playlist.sh, telemetry.sh) into archived/ rather than deleting.
The Laravel webapp/ replaces all of them; archived/README.md spells out
the rollback procedure.
Also:
- rewrite the relevant sections of CLAUDE.md so it points at webapp/
blade views, the Artisan commands, and the supervisord lifecycle
(`supervisorctl restart obs-webapp` after route / controller / .env
changes; Blade view edits are still safe-while-running via OBS's
Refresh cache).
- extend scripts/deploy-rig.sh to install php + composer + supervisor,
run `composer install`, copy obs-webapp.supervisord.conf into
/etc/supervisor.d/, start the program, and call `php artisan
rig:setup` + `rig:telemetry --collect` instead of the old bash.
- .gitignore catches the generated machine-local files that came along
when the old scene dirs moved (telemetry.js, loading.json, playlist.js,
cmd.js, obs-config.js).
- daemon.blade.php is now passive — listens to mpd:state but does not
play audio or broadcast its own queue, so it stops fighting with
bridges/mpd-state.py (the post-browser-daemon-migration source of
truth for mpd:state).
- nc.blade.php overrides .terminal { overflow: hidden } from hud.css
so the `┤ TERMINAL ├` and `┤ NOW PLAYING ├` pane-tabs stick above
the pane border instead of being clipped.
This commit is contained in:
406
archived/scenes/desktop/index.html
Normal file
406
archived/scenes/desktop/index.html
Normal file
@@ -0,0 +1,406 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OPHI-118 / DESKTOP</title>
|
||||
<style>
|
||||
:root {
|
||||
--ink: #e8e8e0;
|
||||
--ink-dim: rgba(232, 232, 224, 0.62);
|
||||
--hud: #4fd2ff;
|
||||
--hud-dim: #2a7fa6;
|
||||
--hud-edge: rgba(79, 210, 255, 0.85);
|
||||
--hud-glow: rgba(79, 210, 255, 0.40);
|
||||
--accent: #e63a2e;
|
||||
--warn: #ffd000;
|
||||
--term-fg: #5fdc62;
|
||||
--term-fg-dim:#2c8d2f;
|
||||
--mono: 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', monospace;
|
||||
--display: 'DejaVu Sans', 'Liberation Sans', 'Helvetica', 'Arial', sans-serif;
|
||||
--pad-y: 40px;
|
||||
--pad-x: 72px;
|
||||
/* Camera frame transform — auto-synced from the Desktop scene's Camera
|
||||
item via OBS WS. These defaults are first-paint fallback. */
|
||||
--cam-x: 1937px;
|
||||
--cam-y: 98px;
|
||||
--cam-w: 549px;
|
||||
--cam-h: 309px;
|
||||
--cam-pad: 10px;
|
||||
/* Screen-capture frame transform — auto-synced from the Screen Capture
|
||||
item the same way. Defaults match the current scene's bounds. */
|
||||
--scr-x: 10px;
|
||||
--scr-y: 14px;
|
||||
--scr-w: 1976px;
|
||||
--scr-h: 1209px;
|
||||
--scr-pad: 0px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%; height: 100%;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font-family: var(--display);
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@keyframes rec-blink {
|
||||
0%, 55% { opacity: 1; }
|
||||
65%, 100% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
/* ───────── Camera frame (shared logic with game/index.html) ───────── */
|
||||
.camera-frame {
|
||||
position: absolute;
|
||||
top: calc(var(--cam-y) - var(--cam-pad));
|
||||
left: calc(var(--cam-x) - var(--cam-pad));
|
||||
width: calc(var(--cam-w) + var(--cam-pad) * 2);
|
||||
height: calc(var(--cam-h) + var(--cam-pad) * 2);
|
||||
pointer-events: none;
|
||||
background: rgba(6, 10, 18, 0.85);
|
||||
border: 1.5px solid rgba(79, 210, 255, 0.55);
|
||||
box-shadow:
|
||||
0 0 28px rgba(79, 210, 255, 0.20),
|
||||
inset 0 0 60px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
/* ───────── Screen-capture frame ─────────
|
||||
Same pattern as the camera frame — corner ticks + label + tracked size —
|
||||
but transparent (the desktop renders behind, must show through) and a
|
||||
thinner perimeter line, since at full-canvas scale a bright cyan rectangle
|
||||
all around the desktop reads as too loud. Bright ticks carry the "this is
|
||||
a framed feed" signal; the line itself is just a hint. */
|
||||
.screen-frame {
|
||||
position: absolute;
|
||||
top: calc(var(--scr-y) - var(--scr-pad));
|
||||
left: calc(var(--scr-x) - var(--scr-pad));
|
||||
width: calc(var(--scr-w) + var(--scr-pad) * 2);
|
||||
height: calc(var(--scr-h) + var(--scr-pad) * 2);
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(79, 210, 255, 0.30);
|
||||
box-shadow: 0 0 18px rgba(79, 210, 255, 0.08);
|
||||
}
|
||||
|
||||
.camera-frame .tick,
|
||||
.screen-frame .tick {
|
||||
position: absolute;
|
||||
width: 24px; height: 24px;
|
||||
border: 3px solid var(--hud-edge);
|
||||
box-shadow: 0 0 8px var(--hud-glow);
|
||||
}
|
||||
.camera-frame .tick.tl, .screen-frame .tick.tl { top: -2px; left: -2px; border-right: none; border-bottom: none; }
|
||||
.camera-frame .tick.tr, .screen-frame .tick.tr { top: -2px; right: -2px; border-left: none; border-bottom: none; }
|
||||
.camera-frame .tick.bl, .screen-frame .tick.bl { bottom: -2px; left: -2px; border-right: none; border-top: none; }
|
||||
.camera-frame .tick.br, .screen-frame .tick.br { bottom: -2px; right: -2px; border-left: none; border-top: none; }
|
||||
.camera-frame .label,
|
||||
.screen-frame .label {
|
||||
position: absolute;
|
||||
top: 10px; left: 14px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--hud);
|
||||
letter-spacing: 0.32em;
|
||||
text-shadow: 0 0 4px rgba(7, 8, 13, 0.9);
|
||||
}
|
||||
.camera-frame .placeholder {
|
||||
position: absolute; inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
font-family: var(--mono);
|
||||
color: var(--hud);
|
||||
pointer-events: none;
|
||||
}
|
||||
.camera-frame .placeholder svg { opacity: 0.6; }
|
||||
.camera-frame .placeholder .ph-text {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.45em;
|
||||
color: rgba(79, 210, 255, 0.7);
|
||||
}
|
||||
.camera-frame .placeholder .ph-coords {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.20em;
|
||||
color: var(--hud-dim);
|
||||
}
|
||||
.camera-frame.off,
|
||||
.screen-frame.off { display: none; }
|
||||
|
||||
/* ───────── Bottom status bar ───────── */
|
||||
.status-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0; right: 0;
|
||||
padding: 22px var(--pad-x) 26px;
|
||||
background:
|
||||
linear-gradient(
|
||||
to top,
|
||||
rgba(7, 8, 13, 0.96) 0%,
|
||||
rgba(7, 8, 13, 0.96) 65%,
|
||||
rgba(7, 8, 13, 0.50) 90%,
|
||||
rgba(7, 8, 13, 0) 100%
|
||||
);
|
||||
border-top: 1px solid rgba(79, 210, 255, 0.28);
|
||||
box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.5);
|
||||
font-family: var(--mono);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.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;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
font-size: 18px;
|
||||
}
|
||||
.live-dot {
|
||||
width: 11px; height: 11px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 10px var(--accent);
|
||||
animation: rec-blink 1.6s ease-in-out infinite;
|
||||
}
|
||||
/* 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;
|
||||
}
|
||||
.meta-line .icon { color: var(--hud); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="screen-frame" id="scr">
|
||||
<span class="tick tl"></span>
|
||||
<span class="tick tr"></span>
|
||||
<span class="tick bl"></span>
|
||||
<span class="tick br"></span>
|
||||
<span class="label">SCR 01</span>
|
||||
</div>
|
||||
|
||||
<div class="camera-frame" id="cam">
|
||||
<span class="tick tl"></span>
|
||||
<span class="tick tr"></span>
|
||||
<span class="tick bl"></span>
|
||||
<span class="tick br"></span>
|
||||
<span class="label">CAM 01</span>
|
||||
<div class="placeholder">
|
||||
<svg width="64" height="48" viewBox="0 0 64 48" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
<rect x="2" y="8" width="44" height="32" rx="2" stroke="#4fd2ff" stroke-width="2"/>
|
||||
<path d="M46 18 L60 10 L60 38 L46 30 Z" stroke="#4fd2ff" stroke-width="2" stroke-linejoin="round"/>
|
||||
<circle cx="14" cy="14" r="2" fill="#e63a2e"/>
|
||||
</svg>
|
||||
<span class="ph-text">— WAITING FOR CAMERA —</span>
|
||||
<span class="ph-coords">auto-syncs from OBS scene transform</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="status-bar">
|
||||
<div class="status-line">
|
||||
<div class="live-block">
|
||||
<span class="live-dot"></span>
|
||||
<span>LIVE</span>
|
||||
</div>
|
||||
<span class="sep">·</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>
|
||||
<span class="elapsed" id="elapsed">00:00:00</span>
|
||||
<span class="sep">·</span>
|
||||
<span class="icon" title="microphone">🎙</span>
|
||||
<span style="color: var(--term-fg);">ON</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="../vendor/obs-config.js"></script>
|
||||
<script src="../vendor/obs-ws-mini.js"></script>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
|
||||
// ── UTC clock ──
|
||||
const clockEl = document.getElementById('clock');
|
||||
function tickClock() {
|
||||
const d = new Date();
|
||||
clockEl.textContent =
|
||||
`${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
||||
}
|
||||
tickClock(); setInterval(tickClock, 1000);
|
||||
|
||||
// ── Signal bar fluctuation (steady, like Game) ──
|
||||
const sigEl = document.getElementById('signal');
|
||||
setInterval(() => {
|
||||
const r = Math.random();
|
||||
const n = r < 0.06 ? 2 : r < 0.70 ? 3 : r < 0.95 ? 4 : 5;
|
||||
sigEl.textContent = '▮'.repeat(n) + '▯'.repeat(5 - n);
|
||||
}, 1500);
|
||||
|
||||
// ── Stream elapsed time (since page load / scene activation) ──
|
||||
const streamStart = Date.now();
|
||||
function tickElapsed() {
|
||||
const s = Math.floor((Date.now() - streamStart) / 1000);
|
||||
const h = Math.floor(s / 3600);
|
||||
const mm = Math.floor((s / 60) % 60);
|
||||
const ss = s % 60;
|
||||
document.getElementById('elapsed').textContent = `${pad(h)}:${pad(mm)}:${pad(ss)}`;
|
||||
}
|
||||
tickElapsed(); setInterval(tickElapsed, 1000);
|
||||
|
||||
// ── OBS WS: live frame sync for camera + screen capture ──
|
||||
// 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_LOCK = params.get('scene') || null;
|
||||
|
||||
const camEl = document.getElementById('cam');
|
||||
const scrEl = document.getElementById('scr');
|
||||
const rootStyle = document.documentElement.style;
|
||||
|
||||
// Compute rendered (w, h) on canvas from an OBS sceneItemTransform.
|
||||
// Camera item has no bounds → scale × source. Screen capture uses
|
||||
// bounds_type=stretch (1) → boundsWidth/boundsHeight are authoritative.
|
||||
function renderedSize(t) {
|
||||
const useBounds = t.boundsType
|
||||
&& t.boundsType !== 'OBS_BOUNDS_NONE'
|
||||
&& (t.boundsWidth ?? 0) > 0;
|
||||
if (useBounds) return { w: t.boundsWidth, h: t.boundsHeight };
|
||||
return {
|
||||
w: (t.sourceWidth ?? 0) * (t.scaleX ?? 1),
|
||||
h: (t.sourceHeight ?? 0) * (t.scaleY ?? 1),
|
||||
};
|
||||
}
|
||||
|
||||
// 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.
|
||||
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;
|
||||
const { w, h } = renderedSize(t);
|
||||
if (!(w > 0 && h > 0)) return;
|
||||
rootStyle.setProperty(`--${varPrefix}-x`, `${t.positionX}px`);
|
||||
rootStyle.setProperty(`--${varPrefix}-y`, `${t.positionY}px`);
|
||||
rootStyle.setProperty(`--${varPrefix}-w`, `${w}px`);
|
||||
rootStyle.setProperty(`--${varPrefix}-h`, `${h}px`);
|
||||
};
|
||||
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() {
|
||||
if (!window.__OBSWS || !window.OBSWSMini) return;
|
||||
try {
|
||||
const obs = new OBSWSMini(window.__OBSWS.url, window.__OBSWS.password);
|
||||
await obs.connect();
|
||||
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);
|
||||
});
|
||||
} catch (e) {
|
||||
// OBS WS optional — silently retry; defaults stand meanwhile.
|
||||
setTimeout(connectOBS, 2500);
|
||||
}
|
||||
}
|
||||
connectOBS();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user