OPHI-118
OBS Studio rig for the ophi118 Twitch channel.
This repo is ~/.config/obs-studio for two physical machines, Ignia and
Midgolem. It holds the OBS scene collection, profile, plugin config, the
Laravel webapp that renders all on-stream overlays, the MPD-backed audio
stack with its OBS-WebSocket bridge, and the Twitch chat bot that drives
playback from chat.
┌──────────────┐ mpd:state ┌──────────────────────────┐
│ MPD │ ──────────────► │ bridges/mpd-state.py │
│ (PipeWire │ │ (systemd --user) │
│ null-sink) │ ◄────────────── │ │
└──────────────┘ mpc next └──────────────┬───────────┘
▲ │ OBS WS CustomEvent
│ ▼
│ ┌──────────────────────┐
┌───────┴───────┐ │ obs-websocket :4455 │
│ twitch-bot.py │◄────►│ (Studio plugin) │
│ (!skip/!info) │ └──────────┬───────────┘
└───────────────┘ │ on(mpd:state)
▼
┌───────────────────────────┐
│ webapp/ (Laravel) │
│ http://127.0.0.1:1118/ │
│ ┌─────────────────────┐ │
│ │ /landing /loading │ │
│ │ /game /desktop │ │
│ │ /goodbye │ │
│ │ /music-box[/...] │ │
│ │ /music-daemon │ │
│ │ POST /cmd/{type} │ │
│ └─────────────────────┘ │
└───────────────────────────┘
▲
│ browser_source
│
┌────────┴────────┐
│ OBS Studio │
│ (Flatpak) │
└─────────────────┘
Stack
- OBS Studio (Flatpak,
com.obsproject.Studio) withfilesystems=hostpermission so~/.config/obs-studio/is read directly, not indirected through the per-app private dir. - Laravel 13 webapp under
webapp/, served byphp artisan serveon127.0.0.1:1118and managed by supervisord. Routes return Blade-rendered overlay pages and a small data API (/data/playlist.js,/cover.jpg,/track?p=…,POST /cmd/{skip|prev|pause|resume}). - MPD (system service) is the audio player. A small Python bridge
(
bridges/mpd-state.py,obs-mpd-bridge.service) listens to MPD and re-broadcasts state asmpd:stateCustomEvents on the OBS WS bus. - Twitch chat bot (
twitch-bot/,obs-twitch-bot.service) handles!skip/!queue/!infoby talking to MPD directly.
Layout
.
├── basic/ OBS scene collection + profile
├── plugin_config/ obs-websocket password (gitignored)
├── plugin_manager/ third-party plugin manifest
├── bridges/ MPD → OBS WS state bridge + cover.jpg cache
├── twitch-bot/ chat bot (git submodule)
├── station/ public rig-spec page (git submodule)
├── webapp/ Laravel app — every overlay served from here
│ ├── app/
│ │ ├── Console/Commands/ rig:setup, rig:telemetry, rig:loading,
│ │ │ rig:playlist, rig:cmd
│ │ ├── Http/Controllers/ Overlays/{Landing,Loading,Game,…}, Data,
│ │ │ MusicCommand, Audio
│ │ ├── Services/ ObsWsClient, TwitchHelix, HardwareSnapshot
│ │ └── Support/RigData.php
│ ├── resources/views/ Blade layouts + partials + per-scene overlays
│ ├── public/ css/hud.css, js/{hud,crt-static,obs-ws-*}.js,
│ │ audio/static-hum.wav
│ └── storage/data/ rig:* commands write JSON here
├── scripts/
│ ├── deploy-rig.sh idempotent bring-up (run on fresh rig)
│ ├── obs-webapp.supervisord.conf → /etc/supervisor.d/obs-webapp.conf
│ ├── rewrite-scene-urls.py one-shot migration helper
│ ├── convert.sh, clean.sh yt-dlp .webm → .m4a re-encode tools
├── archived/ pre-webapp artifacts (rollback only)
├── docs/ deeper context — audio, scenes, deployment, history
├── CLAUDE.md agent-facing repo guide (orientation for AI tools)
└── README.md you are here
Daily operation
# server health
sudo supervisorctl status obs-webapp
tail -F webapp/storage/logs/supervisord.{out,err}.log
# restart after route / controller / .env changes
sudo supervisorctl restart obs-webapp
# blade-view-only changes: no restart needed — just right-click the source
# in OBS → Refresh cache
# refresh a per-session manifest before going live (game / mode / countdown)
cd webapp && php artisan rig:loading
# reindex the music library (after dropping new tracks into ~/HDD/Music)
cd webapp && php artisan rig:playlist
# re-snapshot hardware/OBS profile telemetry (kernel/GPU/encoder changes)
cd webapp && php artisan rig:telemetry --collect
# CLI control of music playback (mirrors the Twitch chat commands)
cd webapp && php artisan rig:cmd skip
# or
curl -X POST http://127.0.0.1:1118/cmd/skip
# rotate the obs-websocket password (or after a fresh rig install)
cd webapp && php artisan rig:setup
sudo supervisorctl restart obs-webapp
The OBS scene browser sources point at http://127.0.0.1:1118/<route>, so
they always pick up the current state of the webapp on next Refresh cache.
Fresh rig (Midgolem)
git clone --recurse-submodules <repo> ~/.config/obs-studio
cd ~/.config/obs-studio
bash scripts/deploy-rig.sh
deploy-rig.sh is idempotent. It installs the Arch packages it needs
(pacman -S, AUR helper for the Sansation font), seeds PipeWire null sinks
for the MPD and Discord audio paths, writes the systemd user units for the
bridge + bot, installs the supervisord program, runs composer install for
the webapp, and calls php artisan rig:setup / rig:telemetry --collect to
materialize per-rig state. Then fill in the Twitch tokens it leaves you
prompts for and you're done.
Run it again any time after a git pull — it'll only act on what's drifted.
Critical gotchas
- Close OBS before editing
basic/scenes/*.json,global.ini, oruser.ini. OBS rewrites them on exit and will clobber your edits. - Never restart
pipewire-pulse.servicewhile OBS is running. OBS does not auto-rebindpulse_output_capturesources when sinks reload; meters go flat and stream audio cuts. Stop OBS first, or load modules ad-hoc withpactl. - OBS is the Flatpak (
com.obsproject.Studio), not the pacmanobsbinary. Detect withflatpak ps --columns=application | grep -qx com.obsproject.Studio—pgrep -fa 'flatpak run …'lies (the wrapper exits, the sandbox keeps running). - Secrets are per-rig, never copy between rigs:
twitch-bot/.env*,plugin_config/obs-websocket/config.json,basic/profiles/*/service.json,webapp/.env. Everything else is portable.
Deeper reading
- docs/audio.md — PipeWire null-sink architecture, MPD config,
the
mpd:stateevent payload, systemd user units. - docs/scenes-and-overlays.md — scene JSON internals, source ids, HUD browser-resolution constraints, camera-frame OBS WS sync.
- docs/deployment.md — Twitch identity + token scopes, per-rig vs portable state, Flatpak permission story.
- docs/history.md — migration history: linuxbrowser → CEF, browser-daemon → MPD bridge, Discord audio split, file:// → HTTP webapp.
- docs/cheatsheet.md — one-liners.
- archived/README.md — pre-webapp scene HTML / bash helpers, kept around for rollback.
License
Personal config; nothing here is a published product. The Blade templates, CSS, and scripts are MIT-style — copy what's useful.