Music daemon + cross-source sync via OBS WebSocket
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.
This commit is contained in:
80
loading/convert.sh
Executable file
80
loading/convert.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# loading/convert.sh — strip video tracks out of audio files in
|
||||
# ~/.config/obs-studio/playlist/ and re-encode to AAC m4a in place.
|
||||
#
|
||||
# WHY: yt-dlp .webm downloads include 1080p video. Chromium's <audio>
|
||||
# element decodes the video stream too (just doesn't render it) — pegs
|
||||
# CPU and stalls playback mid-track. Audio-only m4a is ~95% smaller
|
||||
# and decodes ~20× faster.
|
||||
#
|
||||
# ./convert.sh — convert all video-bearing files
|
||||
# ./convert.sh --dry-run — show what would happen, do nothing
|
||||
#
|
||||
# DO NOT run this while OBS has a playlist track open (between streams
|
||||
# is fine; mid-stream you'll lock the file ffmpeg is reading from).
|
||||
#
|
||||
# After conversion, re-index: ./playlist.sh
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
OBS_DIR="$(cd "$DIR/.." && pwd)"
|
||||
AUDIO_DIR="$OBS_DIR/playlist"
|
||||
|
||||
DRY=0
|
||||
[[ "${1:-}" == "--dry-run" ]] && DRY=1
|
||||
|
||||
if [[ ! -d "$AUDIO_DIR" ]]; then
|
||||
echo " ✗ $AUDIO_DIR does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v ffprobe >/dev/null 2>&1 || ! command -v ffmpeg >/dev/null 2>&1; then
|
||||
echo " ✗ ffmpeg / ffprobe required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shopt -s nullglob nocaseglob
|
||||
|
||||
# Files that contain a video stream (and aren't already m4a)
|
||||
declare -a TODO=()
|
||||
for f in "$AUDIO_DIR"/*.{webm,mp4,mkv,mov,avi}; do
|
||||
[[ -f "$f" ]] || continue
|
||||
# Skip yt-dlp intermediate files
|
||||
case "$(basename "$f")" in
|
||||
*.f[0-9]*.*|*.temp.*|*.part) continue ;;
|
||||
esac
|
||||
# Only consider files that actually contain a video stream
|
||||
if ffprobe -v error -select_streams v -show_entries stream=codec_type \
|
||||
-of csv=p=0 "$f" 2>/dev/null | grep -q video; then
|
||||
TODO+=("$f")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#TODO[@]} -eq 0 ]]; then
|
||||
echo " ✓ nothing to convert (no video-bearing files in $AUDIO_DIR)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "── Converting ${#TODO[@]} file(s) → AAC m4a ──"
|
||||
for f in "${TODO[@]}"; do
|
||||
base="${f%.*}"
|
||||
out="$base.m4a"
|
||||
if [[ -f "$out" ]]; then
|
||||
echo " ⊘ skip (m4a exists): $(basename "$f")"
|
||||
continue
|
||||
fi
|
||||
echo " → $(basename "$f")"
|
||||
if [[ $DRY -eq 1 ]]; then
|
||||
echo " (dry-run: would write $(basename "$out") and remove the source)"
|
||||
continue
|
||||
fi
|
||||
if ffmpeg -nostdin -loglevel error -i "$f" -vn -c:a aac -b:a 192k "$out"; then
|
||||
rm -f "$f"
|
||||
echo " ✓ ${out##*/} ($(du -h "$out" | cut -f1))"
|
||||
else
|
||||
echo " ✗ ffmpeg failed; leaving $(basename "$f") in place" >&2
|
||||
rm -f "$out"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo " → next: ./playlist.sh (re-index for the new filenames)"
|
||||
Reference in New Issue
Block a user