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:
14
.gitignore
vendored
14
.gitignore
vendored
@@ -52,6 +52,20 @@ webapp/storage/data/*.manual
|
||||
/webapp/storage/framework/views/
|
||||
/webapp/bootstrap/cache/*.php
|
||||
|
||||
# archived/ — old machine-local generated files that came along when the
|
||||
# legacy scene dirs were moved. The hand-authored HTML / JSON sources are
|
||||
# tracked; only the script-generated wrappers and runtime state aren't.
|
||||
archived/scenes/landing/telemetry.js
|
||||
archived/scenes/landing/*.manual
|
||||
archived/scenes/loading/loading.json
|
||||
archived/scenes/loading/loading.js
|
||||
archived/scenes/loading/playlist.json
|
||||
archived/scenes/loading/playlist.js
|
||||
archived/scenes/loading/cmd.js
|
||||
archived/scenes/loading/*.manual
|
||||
archived/scenes/loading/playlist/
|
||||
archived/vendor/obs-config.js
|
||||
|
||||
# ============================================================
|
||||
# OBS runtime / generated state — no value in version control
|
||||
# ============================================================
|
||||
|
||||
63
CLAUDE.md
63
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Current state (read this first)
|
||||
|
||||
Active OBS Studio config dir for the **ophi118** Twitch streaming rig. It hosts: six scenes (Landing, Project Loading, Game, Desktop, Music Box, Good Bye), six custom HTML/CSS/JS overlays rendered as `browser_source`, an MPD-based audio architecture with a state bridge to OBS WebSocket, a Twitch chat bot that controls MPD via `!skip`/`!queue`/`!info`, and an optional Mattermost notifier that posts when the channel goes live. Two rigs share this codebase via a single git repo: **Ignia** (current) and **Midgolem** (deployment target).
|
||||
Active OBS Studio config dir for the **ophi118** Twitch streaming rig. It hosts: six scenes (Landing, Project Loading, Game, Desktop, Music Box, Good Bye), a Laravel webapp (`webapp/`) that serves the scene overlays over `http://127.0.0.1:1118/` under supervisord, an MPD-based audio architecture with `bridges/mpd-state.py` re-broadcasting MPD state as `mpd:state` events over OBS WebSocket, a Twitch chat bot that controls MPD via `!skip`/`!queue`/`!info`, and an optional Mattermost notifier that posts when the channel goes live. Two rigs share this codebase via a single git repo: **Ignia** (current) and **Midgolem** (deployment target).
|
||||
|
||||
## What this directory is
|
||||
|
||||
@@ -32,38 +32,59 @@ Or via the app menu ("OBS Studio"). The native `obs` binary still exists from `p
|
||||
- `plugin_manager/modules.json` — third-party plugins OBS loads.
|
||||
- `logs/`, `profiler_data/`, `.sentinel/` — runtime state.
|
||||
|
||||
**Custom overlays (browser sources rendering local HTML — safe to edit while OBS is up):**
|
||||
- `landing/` — Fallout-style "PLEASE STAND BY" overlay + telemetry + 12 s static-hum loop.
|
||||
- `loading/` — terminal-style Project Loading overlay + per-session manifest + flat playlist index + legacy `cmd.js`.
|
||||
- `game/index.html` — Game scene HUD (TRANSMISSION ID, signal, clock, camera frame, status bar). Live-syncs camera transform via OBS WS.
|
||||
- `desktop/index.html` — Desktop scene HUD, forked from Game (no game-name slot, no music widget — that lives below the camera as a separate source).
|
||||
- `goodbye/index.html` — sign-off overlay.
|
||||
- `music-box/index.html` — full-screen Music Box terminal (scrolling `[audio]` log + pinned 3-up next + chat help strip).
|
||||
- `music-box/widget.html` — compact 549×880 sidebar widget for the Desktop scene. **Authored at native pixel size** (`bounds_type: 0`, no stretch).
|
||||
- `music/index.html` — legacy "Music Daemon" browser source.
|
||||
- `vendor/obs-ws-mini.js` — minimal OBS WebSocket v5 client. `vendor/obs-config.js` is the auto-generated WS connection config (gitignored).
|
||||
**Scene overlays (`webapp/` — Laravel app served on 127.0.0.1:1118 by supervisord):**
|
||||
- `webapp/resources/views/overlays/landing.blade.php` — Fallout-style "PLEASE STAND BY" overlay + telemetry rotator.
|
||||
- `webapp/resources/views/overlays/loading.blade.php` — terminal-style Project Loading overlay + countdown.
|
||||
- `webapp/resources/views/overlays/game.blade.php` — Game HUD (camera-frame OBS-WS sync, status bar).
|
||||
- `webapp/resources/views/overlays/desktop.blade.php` — Desktop HUD (camera + screen-capture frames).
|
||||
- `webapp/resources/views/overlays/goodbye.blade.php` — sign-off terminal + "OFFLINE FOR" counter.
|
||||
- `webapp/resources/views/overlays/music/box.blade.php` — full-screen Music Box terminal.
|
||||
- `webapp/resources/views/overlays/music/widget.blade.php` — compact 549×880 widget (`bounds_type: 0`, no stretch).
|
||||
- `webapp/resources/views/overlays/music/cover.blade.php` — album-art widget (`?bars=0` for compact mode).
|
||||
- `webapp/resources/views/overlays/music/nc.blade.php` — ncurses-styled 2-pane Music Box.
|
||||
- `webapp/resources/views/overlays/music/daemon.blade.php` — passive diagnostic; playback is handled by MPD + `bridges/mpd-state.py`.
|
||||
- `webapp/public/css/hud.css` — extracted shared HUD chrome, scanlines, terminal styling, pulse keyframes.
|
||||
- `webapp/public/js/{hud,crt-static,obs-ws-bootstrap,obs-ws-mini}.js` — shared JS; `obs-config.js` is gitignored, generated by `php artisan rig:setup`.
|
||||
- Routes + controllers under `webapp/app/Http/Controllers/`. See `routes/web.php` for the URL → handler table.
|
||||
|
||||
**Artisan commands (replace the old bash helpers — see `archived/scripts/`):**
|
||||
- `php artisan rig:setup` — generate `webapp/public/js/obs-config.js` + `OBS_WS_*` env from `plugin_config/obs-websocket/config.json`.
|
||||
- `php artisan rig:telemetry [--collect]` — refresh hardware/OBS-profile snapshot at `webapp/storage/data/telemetry.json`.
|
||||
- `php artisan rig:loading` — interactive manifest builder; updates Twitch channel via Helix (no Python shell-out).
|
||||
- `php artisan rig:playlist` — index audio dirs with ffprobe → `webapp/storage/data/playlist.json`.
|
||||
- `php artisan rig:cmd <skip|prev|pause|resume>` — broadcast `mpd:cmd` over OBS WS (CLI mirror of `POST /cmd/{type}`).
|
||||
|
||||
**Audio + automation surfaces:**
|
||||
- `bridges/mpd-state.py` — MPD → OBS WS state bridge (`obs-mpd-bridge.service`).
|
||||
- `twitch-bot/bot.py` — Twitch chat bot for `!skip` / `!queue` / `!info` + optional Mattermost going-live notifier (`obs-twitch-bot.service`).
|
||||
- `twitch-bot/{search-game,set-channel,_twitch}.py` — Helix helpers used by `scripts/loading.sh`.
|
||||
- `bridges/mpd-state.py` — MPD → OBS WS state bridge (`obs-mpd-bridge.service`). Writes `bridges/cover.jpg`. Authoritative source of `mpd:state` events.
|
||||
- `twitch-bot/bot.py` — Twitch chat bot for `!skip` / `!queue` / `!info`. Talks to MPD directly, not via OBS WS.
|
||||
- `twitch-bot/{search-game,set-channel,_twitch}.py` — Helix helpers (still present; `rig:loading` no longer needs them — it uses Guzzle directly).
|
||||
- `twitch-bot/.env*` — secrets (per-rig, never copy).
|
||||
|
||||
**Scripts (`.sh` lives only here):**
|
||||
- `scripts/deploy-rig.sh` — idempotent bring-up for a fresh rig.
|
||||
- `scripts/setup.sh` — generate `vendor/obs-config.js` after a WS password change.
|
||||
- `scripts/telemetry.sh` — refresh `landing/telemetry.json` + wrap to `.js`.
|
||||
- `scripts/loading.sh` — interactive `loading.json` builder; pushes title/category to Twitch.
|
||||
- `scripts/playlist.sh` — playlist sync + legacy daemon control.
|
||||
- `scripts/deploy-rig.sh` — idempotent bring-up for a fresh rig (now also installs the supervisord program + runs `composer install`).
|
||||
- `scripts/obs-webapp.supervisord.conf` — supervisord program file; install to `/etc/supervisor.d/obs-webapp.conf`.
|
||||
- `scripts/rewrite-scene-urls.py` — one-shot tool that rewrote `basic/scenes/Default_Stream_HUD.json` from `file://` to `http://127.0.0.1:1118/...` URLs during migration. Idempotent.
|
||||
- `scripts/convert.sh`, `scripts/clean.sh` — yt-dlp `.webm` → AAC `.m4a` re-encode + audit.
|
||||
|
||||
**Reference / data:** `playlist/` (gitignored library), `profile/` (rig spec docs), `station/` (git submodule for the public rig-spec page).
|
||||
**Reference / archive:** `archived/` (pre-migration scenes + bash scripts + `vendor/`, kept for rollback — see `archived/README.md`). `playlist/` (gitignored audio library). `profile/` (rig spec docs). `station/` (submodule for the public rig-spec page).
|
||||
|
||||
## How OBS reaches the overlays — supervisord + Laravel
|
||||
|
||||
OBS browser sources point at `http://127.0.0.1:1118/<route>` (rewritten from the old `file://...` URLs by `scripts/rewrite-scene-urls.py`). The Laravel app runs under supervisord:
|
||||
|
||||
```
|
||||
sudo supervisorctl status obs-webapp # → RUNNING
|
||||
sudo supervisorctl restart obs-webapp # after route / controller / .env changes
|
||||
tail -F webapp/storage/logs/supervisord.{out,err}.log
|
||||
```
|
||||
|
||||
The program file is `scripts/obs-webapp.supervisord.conf`; the installed copy is at `/etc/supervisor.d/obs-webapp.conf` (root-owned).
|
||||
|
||||
## Critical gotcha: OBS overwrites on exit
|
||||
|
||||
OBS rewrites `basic/scenes/<collection>.json`, `global.ini`, and `user.ini` on shutdown (and periodically). **Close OBS before editing these files**, or the next OBS exit will clobber the edit. After editing, re-launch OBS to load the change.
|
||||
|
||||
**HTML overlays under `landing/`, `loading/`, `game/`, `desktop/`, `goodbye/`, `music-box/`, `music/` are safe to edit while OBS is up** — CEF re-reads them on demand. Apply the change with right-click source → **Refresh cache** in OBS, no restart needed.
|
||||
**Blade views under `webapp/resources/views/overlays/` are safe to edit while OBS is up** — CEF re-fetches on right-click source → **Refresh cache** in OBS, no restart needed. **Controller / route / `.env` changes need a supervisord restart** (`sudo supervisorctl restart obs-webapp`) — only then will the browser sources see the new behavior on next Refresh cache. CSS and JS under `webapp/public/` are pure static files; no restart needed, just Refresh cache.
|
||||
|
||||
**Detecting whether OBS is running** — the Flatpak's inner binary is just `obs`, not `flatpak run com.obsproject.Studio`. Use:
|
||||
```bash
|
||||
|
||||
52
archived/README.md
Normal file
52
archived/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# archived/
|
||||
|
||||
Pre-`webapp/` artefacts retained for reference and rollback.
|
||||
|
||||
## scenes/
|
||||
The seven hand-authored HTML scene overlays that lived at the repo root
|
||||
before the Laravel migration:
|
||||
|
||||
- `landing/` — "PLEASE STAND BY" with telemetry rotator
|
||||
- `loading/` — project-load terminal + countdown + manifest
|
||||
- `game/` — game HUD with camera-frame OBS-WS sync
|
||||
- `desktop/` — desktop HUD (camera + screen frames)
|
||||
- `goodbye/` — sign-off terminal + "OFFLINE FOR" counter
|
||||
- `music/` — legacy browser-side music daemon (HTML5 `<audio>`)
|
||||
- `music-box/` — full HUD, widget, cover, and nc variant
|
||||
|
||||
Each `index.html` is a self-contained file with inline CSS+JS. The
|
||||
corresponding Blade view lives in
|
||||
`webapp/resources/views/overlays/<scene>.blade.php` (and `music/box.blade.php`,
|
||||
`music/widget.blade.php`, `music/cover.blade.php`, `music/nc.blade.php`,
|
||||
`music/daemon.blade.php` for the music sub-tree).
|
||||
|
||||
## scripts/
|
||||
Bash helpers that wrote `window.__X` global JS files. Replaced by Artisan:
|
||||
|
||||
| Old | New |
|
||||
|---------------------------|------------------------------------|
|
||||
| `scripts/setup.sh` | `php artisan rig:setup` |
|
||||
| `scripts/loading.sh` | `php artisan rig:loading` |
|
||||
| `scripts/playlist.sh` | `php artisan rig:playlist` |
|
||||
| `scripts/telemetry.sh` | `php artisan rig:telemetry` |
|
||||
| `scripts/playlist.sh skip`| `php artisan rig:cmd skip` (or `curl -X POST http://127.0.0.1:1118/cmd/skip`) |
|
||||
|
||||
## vendor/
|
||||
- `obs-ws-mini.js` — moved to `webapp/public/js/obs-ws-mini.js`.
|
||||
- `obs-config.js` — moved to `webapp/public/js/obs-config.js` (still
|
||||
generated by `rig:setup`).
|
||||
|
||||
## Rollback
|
||||
1. Close OBS (`flatpak kill com.obsproject.Studio`).
|
||||
2. Restore the scene JSON:
|
||||
```
|
||||
cp basic/scenes/Default_Stream_HUD.json.pre-webapp \
|
||||
basic/scenes/Default_Stream_HUD.json
|
||||
```
|
||||
3. Move directories back from `archived/scenes/` to the repo root,
|
||||
`archived/vendor/` back to `vendor/`, and `archived/scripts/*.sh`
|
||||
back to `scripts/`.
|
||||
4. Re-run `bash scripts/setup.sh` to regenerate `vendor/obs-config.js`
|
||||
at the old path.
|
||||
5. Stop the webapp: `sudo supervisorctl stop obs-webapp`.
|
||||
6. Restart OBS.
|
||||
@@ -3719,7 +3719,7 @@
|
||||
"saved_projectors": [],
|
||||
"preview_locked": false,
|
||||
"scaling_enabled": false,
|
||||
"scaling_level": -14,
|
||||
"scaling_level": -5,
|
||||
"scaling_off_x": 0.0,
|
||||
"scaling_off_y": 0.0,
|
||||
"modules": {
|
||||
|
||||
@@ -47,7 +47,7 @@ RIG_NAME_TC="${RIG_NAME^}" # title-case: ignia → Ignia
|
||||
|
||||
# ── output helpers ─────────────────────────────────────────────────────────
|
||||
PHASE=0
|
||||
TOTAL=9
|
||||
TOTAL=10
|
||||
step() { PHASE=$((PHASE+1)); printf '\n\033[1;36m[%d/%d] %s\033[0m\n' "$PHASE" "$TOTAL" "$*"; }
|
||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||
skip() { printf ' \033[90m·\033[0m %s\n' "$*"; }
|
||||
@@ -95,6 +95,8 @@ PACMAN_PKGS=(
|
||||
mpd mpc
|
||||
avahi nss-mdns
|
||||
fontconfig
|
||||
php composer
|
||||
supervisor
|
||||
)
|
||||
|
||||
if command -v pacman >/dev/null; then
|
||||
@@ -558,17 +560,87 @@ PY
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "$OBS_DIR/vendor/obs-config.js" ]]; then
|
||||
ok "vendor/obs-config.js present"
|
||||
if [[ -f "$OBS_DIR/webapp/public/js/obs-config.js" ]]; then
|
||||
ok "webapp/public/js/obs-config.js present"
|
||||
else
|
||||
if would "bash scripts/setup.sh"; then
|
||||
bash "$DIR/setup.sh" || warn "setup.sh failed — re-run after OBS WS port is reachable"
|
||||
if would "php artisan rig:setup"; then
|
||||
( cd "$OBS_DIR/webapp" && php artisan rig:setup ) \
|
||||
|| warn "rig:setup failed — re-run after OBS WS port is reachable"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Phase 9 — secrets stub + final report
|
||||
# Phase 9 — webapp (Laravel) + supervisord
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
step "Webapp (Laravel) + supervisord program"
|
||||
|
||||
if [[ -d "$OBS_DIR/webapp" ]]; then
|
||||
if [[ -d "$OBS_DIR/webapp/vendor" ]] && [[ -f "$OBS_DIR/webapp/vendor/autoload.php" ]]; then
|
||||
ok "webapp/vendor present (composer install already ran)"
|
||||
else
|
||||
if would "composer install in webapp/"; then
|
||||
( cd "$OBS_DIR/webapp" && composer install --no-dev --optimize-autoloader ) \
|
||||
|| warn "composer install failed — re-run manually if php/composer were just installed"
|
||||
ok "composer install complete"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "$OBS_DIR/webapp/.env" ]]; then
|
||||
ok "webapp/.env present"
|
||||
elif [[ -f "$OBS_DIR/webapp/.env.example" ]]; then
|
||||
if would "cp .env.example .env + php artisan key:generate"; then
|
||||
cp "$OBS_DIR/webapp/.env.example" "$OBS_DIR/webapp/.env"
|
||||
( cd "$OBS_DIR/webapp" && php artisan key:generate ) >/dev/null
|
||||
ok "seeded webapp/.env from .env.example"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f /etc/supervisor.d/obs-webapp.conf ]]; then
|
||||
ok "/etc/supervisor.d/obs-webapp.conf already installed"
|
||||
else
|
||||
if would "sudo install scripts/obs-webapp.supervisord.conf → /etc/supervisor.d/"; then
|
||||
sudo install -m 644 "$DIR/obs-webapp.supervisord.conf" /etc/supervisor.d/obs-webapp.conf
|
||||
ok "installed /etc/supervisor.d/obs-webapp.conf"
|
||||
fi
|
||||
fi
|
||||
|
||||
if systemctl is-enabled supervisord >/dev/null 2>&1; then
|
||||
ok "supervisord enabled"
|
||||
else
|
||||
if would "sudo systemctl enable --now supervisord"; then
|
||||
sudo systemctl enable --now supervisord
|
||||
ok "enabled supervisord"
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( ! CHECK_ONLY )); then
|
||||
sudo supervisorctl reread >/dev/null 2>&1 || true
|
||||
sudo supervisorctl update >/dev/null 2>&1 || true
|
||||
if sudo supervisorctl status obs-webapp 2>/dev/null | grep -q RUNNING; then
|
||||
ok "obs-webapp program RUNNING"
|
||||
else
|
||||
if would "sudo supervisorctl start obs-webapp"; then
|
||||
sudo supervisorctl start obs-webapp 2>&1 | sed 's/^/ /' || \
|
||||
warn "obs-webapp didn't start — check storage/logs/supervisord.err.log"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( ! CHECK_ONLY )); then
|
||||
sleep 1
|
||||
if curl -fsS -o /dev/null http://127.0.0.1:1118/landing; then
|
||||
ok "http://127.0.0.1:1118/landing responding"
|
||||
else
|
||||
warn "127.0.0.1:1118/landing not reachable yet — check obs-webapp status"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "webapp/ missing — clone the repo properly first"
|
||||
fi
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Phase 10 — secrets stub + final report
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
step "Secrets stub + final report"
|
||||
|
||||
@@ -590,9 +662,9 @@ stub_env "$OBS_DIR/twitch-bot/.env.ophi118"
|
||||
|
||||
# Refresh telemetry so the rig name is correct on this machine.
|
||||
if (( ! CHECK_ONLY )); then
|
||||
if would "scripts/telemetry.sh --collect --no-review (rig snapshot)"; then
|
||||
bash "$DIR/telemetry.sh" --collect --no-review || \
|
||||
warn "telemetry.sh --collect failed — re-run interactively to review"
|
||||
if would "php artisan rig:telemetry --collect (rig snapshot)"; then
|
||||
( cd "$OBS_DIR/webapp" && php artisan rig:telemetry --collect ) || \
|
||||
warn "rig:telemetry failed — re-run interactively to review"
|
||||
ok "telemetry refreshed (rig=$RIG_NAME_TC)"
|
||||
fi
|
||||
fi
|
||||
@@ -620,7 +692,7 @@ cat <<DONE
|
||||
systemctl --user restart obs-twitch-bot.service
|
||||
|
||||
5. Review telemetry interactively:
|
||||
bash scripts/telemetry.sh --collect
|
||||
cd webapp && php artisan rig:telemetry --collect
|
||||
|
||||
6. Launch OBS:
|
||||
flatpak run com.obsproject.Studio
|
||||
@@ -633,6 +705,8 @@ cat <<DONE
|
||||
Sanity checks:
|
||||
pactl list short sinks | grep -E 'mpd_stream|discord_stream'
|
||||
systemctl --user status mpd obs-mpd-bridge obs-twitch-bot
|
||||
sudo supervisorctl status obs-webapp
|
||||
curl -fsS http://127.0.0.1:1118/landing | head -1
|
||||
mpc status
|
||||
ss -tlnp | grep 4455
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -54,12 +54,17 @@
|
||||
z-index: 1;
|
||||
}
|
||||
.pane.terminal {
|
||||
/* Override hud.css `.terminal { overflow: hidden }` so the `┤ TERMINAL ├`
|
||||
pane-tab can stick out 14px above the pane border. The scrolling
|
||||
clip still happens inside #termOutput. */
|
||||
overflow: visible;
|
||||
padding: 28px 36px;
|
||||
font-size: 24px;
|
||||
line-height: 1.55;
|
||||
color: var(--term-fg);
|
||||
text-shadow: 0 0 6px var(--term-glow);
|
||||
}
|
||||
.pane.np-pane { overflow: visible; }
|
||||
.np-pane {
|
||||
padding: 24px 22px 20px;
|
||||
font-size: 18px;
|
||||
|
||||
Reference in New Issue
Block a user