CLAUDE.md update
This commit is contained in:
87
docs/audio.md
Normal file
87
docs/audio.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Audio pipeline, MPD, OBS WS bus, systemd units
|
||||
|
||||
**Read when:** editing `bridges/`, `twitch-bot/`, `~/.config/mpd/mpd.conf`, `~/.config/pipewire/pipewire-pulse.conf.d/*.conf`, or working with `mpd:state` events on the OBS WebSocket bus. Also read before touching audio routing in scene JSON.
|
||||
|
||||
## Audio architecture
|
||||
|
||||
Two parallel isolated streams into OBS — one for MPD music, one for Discord voice — so neither mixes with the rest of the desktop, and OBS gets independent meters / per-source volume for each.
|
||||
|
||||
```
|
||||
┌────────────┐ ┌─────────────┐
|
||||
│ MPD │ systemd user │ Discord │ Flatpak / native
|
||||
└──────┬─────┘ unit └──────┬──────┘ app
|
||||
│ PipeWire target= │ pavucontrol → "Discord-Stream"
|
||||
│ "mpd_stream" │ (one-time per rig)
|
||||
▼ ▼
|
||||
┌────────────────────────┐ ┌────────────────────────────┐
|
||||
│ null sink "mpd_stream" │ │ null sink "discord_stream" │
|
||||
└──────┬──────────┬──────┘ └──────┬─────────────┬───────┘
|
||||
│ │ │ │
|
||||
loopback│ │ monitor loopback│ │ monitor
|
||||
▼ ▼ ▼ ▼
|
||||
default OBS default OBS
|
||||
sink pulse_output_ sink pulse_output_
|
||||
capture capture
|
||||
"MPD" "Discord"
|
||||
(defined in pipewire-pulse.conf.d/
|
||||
mpd-stream.conf, discord-stream.conf)
|
||||
│
|
||||
▼
|
||||
Twitch
|
||||
```
|
||||
|
||||
The dedicated null sinks are the load-bearing piece: without them, OBS would either pick up *all* desktop audio (browser, system, every app) or *none*. The loopback modules mirror each null sink back to the user's actual default sink so MPD and Discord are still audible locally while OBS captures the isolated monitors.
|
||||
|
||||
**Routing the apps to their sinks:**
|
||||
- **MPD** binds via `target "mpd_stream"` in `~/.config/mpd/mpd.conf` — automatic.
|
||||
- **Discord** has no equivalent target setting. One-time per rig: open pavucontrol → Playback tab → while Discord plays sound, set its dropdown to **Discord-Stream**. PipeWire remembers across launches via `module-stream-restore`.
|
||||
|
||||
Sanity check after login:
|
||||
```bash
|
||||
pactl list short sinks | grep -E 'mpd_stream|discord_stream' # both should appear
|
||||
```
|
||||
If either is missing, `pipewire-pulse` didn't load the conf. Either relog or stop OBS and `systemctl --user restart pipewire-pulse.service` (do **not** restart pipewire-pulse while OBS is running — see CLAUDE.md "Critical gotcha").
|
||||
|
||||
## MPD setup
|
||||
|
||||
Config: `~/.config/mpd/mpd.conf`. Library: `~/HDD/Music`. Output target: `mpd_stream` (the null sink above). State files (`database`, `state`, `sticker.sql`, `playlists/`) live in `~/.config/mpd/`. Run via the system-provided units: `systemctl --user enable --now mpd.socket mpd.service`.
|
||||
|
||||
The `zeroconf_name` in `mpd.conf` is per-rig (`"MPD on Ignia"` / `"MPD on Midgolem"`) — `scripts/deploy-rig.sh` sets it to the local hostname when seeding the config.
|
||||
|
||||
`auto_update "yes"` means `inotify`-watch on the library — drop a file or `yt-dlp -P ~/HDD/Music/...` and MPD picks it up without `mpc update`.
|
||||
|
||||
## OBS WebSocket bus
|
||||
|
||||
Port 4455 (default). Password lives in `plugin_config/obs-websocket/config.json` (gitignored — secret). `scripts/setup.sh` extracts URL + password to `vendor/obs-config.js`, which every overlay loads as a `<script>` tag (no `fetch()` because CEF blocks it on `file://`).
|
||||
|
||||
Auth is HMAC-SHA256, handled by `vendor/obs-ws-mini.js`. The lib falls back to a pure-JS SHA-256 if `crypto.subtle` is unavailable (CEF on `file://` origins sometimes lacks it).
|
||||
|
||||
If `ss -tlnp | grep 4455` shows nothing, OBS's WS server is disabled — flip `Tools → WebSocket Server Settings → Enable` in the UI. The JSON file's `server_enabled` lags actual state until OBS shutdown, so trust the socket, not the file.
|
||||
|
||||
### Custom events on the bus
|
||||
|
||||
| Event | Publisher | Consumers |
|
||||
|---|---|---|
|
||||
| `mpd:state` | `bridges/mpd-state.py` (1 Hz + on track change) | `loading/index.html`, `game/index.html`, `desktop/index.html` (camera sync only), `goodbye/index.html`, `music-box/index.html`, `music-box/widget.html` |
|
||||
| `mpd:cmd` | `music/index.html` (legacy; reads `loading/cmd.js`) | self-handled inside the music daemon |
|
||||
|
||||
`mpd:state` payload: `{_type, index, total, title, artist, trackTitle, file, currentTime, duration, paused, nextTitle, nextArtist, nextTrackTitle, nextFile, nextTitles}`. `nextTitles` is an array of up to 3 display strings peeked from the queue starting at the current song's successor (used by the Music Box overlays to show 3-up "coming next"). The single `nextTitle`/`nextArtist`/`nextTrackTitle`/`nextFile` fields mirror `nextTitles[0]` for overlays that only need one. All `next*` fields are populated when MPD has a queued follow-up (`status.nextsong` present); they're `None`/`[]` at the end of the queue.
|
||||
|
||||
## Systemd user units
|
||||
|
||||
Live in `~/.config/systemd/user/`. Two custom + two system-provided:
|
||||
|
||||
| Unit | Source | Purpose |
|
||||
|---|---|---|
|
||||
| `obs-mpd-bridge.service` | `bridges/mpd-state.py` | MPD → OBS WS state bridge |
|
||||
| `obs-twitch-bot.service` | `twitch-bot/bot.py` | Twitch chat → MPD control |
|
||||
| `mpd.service` + `mpd.socket` | system | MPD itself (socket-activated) |
|
||||
|
||||
Common ops:
|
||||
```bash
|
||||
systemctl --user status obs-mpd-bridge obs-twitch-bot mpd
|
||||
systemctl --user restart obs-mpd-bridge
|
||||
journalctl --user -u obs-twitch-bot -f
|
||||
```
|
||||
|
||||
`scripts/deploy-rig.sh` ships the two custom units inline (heredoc) — single source of truth for unit content lives in the deploy script, not in this repo as a separate file.
|
||||
39
docs/cheatsheet.md
Normal file
39
docs/cheatsheet.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# One-liners
|
||||
|
||||
**Read when:** you want a quick command reference. These are pasted directly from typical workflows.
|
||||
|
||||
```bash
|
||||
# Refresh a browser source after editing HTML
|
||||
# In OBS: right-click the source → Refresh cache (no restart needed)
|
||||
|
||||
# Regenerate vendor/obs-config.js after WS password change
|
||||
bash scripts/setup.sh
|
||||
|
||||
# Refresh telemetry (interactive — review every field)
|
||||
bash scripts/telemetry.sh --collect
|
||||
|
||||
# Rebuild the playlist index after dropping new files into ~/HDD/Music
|
||||
bash scripts/playlist.sh
|
||||
|
||||
# Deploy this rig from scratch (run on Midgolem)
|
||||
git clone <repo> ~/.config/obs-studio && cd ~/.config/obs-studio && bash scripts/deploy-rig.sh
|
||||
|
||||
# Bridge / bot status + tail
|
||||
systemctl --user status obs-mpd-bridge obs-twitch-bot
|
||||
journalctl --user -u obs-twitch-bot -f
|
||||
|
||||
# Verify both audio sinks exist
|
||||
pactl list short sinks | grep -E 'mpd_stream|discord_stream'
|
||||
|
||||
# Check OBS WS port is listening
|
||||
ss -tlnp | grep 4455
|
||||
|
||||
# Is OBS running? (flatpak-aware — DO NOT use `pgrep -fa 'flatpak run …'`)
|
||||
flatpak ps --columns=application | grep -qx com.obsproject.Studio && echo running
|
||||
|
||||
# Inspect a scene collection
|
||||
python3 -m json.tool basic/scenes/Default_Stream_HUD.json | less
|
||||
|
||||
# List scenes in the active collection
|
||||
python3 -c "import json; d=json.load(open('basic/scenes/Default_Stream_HUD.json')); print([s['name'] for s in d['scene_order']])"
|
||||
```
|
||||
69
docs/deployment.md
Normal file
69
docs/deployment.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Deployment, per-rig state, secrets, sandbox
|
||||
|
||||
**Read when:** bringing up a fresh rig (Midgolem or otherwise), rotating tokens, deciding what to commit vs regenerate, or hitting Flatpak sandbox / hardcoded-asset issues.
|
||||
|
||||
## Deploy a fresh rig
|
||||
|
||||
```bash
|
||||
git clone <repo> ~/.config/obs-studio
|
||||
cd ~/.config/obs-studio
|
||||
bash scripts/deploy-rig.sh
|
||||
```
|
||||
|
||||
Idempotent. 9 phases: pacman + pip + flatpak + submodules + PipeWire sink + MPD + systemd units + OBS first-launch + secrets stub. See file header for details.
|
||||
|
||||
## Twitch identity & secrets
|
||||
|
||||
| Account | Purpose | Token file | Scopes |
|
||||
|---|---|---|---|
|
||||
| `ophi118` (broadcaster) | Channel title/category updates by `set-channel.py` | `twitch-bot/.env.ophi118` | `channel:manage:broadcast` |
|
||||
| `vault118` (bot) | Handles `!skip` / `!queue` / `!info`, posts now-playing, optional Mattermost going-live notifier (outbound HTTP only — no extra Twitch scopes needed) | `twitch-bot/.env` | `chat:read`, `chat:edit` |
|
||||
| `ophi118` (stream key) | RTMP stream key | `basic/profiles/ophi118/service.json` | n/a |
|
||||
|
||||
All three are gitignored. **Do not copy between rigs** — regenerate via https://twitchtokengenerator.com (for OAuth tokens) and the Twitch Dashboard (for the stream key).
|
||||
|
||||
## External assets referenced by absolute path
|
||||
|
||||
These hardcoded paths exist in `basic/scenes/Default_Stream_HUD.json`. On a fresh rig they need to exist or OBS will surface "missing source" errors:
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `/home/jin/HDD/Images/Gifs/stamd.png` | Game-scene "Starting Soon" image |
|
||||
| `/home/jin/cloud.jakubzych.com/_img/logos/dgw.png` | Doomguard watermark (Game/Landing) |
|
||||
| `/home/jin/HDD/Music/` | MPD library root |
|
||||
| `/home/jin/Videos/` | Recording output |
|
||||
| `/home/jin/.config/obs-studio/landing/static-hum.wav` | Landing audio |
|
||||
|
||||
`scripts/deploy-rig.sh` creates the directories and warns about missing image files; the user has to bring those over manually.
|
||||
|
||||
## Per-rig vs portable state
|
||||
|
||||
**Differs per rig** (don't copy from one to the other; let the deploy script or OBS itself regenerate):
|
||||
- `global.ini` — ALSA device IDs, monitor IDs
|
||||
- `~/.config/mpd/mpd.conf` — `zeroconf_name`
|
||||
- `twitch-bot/.env*` — OAuth tokens
|
||||
- `basic/profiles/ophi118/service.json` — stream key
|
||||
- `plugin_config/obs-websocket/config.json` — WebSocket password
|
||||
- `landing/telemetry.json` — `rig`, hardware specs, kernel
|
||||
- `vendor/obs-config.js` — derived from WS password
|
||||
|
||||
**Identical across rigs** (commit and reuse):
|
||||
- `basic/scenes/Default_Stream_HUD.json` — scenes + transitions + audio routing
|
||||
- `basic/profiles/ophi118/{basic,streamEncoder,recordEncoder}.json` — encoder/output settings (minus `service.json`)
|
||||
- All HTML overlays under `landing/`, `loading/`, `game/`, `desktop/`, `goodbye/`, `music-box/`, `music/`
|
||||
- All Python in `bridges/`, `twitch-bot/`
|
||||
- All scripts in `scripts/`
|
||||
- `vendor/obs-ws-mini.js`
|
||||
|
||||
**Outside this repo but written by `scripts/deploy-rig.sh` (identical across rigs):**
|
||||
- `~/.config/pipewire/pipewire-pulse.conf.d/mpd-stream.conf` — MPD null sink + loopback
|
||||
- `~/.config/pipewire/pipewire-pulse.conf.d/discord-stream.conf` — Discord null sink + loopback
|
||||
|
||||
**Manual one-time per rig (no automation):**
|
||||
- pavucontrol → Playback → Discord → set output to **Discord-Stream** (PipeWire remembers across launches; Discord has no equivalent of MPD's `target` config).
|
||||
|
||||
## Flatpak permissions — what the sandbox allows
|
||||
|
||||
OBS flatpak ships with `filesystems=host` by default — full host access. Asset paths under `/home/jin/HDD/`, `/home/jin/cloud.jakubzych.com/`, etc. work without `flatpak override`. PipeWire audio works via `xdg-run/pipewire-0`. NVIDIA acceleration works via `org.freedesktop.Platform.GL.nvidia-*` (auto-installed). If you ever need to widen permissions: `flatpak override --user com.obsproject.Studio --<flag>=<value>`.
|
||||
|
||||
`filesystems=host` is also the reason this config lives at `~/.config/obs-studio/` instead of the per-app sandboxed path — see CLAUDE.md "What this directory is".
|
||||
12
docs/history.md
Normal file
12
docs/history.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Migration history
|
||||
|
||||
**Read when:** encountering pre-migration artifacts (e.g. `linuxbrowser-source` source ids, references to native `obs` binary, browser-side music daemon code paths), reading old scene JSON, or auditing AI-written files that may target stale conventions.
|
||||
|
||||
- **Was:** native Arch `obs-studio` (no browser support) + AUR `obs-linuxbrowser-source` (id: `linuxbrowser-source`).
|
||||
- **2026-04-25:** migrated to Flatpak `com.obsproject.Studio` with bundled CEF browser source (id: `browser_source`). Settings keys: `url`, `width`, `height`, `fps` map directly; CEF requires `fps_custom: true`, `reroute_audio: false`, `restart_when_active: false`, `shutdown: false`, `webpage_control_level: 1`. Stale `linuxbrowser.reloadpage` hotkey keys dropped. Pre-migration backups have been deleted as of 2026-04-26.
|
||||
- **2026-04-26:** music architecture moved from a browser-side daemon (`music/index.html`) to MPD + a Python state bridge (`bridges/mpd-state.py`). Twitch chat bot (`twitch-bot/bot.py`) added with `!skip → MPD` integration. `station/` submodule added for the public rig-spec page. Game scene HUD added with live OBS WS sync. All `.sh` scripts consolidated under `scripts/`.
|
||||
- **2026-04-26 (later):** Music Box scene added (`music-box/index.html`, full-screen) plus sidebar widget (`music-box/widget.html`) for the Desktop scene. Desktop scene reworked Game-style — camera in right sidebar with the Music Box widget directly below. Desktop HUD forked from Game HUD into `desktop/index.html` (no game-name slot). Bridge payload extended with `nextTitle`/`nextArtist`/`nextTrackTitle`/`nextFile`. Bot gained `!queue` (next 3 tracks) and `!info` (full metadata) commands; `!skip` cooldown shifted from global 5s to per-user 60s.
|
||||
- **2026-04-27:** Music Box overlays rewritten as proper terminals (boot sequence + scrolling `[audio] now: …` history + static-HTML pinned block + ellipsis-clipped track titles). Music Box overlay gained a dedicated chat status strip between the terminal and the banner so the command help stays visible regardless of what's scrolling. Music Box widget resized to 549×880 to pair with camera width. Bridge payload gained `nextTitles[]` (array of next 3 display strings) so Music Box can show 3-up "coming next". Bot gained `live_watch()` — optional Mattermost going-live notifier driven by `MM_HOOK` env var. Landing top HUD identifier upgraded to `OPHI-118 // [<RIG>]` with cyberpunk decode/scramble reveal animation reading `window.__TEL.rig` from telemetry.
|
||||
- **2026-04-28:** Discord audio split out to its own isolated stream — `~/.config/pipewire/pipewire-pulse.conf.d/discord-stream.conf` adds a `discord_stream` null sink + loopback (mirrors the existing `mpd-stream.conf` pattern). New OBS source `Discord` (`pulse_output_capture` on `discord_stream.monitor`) added to scenes Game / Desktop / Desktop (No Cam) / Music Box. `scripts/deploy-rig.sh` phase 5 now provisions both sinks and skips the `pipewire-pulse` restart when OBS is running (a restart silently detaches OBS audio sources — fix needs an OBS process restart, not just a settings refresh). Discord routing to its sink is the only manual per-rig step (pavucontrol → Playback → Discord → Discord-Stream).
|
||||
|
||||
If you encounter `linuxbrowser-source` anywhere — in a backup, a doc, a paste — assume it's pre-migration and migrate the same way.
|
||||
49
docs/scenes-and-overlays.md
Normal file
49
docs/scenes-and-overlays.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Scene JSON, HUD overlays, OBS internals
|
||||
|
||||
**Read when:** editing `basic/scenes/Default_Stream_HUD.json`, building or modifying the HTML HUDs (`landing/`, `loading/`, `game/`, `desktop/`, `goodbye/`, `music-box/`, `music/`), debugging plugin behavior, or programmatically driving OBS.
|
||||
|
||||
## Scene collection JSON — structure
|
||||
|
||||
Top-level keys in `basic/scenes/Default_Stream_HUD.json`:
|
||||
|
||||
- `name` — collection name (matches filename stem).
|
||||
- `sources` — **flat array of every source, scene, and group**. Scenes are entries here with `"id": "scene"`; their `settings.items[]` references child sources **by name string**, not uuid. Renaming a source means updating every scene that references it.
|
||||
- `scene_order` — array of `{name}` defining sidebar order.
|
||||
- `current_scene` / `current_program_scene` — strings matching scene names.
|
||||
- `transitions`, `quick_transitions`, `current_transition`, `transition_duration` — transition setup.
|
||||
- `groups` — group definitions.
|
||||
- `modules` — per-source-plugin state.
|
||||
- `version`, `migration_resolution`, `resolution`, `canvases` — collection schema + canvas sizing. Canvas: **2560×1336**.
|
||||
|
||||
Each source entry has: `name`, `uuid`, `id`, `versioned_id`, `settings` (plugin-specific), plus filters/transform/audio fields. Scene items inside `settings.items[]` reference sources via `source_uuid`. **Use uuids when adding new items programmatically; use names when reading.**
|
||||
|
||||
Source `id` values currently in use:
|
||||
|
||||
- `scene` — a scene
|
||||
- `pipewire-screen-capture-source` — Wayland/PipeWire screen/window capture
|
||||
- `v4l2_input` — V4L2 webcam (legacy path)
|
||||
- `browser_source` — CEF browser (post-migration)
|
||||
- `image_source` — static image
|
||||
- `text_ft2_source` — FreeType2 text
|
||||
- `ffmpeg_source` — media file (audio/video) via ffmpeg
|
||||
- `pulse_output_capture` / `pulse_input_capture` — PulseAudio sink/source capture (used for `mpd_stream.monitor` and the mic)
|
||||
|
||||
When editing scene JSON: keep `uuid` values stable across edits, preserve 4-space indentation, and validate with `python3 -m json.tool` before re-launching OBS.
|
||||
|
||||
## Game / Desktop / Music Box HUDs — browser-resolution constraints
|
||||
|
||||
The Game (`HUD`), Desktop (`Desktop HUD`), and Music Box (`Music Box HUD`) browser sources all stretch (`bounds_type: 1`, bounds 2560×1336) to fill the canvas. **Their `width` / `height` settings must match the canvas (2560×1336)**, not 16:9 like the output resolution. If the browser viewport is 1920×1080, the canvas stretch is non-uniform (1.333× horizontal, 1.237× vertical), which distorts every CSS box relative to the camera's transform (camera is positioned in canvas coords directly, so a CSS-positioned frame around it looks visibly off — wider/squashed than the camera). Symptom: cyan "camera frame" doesn't align with the camera feed inside it. Fix: keep all three browser sources at `width: 2560, height: 1336` so 1 CSS px = 1 canvas px, then position any frame using canvas coords.
|
||||
|
||||
The Camera frame in both `game/index.html` and `desktop/index.html` auto-syncs via OBS WebSocket: `GetSceneItemTransform` on connect + `SceneItemTransformChanged` event listener update four CSS vars (`--cam-x/y/w/h`). Move/resize the Camera item in OBS and the frame follows live. Assumes Camera item alignment 5 (top-left) — OBS default; if you change alignment, the math in `setCameraTransform()` needs adjusting. The `?scene=` and `?camera=` URL params let either HUD target a different scene or camera source name.
|
||||
|
||||
**The Music Box widget** (`music-box/widget.html`) at 549×880 is the exception: it sits in canvas coords as a native-sized scene item (`bounds_type: 0`), NOT stretched. Authoring viewport must match scene-item dimensions; do not toggle `bounds_type` on without resizing the source's `width`/`height` to match the new bounds. The 549 width is deliberate — camera renders at 1920×1080 × 0.286 = 549×309, so the widget pairs visually as a same-width column directly under the camera.
|
||||
|
||||
## Working with scene JSON & overlays
|
||||
|
||||
- **Confirm OBS is using THIS dir, not the ghost** before assuming an edit will land. While OBS is running: `ls -la /proc/$(pgrep -x obs)/cwd` and check `lsof -p $(pgrep -x obs) | grep scenes`. Or compare mtimes after an OBS session — the file OBS rewrites is the live one.
|
||||
- **Inspecting a scene collection:** `python3 -m json.tool basic/scenes/Default_Stream_HUD.json | less`. List scenes: `python3 -c "import json; d=json.load(open('basic/scenes/Default_Stream_HUD.json')); print([s['name'] for s in d['scene_order']])"`.
|
||||
- **Diffing after an OBS session:** OBS rewrites the file even when nothing visibly changed. Expect noisy diffs; focus on `sources[]` and `scene_order` for meaningful changes.
|
||||
- **Backing up before edits:** copy the active `.json` somewhere outside this dir — OBS's own `.bak` will be overwritten on the next save.
|
||||
- **Refreshing a browser source after editing HTML:** right-click source in OBS → **Refresh cache**. No restart needed — CEF re-reads on demand.
|
||||
- **Plugin development:** Flatpak OBS plugins live inside the runtime; you can't drop `.so` files into the host filesystem and have them load. Plugin extensions for the OBS flatpak come as `com.obsproject.Studio.Plugin.<name>` flatpak extensions (search Flathub). If a plugin only has source/AUR distribution, it won't work in Flatpak — that's the tradeoff of this distribution choice.
|
||||
- **WebSocket control:** `plugin_config/obs-websocket/config.json` holds connection settings. Programmatic scene edits while OBS is running should go through obs-websocket — file edits get clobbered on save.
|
||||
Reference in New Issue
Block a user