Music persistence across scene switches by extracting playback into a
separate browser source (music/index.html) — single source instance
shared across scenes via OBS's "Add Existing", so it doesn't restart on
scene change.
Loading and Game overlays subscribe to mpd:state CustomEvents over OBS
WebSocket; the daemon broadcasts at 1 Hz plus on every track change.
File-based command pipeline lets bash control playback without a
WebSocket client of its own:
- vendor/obs-ws-mini.js — minimal WS v5 client with HMAC-SHA256 auth,
hard timeout, close-rejection, and a pure-JS SHA-256 fallback for
CEF file:// origins where crypto.subtle is unavailable.
- setup.sh — generates vendor/obs-config.js from the OBS plugin config;
checks the live socket instead of the (lagging) config file.
- music/index.html — invisible audio worker. Plays through a shuffled
queue with hardened error/stall recovery, polls loading/cmd.js for
bash-issued commands, broadcasts mpd:state via WS. Includes a
diagnostic status block (visible if the source's eye-icon is on).
- loading/index.html — strips local audio playback; subscribes to
mpd:state to drive the now-playing terminal line live. Removes the
redundant trailing prompt; terminal lines anchor at the bottom.
- loading/playlist.sh — rebuilt as subcommand API:
sync (default), skip, prev, pause, resume, status
control commands write loading/cmd.js (id, type, ts), which the
daemon polls every 250ms.
- loading/convert.sh — strips video tracks from .webm music downloads
to audio-only m4a (~95% smaller, ~20× faster decode); fixes the
CPU pegging from CEF decoding 1080p VP9 video for music playback.
- .gitignore — track manifests, ignore generated wrappers + secrets +
the audio cache.
120 lines
3.2 KiB
Bash
Executable File
120 lines
3.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Interactive manifest builder for the Project Loading scene.
|
|
#
|
|
# ./loading.sh
|
|
#
|
|
# Walks you through 5 prompts, writes loading.json + loading.js, then
|
|
# refresh the Project Loading browser source in OBS. Re-running uses
|
|
# previous answers as defaults — press Enter to keep them.
|
|
set -euo pipefail
|
|
|
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
JSON="$DIR/loading.json"
|
|
JS="$DIR/loading.js"
|
|
|
|
# Reads a top-level field from the previous loading.json. Always exits 0;
|
|
# prints empty string if file/field is missing.
|
|
prev() {
|
|
[[ -f "$JSON" ]] || return 0
|
|
python3 - "$JSON" "$1" <<'PY' 2>/dev/null || true
|
|
import json, sys
|
|
try:
|
|
d = json.load(open(sys.argv[1]))
|
|
v = d.get(sys.argv[2])
|
|
if v is None: print('')
|
|
elif isinstance(v, bool): print('y' if v else 'n')
|
|
else: print(v)
|
|
except Exception:
|
|
pass
|
|
PY
|
|
}
|
|
|
|
ask() {
|
|
# ask "question" "default" → echoes user input or default if blank
|
|
local q="$1" def="${2:-}"
|
|
local prompt=" $q"
|
|
[[ -n "$def" ]] && prompt+=" [$def]"
|
|
prompt+=": "
|
|
local v
|
|
read -rp "$prompt" v
|
|
[[ -z "$v" && -n "$def" ]] && v="$def"
|
|
printf '%s' "$v"
|
|
}
|
|
|
|
ask_yn() {
|
|
local v; v=$(ask "$1" "${2:-y}")
|
|
case "${v,,}" in
|
|
y|yes|on|true|1) printf 'true' ;;
|
|
n|no|off|false|0) printf 'false' ;;
|
|
*) printf 'true' ;;
|
|
esac
|
|
}
|
|
|
|
ask_int() {
|
|
local v
|
|
while :; do
|
|
v=$(ask "$1" "${2:-}")
|
|
[[ "$v" =~ ^[0-9]+$ ]] && { printf '%s' "$v"; return; }
|
|
echo " ✗ must be a whole number" >&2
|
|
done
|
|
}
|
|
|
|
sanitize() {
|
|
printf '%s' "${1:-}" \
|
|
| tr -d '"\\' \
|
|
| tr '\n\r\t' ' ' \
|
|
| sed 's/ */ /g; s/^ //; s/ $//'
|
|
}
|
|
emit_str() { [[ -z "${1:-}" ]] && printf 'null' || printf '"%s"' "$1"; }
|
|
|
|
# ── Pull previous answers as defaults ───────────────
|
|
prev_game=$(prev game)
|
|
prev_subtitle=$(prev subtitle)
|
|
prev_count=$(prev countdownMin); [[ -z "$prev_count" ]] && prev_count="5"
|
|
prev_camera=$(prev camera); [[ -z "$prev_camera" ]] && prev_camera="y"
|
|
prev_mic=$(prev microphone); [[ -z "$prev_mic" ]] && prev_mic="y"
|
|
|
|
echo "── Project Loading manifest ──"
|
|
echo " (press Enter to keep [defaults])"
|
|
echo
|
|
|
|
game=$(ask "Game" "$prev_game")
|
|
while [[ -z "$game" ]]; do
|
|
echo " ✗ Game is required" >&2
|
|
game=$(ask "Game" "")
|
|
done
|
|
|
|
subtitle=$(ask "Subtitle (mode / episode / note)" "$prev_subtitle")
|
|
countdown=$(ask_int "Countdown (minutes)" "$prev_count")
|
|
camera=$(ask_yn "Camera" "$prev_camera")
|
|
mic=$(ask_yn "Microphone" "$prev_mic")
|
|
|
|
game=$(sanitize "$game")
|
|
subtitle=$(sanitize "$subtitle")
|
|
|
|
now_iso=$(date -u +%FT%TZ)
|
|
tmp="$JSON.tmp.$$"
|
|
cat > "$tmp" <<JSON
|
|
{
|
|
"compiledAt": "$now_iso",
|
|
"game": $(emit_str "$game"),
|
|
"subtitle": $(emit_str "$subtitle"),
|
|
"countdownMin": $countdown,
|
|
"camera": $camera,
|
|
"microphone": $mic
|
|
}
|
|
JSON
|
|
|
|
if ! python3 -m json.tool "$tmp" >/dev/null 2>&1; then
|
|
echo " ✗ produced invalid JSON, leaving $tmp for inspection" >&2
|
|
exit 1
|
|
fi
|
|
mv -f "$tmp" "$JSON"
|
|
{ printf 'window.__LOADING = '; cat "$JSON"; printf ';\n'; } > "$JS.tmp.$$"
|
|
mv -f "$JS.tmp.$$" "$JS"
|
|
|
|
echo
|
|
echo " ✓ wrote $JSON"
|
|
echo " ✓ wrote $JS"
|
|
echo " → refresh the Project Loading browser source in OBS."
|