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:
@@ -4,6 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<title>OPHI-118 / Music Daemon</title>
|
||||
<style>
|
||||
/* Diagnostic status page. Music is actually played by MPD (system service)
|
||||
and broadcast over OBS WS by bridges/mpd-state.py. This page no longer
|
||||
plays audio or broadcasts mpd:state — those would conflict with the
|
||||
bridge. It only listens to incoming mpd:state events so you can sanity-
|
||||
check the bridge from a single browser source. */
|
||||
html, body { margin: 0; padding: 0; background: #0a0e0a; overflow: hidden; color: #5fdc62;
|
||||
font: 14px/1.45 'DejaVu Sans Mono', 'Consolas', monospace; }
|
||||
#status { padding: 12px 14px; }
|
||||
@@ -13,66 +18,36 @@
|
||||
#status .v { color: #97f99a; word-break: break-all; }
|
||||
#status .err { color: #ff6f5a; }
|
||||
#status .ok { color: #97f99a; }
|
||||
#status .dim { color: #2c8d2f; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="status">
|
||||
<h1>OPHI MUSIC DAEMON</h1>
|
||||
<h1>OPHI MUSIC DAEMON (passive)</h1>
|
||||
<div class="row"><span class="k">role</span><span class="v dim">listener — playback handled by mpd + bridges/mpd-state.py</span></div>
|
||||
<div class="row"><span class="k">ws</span><span class="v" id="s-ws">init…</span></div>
|
||||
<div class="row"><span class="k">queue</span><span class="v" id="s-queue">—</span></div>
|
||||
<div class="row"><span class="k">now</span><span class="v" id="s-now">—</span></div>
|
||||
<div class="row"><span class="k">audio</span><span class="v" id="s-audio">—</span></div>
|
||||
<div class="row"><span class="k">queue</span><span class="v" id="s-queue">—</span></div>
|
||||
<div class="row"><span class="k">elapsed</span><span class="v" id="s-elapsed">—</span></div>
|
||||
<div class="row"><span class="k">last cmd</span><span class="v" id="s-cmd">—</span></div>
|
||||
</div>
|
||||
|
||||
<audio id="player" preload="auto"></audio>
|
||||
|
||||
@include('partials.obs-ws-scripts')
|
||||
<script src="{{ asset('data/playlist.js') }}"></script>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
const data = window.__PLAYLIST || { tracks: [] };
|
||||
const allTracks = data.tracks || [];
|
||||
const audio = document.getElementById('player');
|
||||
audio.volume = 1.0;
|
||||
const $ws = document.getElementById('s-ws');
|
||||
const $now = document.getElementById('s-now');
|
||||
const $queue = document.getElementById('s-queue');
|
||||
const $elapsed = document.getElementById('s-elapsed');
|
||||
const $cmd = document.getElementById('s-cmd');
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
const fmtClock = s => (!isFinite(s) || s < 0)
|
||||
? '--:--' : `${pad(Math.floor(s / 60))}:${pad(Math.floor(s % 60))}`;
|
||||
|
||||
function shuffle(a) {
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
const queue = shuffle([...allTracks]);
|
||||
let qIdx = 0;
|
||||
let stallTimer = null;
|
||||
let _watchT = 0, _watchAt = Date.now();
|
||||
|
||||
const $ws = document.getElementById('s-ws');
|
||||
const $q = document.getElementById('s-queue');
|
||||
const $n = document.getElementById('s-now');
|
||||
const $a = document.getElementById('s-audio');
|
||||
const $c = document.getElementById('s-cmd');
|
||||
function setWs(text, cls = '') { $ws.textContent = text; $ws.className = 'v ' + cls; }
|
||||
function refreshStatus() {
|
||||
$q.textContent = window.__PLAYLIST
|
||||
? `${allTracks.length} tracks (playlist.js loaded)`
|
||||
: 'playlist.js NOT loaded';
|
||||
const t = allTracks.length ? `#${qIdx + 1}/${allTracks.length} — ${queue[qIdx]?.title || '?'}` : '—';
|
||||
$n.textContent = t;
|
||||
let aState = 'idle';
|
||||
if (audio.paused) aState = 'paused';
|
||||
else if (audio.ended) aState = 'ended';
|
||||
else if (audio.src) aState = `playing (${audio.currentTime.toFixed(0)}s / ${(audio.duration||0).toFixed(0)}s)`;
|
||||
$a.textContent = aState;
|
||||
}
|
||||
setInterval(refreshStatus, 500);
|
||||
refreshStatus();
|
||||
|
||||
function clearStall() { clearTimeout(stallTimer); stallTimer = null; }
|
||||
function setWs(text, cls = '') { $ws.textContent = text; $ws.className = 'v ' + cls; }
|
||||
|
||||
let obs = null;
|
||||
async function connectOBS() {
|
||||
@@ -89,92 +64,24 @@
|
||||
setWs('closed — retrying in 2s', 'err');
|
||||
setTimeout(connectOBS, 2000);
|
||||
});
|
||||
obs.onCustom('mpd:cmd', (d) => handleCommand(d));
|
||||
broadcastState();
|
||||
obs.onCustom('mpd:state', (s) => {
|
||||
$now.textContent = s.title ? `${s.title}${s.artist ? ` — ${s.artist}` : ''}` : '—';
|
||||
const total = Number(s.total) || 0;
|
||||
const idx = Number.isInteger(s.index) ? s.index : -1;
|
||||
$queue.textContent = (total > 0 && idx >= 0)
|
||||
? `${idx + 1} / ${total}${s.paused ? ' (paused)' : ''}`
|
||||
: '—';
|
||||
$elapsed.textContent = `${fmtClock(s.currentTime)} / ${fmtClock(s.duration)}`;
|
||||
});
|
||||
obs.onCustom('mpd:cmd', (d) => {
|
||||
$cmd.textContent = `${d.type || '?'} @ ${new Date().toLocaleTimeString()}`;
|
||||
});
|
||||
} catch (e) {
|
||||
setWs(`connect failed (${e.message}) — retrying`, 'err');
|
||||
setTimeout(connectOBS, 2500);
|
||||
}
|
||||
}
|
||||
connectOBS();
|
||||
|
||||
function broadcastState() {
|
||||
if (!obs || !obs.identified) return;
|
||||
const t = queue[qIdx];
|
||||
obs.broadcast('mpd:state', {
|
||||
index: qIdx,
|
||||
total: queue.length,
|
||||
title: t?.title || null,
|
||||
file: t?.file || null,
|
||||
currentTime: audio.currentTime || 0,
|
||||
duration: audio.duration || t?.durationSec || 0,
|
||||
paused: audio.paused,
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function playCurrent() {
|
||||
if (queue.length === 0) return;
|
||||
qIdx = ((qIdx % queue.length) + queue.length) % queue.length;
|
||||
const t = queue[qIdx];
|
||||
clearStall();
|
||||
audio.src = t.file;
|
||||
stallTimer = setTimeout(() => {
|
||||
console.warn('[music] start timeout, skipping', t.title);
|
||||
advance(+1);
|
||||
}, 15000);
|
||||
audio.play().catch(() => {
|
||||
clearStall();
|
||||
console.warn('[music] play() rejected, skipping', t.title);
|
||||
advance(+1);
|
||||
});
|
||||
broadcastState();
|
||||
}
|
||||
function advance(direction) {
|
||||
clearStall();
|
||||
if (queue.length === 0) return;
|
||||
qIdx = (qIdx + direction + queue.length) % queue.length;
|
||||
playCurrent();
|
||||
}
|
||||
audio.addEventListener('ended', () => advance(+1));
|
||||
audio.addEventListener('error', () => { console.warn('[music] decode error'); advance(+1); });
|
||||
audio.addEventListener('playing', clearStall);
|
||||
setInterval(() => {
|
||||
if (audio.paused || audio.ended || !audio.src) {
|
||||
_watchT = audio.currentTime;
|
||||
_watchAt = Date.now();
|
||||
return;
|
||||
}
|
||||
if (Math.abs(audio.currentTime - _watchT) >= 0.05) {
|
||||
_watchT = audio.currentTime;
|
||||
_watchAt = Date.now();
|
||||
} else if (Date.now() - _watchAt > 8000) {
|
||||
console.warn('[music] mid-track stall, skipping');
|
||||
_watchT = 0; _watchAt = Date.now();
|
||||
advance(+1);
|
||||
}
|
||||
}, 1000);
|
||||
setInterval(broadcastState, 1000);
|
||||
|
||||
// Only command source now: OBS WS `mpd:cmd`. The Twitch bot and the
|
||||
// Laravel `POST /cmd/{type}` route both broadcast through this same event.
|
||||
function handleCommand(cmd) {
|
||||
const type = (cmd?.type || '').toLowerCase();
|
||||
$c.textContent = `${type} @ ${new Date().toLocaleTimeString()}`;
|
||||
switch (type) {
|
||||
case 'skip': advance(+1); break;
|
||||
case 'prev': advance(-1); break;
|
||||
case 'pause': audio.pause(); broadcastState(); break;
|
||||
case 'resume': audio.play().catch(() => {}); broadcastState(); break;
|
||||
default:
|
||||
console.warn('[music] unknown command:', type);
|
||||
}
|
||||
}
|
||||
|
||||
if (queue.length > 0) {
|
||||
playCurrent();
|
||||
} else {
|
||||
console.warn('[music] no tracks — run `php artisan rig:playlist`');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user