Remote OBS Studio control API explorer REST API · WebSocket relay · TouchOSC bridge · M3U playlist scheduler
obs-relay sits between your control surfaces and OBS Studio. It exposes a REST API and WebSocket endpoint that any device on your network can call to control OBS — switch scenes, manage video playlists, start/stop streams, control audio, and more.
The codebase is also bundled with an API explorer. Think of it as a Pluto.tv-style channel controller: define playlists of video files, name each broadcast state (Live, BRB, Standby, Intermission), and switch between them from a phone, tablet, custom controller, or automation script.
┌─────────────────────┐
REST clients ───▶│ │
WebSocket ───▶ │ obs-relay │──▶ OBS Studio (WebSocket 5.x)
TouchOSC ───▶ │ (FastAPI + asyncio)│ ↑ events flow back too
(UDP OSC) └─────────────────────┘
│
Playlist Manager
(M3U auto-scheduler)
| Feature | Details |
|---|---|
| Scene control | Switch scenes, presets, studio mode, transitions |
| Playlist scheduler | M3U playlists with auto-advance when a video ends |
| Hot-swap channels | Switch between playlists mid-show (Pluto.tv style) |
| WebSocket relay | Real-time bidirectional control + push events |
| TouchOSC / OSC | UDP bridge — build a custom hardware controller |
| Recording | Start, stop, pause, resume, status |
| Audio | Volume (dB) and mute per source |
| Transition control | Set type (Fade/Cut/etc) and duration from API |
| Preflight validation | Check all playlist files exist before going live |
| State persistence | Survives restarts — resumes playlist position |
| Auth | Optional Bearer token for REST + ?token= for WebSocket |
| Standalone build | Single executable via PyInstaller |
- Python 3.10 or higher
- OBS Studio 28+ (WebSocket 5.x is built in — no plugin needed)
- macOS, Windows, or Linux
git clone https://github.com/YOUR_ORG/obs-relay.git
cd obs-relaypip install -r requirements.txtOr manually:
pip install fastapi "uvicorn[standard]" websockets obs-websocket-py python-osc \
pydantic pydantic-settings aiofiles httpx rich typer \
python-multipart pyyaml apschedulerRecommended — use a virtual environment:
python -m venv .venv source .venv/bin/activate # macOS/Linux .venv\Scripts\activate # Windows pip install -r requirements.txt
pip install -e .If
obs-relaycommand is not found on macOS/zsh — usepython run.pyinstead. It always works without any PATH setup. Or add pip's script dir to your shell:echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
- Open OBS Studio
- Go to Tools → WebSocket Server Settings
- Enable the WebSocket server
- Set port: 4455
- Set a password (recommended)
- Click OK
Important for playlists: Create a Media Source input in your scene named
MediaSource(or whatever you set inconfig.yamlunderplaylist.source_name). obs-relay controls playlist playback by swapping files into this source.
python run.py init-configEdit config.yaml — at minimum set your OBS password:
obs:
password: "your_obs_password_here"python run.py check --password your_obs_password_hereYou should see your OBS version, scene list, and current transition.
python run.py startOr with inline flags (no config file needed):
python run.py start --obs-password mypassword --port 8080curl http://localhost:8080/health{"status": "ok", "obs_connected": true, "ws_clients": 0, "osc_active": true, "version": "1.1.0"}Interactive API docs: http://localhost:8080/docs
A playlist is a .m3u file containing a list of video paths. When you activate a playlist, obs-relay loads the first file into your OBS Media Source. When that video finishes, OBS fires a MediaInputPlaybackEnded event — obs-relay catches it and automatically loads the next file. No polling, no timers.
Drop .m3u files into the playlists/ folder. They load automatically on startup.
#EXTM3U
#EXTINF:15,Intro Bumper
/absolute/path/to/intro.mp4
#EXTINF:3600,Main Content Block
/absolute/path/to/episode-01.mp4
#EXTINF:-1,Outro (unknown duration)
/absolute/path/to/outro.mp4Always use absolute paths. OBS resolves paths from its own working directory.
#EXTINF:30,Bumper (trimmed from a longer file)
#EXTVLCOPT:start-time=120.0
#EXTVLCOPT:stop-time=150.0
/media/long-video.mp4python run.py validate-playlists✓ main: 6/6 files OK
✗ friday-show: 1 missing
→ /media/episode-03.mp4
# Check what scenes OBS has
curl http://localhost:8080/obs/scenes
# Switch to a scene immediately
curl -X POST http://localhost:8080/obs/scene/Live
# Activate a preset (scene switch + side effects)
curl -X POST http://localhost:8080/presets/brb/activate
# Load and start a playlist
curl -X POST http://localhost:8080/playlists/main/activate
# Advance to next track manually
curl -X POST http://localhost:8080/playlists/next
# Set transition to a 500ms fade
curl -X POST http://localhost:8080/obs/transition \
-H "Content-Type: application/json" \
-d '{"name": "Fade", "duration_ms": 500}'
# Start/stop streaming
curl -X POST http://localhost:8080/obs/stream/start
curl -X POST http://localhost:8080/obs/stream/stop
# Start/stop recording
curl -X POST http://localhost:8080/obs/record/start
curl -X POST http://localhost:8080/obs/record/stop
# Mute/unmute a source
curl -X POST http://localhost:8080/obs/source/Mic/mute \
-H "Content-Type: application/json" \
-d '{"muted": true}'
# Check recording status
curl http://localhost:8080/obs/record/status
# Validate all playlists
curl http://localhost:8080/playlists/validateConnect to ws://localhost:8080/ws (add ?token=YOUR_KEY if auth is enabled).
{"cmd": "activate_preset", "params": {"name": "brb"}}
{"cmd": "switch_scene", "params": {"scene_name": "Live"}}
{"cmd": "playlist_activate", "params": {"name": "main"}}
{"cmd": "playlist_next"}
{"cmd": "playlist_prev"}
{"cmd": "playlist_seek", "params": {"position": 3}}
{"cmd": "stream_start"}
{"cmd": "stream_stop"}
{"cmd": "record_start"}
{"cmd": "record_stop"}
{"cmd": "set_transition", "params": {"name": "Fade", "duration_ms": 300}}
{"cmd": "get_status"}{"event": "scene_switched", "data": {"scene": "BRB"}}
{"event": "scene_changed_external", "data": {"scene": "Live"}}
{"event": "preset_activated", "data": {"preset": "brb", "actions": [...]}}
{"event": "playlist_activated", "data": {"playlist": "main", "track": "Intro"}}
{"event": "track_changed", "data": {"track": "Content Block 2", "status": "advanced"}}
{"event": "stream_started", "data": {"status": "streaming_started"}}
{"event": "recording_started", "data": {"status": "recording_started"}}scene_changed_external fires whenever the scene is changed from the OBS UI or another client — keeps all connected panels in sync.
Presets are named broadcast states. Built-in defaults:
| Preset | OBS Scene | Side effects |
|---|---|---|
live |
Live | — |
brb |
BRB | Auto-mutes Mic |
standby |
Standby | — |
intermission |
Intermission | Activates intermission playlist |
end_card |
EndCard | — |
Configure scene names in config.yaml to match your OBS setup. Activate:
curl -X POST http://localhost:8080/presets/live/activateobs-relay listens on UDP port 9000 and sends feedback on port 9001.
TouchOSC device setup:
- TouchOSC → Connections → OSC
- Host: your server's IP
- Send port:
9000/ Receive port:9001 - In
config.yaml: setosc.client_hostto your device's IP for direct feedback
See docs/touchosc-layout.md for the full OSC address map.
# config.yaml
api:
api_key: "your-secret-token"# REST requests
curl -H "Authorization: Bearer your-secret-token" http://localhost:8080/obs/scenes
# WebSocket
wscat -c "ws://localhost:8080/ws?token=your-secret-token"Leave api_key blank for open access (fine for LAN-only use).
obs:
host: localhost
port: 4455
password: ""
reconnect_interval: 5.0
max_reconnect_attempts: 0 # 0 = infinite
api:
host: "0.0.0.0"
port: 8080
api_key: ""
cors_origins: ["*"]
log_level: info
osc:
enabled: true
listen_host: "0.0.0.0"
listen_port: 9000
reply_port: 9001
client_host: "255.255.255.255" # or specific device IP
playlist:
directory: playlists
default_playlist: "" # e.g. "main" to auto-load
loop: true
source_name: MediaSource # must match your OBS source nameAll values can be set as environment variables: OBS_PASSWORD=x API_PORT=8080 python run.py start
python run.py start Start the server
python run.py init-config Create config.yaml
python run.py check Test OBS connection
python run.py list-presets Show scene presets
python run.py validate-playlists Check all playlist files exist
python run.py build-standalone Build standalone exe (needs pyinstaller)
obs-relay: command not found
Use python run.py start — it always works. See Installation for the PATH fix.
OBS keeps reconnecting
- WebSocket must be enabled in OBS: Tools → WebSocket Server Settings
- Check the password is exact (case-sensitive)
- Run
python run.py check --password yourpasswordto isolate the issue
Playlist not auto-advancing
- The OBS Media Source must be named exactly as
playlist.source_name(default:MediaSource) - Confirm with
GET /playlists/status→auto_advanceshould betrue - Check
GET /obs/source/MediaSource/mediato see if OBS is playing the file
Videos not loading / black screen
- Use absolute paths in
.m3ufiles - Run
python run.py validate-playliststo find missing files - Confirm the file format is supported by OBS (mp4, mov, mkv, etc.)
WebSocket drops immediately with auth enabled
- Connect as:
ws://host:8080/ws?token=YOUR_API_KEY
TouchOSC not receiving feedback
- Set
osc.client_hostto your specific device IP (not broadcast) - Send
/obs/state/querywith value1.0to trigger a state broadcast
- Scheduled cue queue / show rundown
- HTTPS/TLS for internet-facing deployments
- Web control panel UI
- Multi-OBS support (backup failover)
- Log rotation to file
obs-relay/
├── run.py Direct launcher (no install needed)
├── config.yaml Default configuration
├── requirements.txt Dependencies
├── pyproject.toml Package metadata
├── playlists/ Drop .m3u files here
├── docs/
│ ├── touchosc-layout.md OSC address map + TouchOSC guide
│ └── api-reference.html Interactive API reference
├── obs_relay/
│ ├── main.py CLI + app assembly
│ ├── config/ Settings (pydantic + YAML + env)
│ ├── core/ OBS WebSocket client
│ ├── api/ FastAPI REST + WebSocket
│ ├── osc/ TouchOSC / OSC bridge
│ ├── playlist/ M3U parser + auto-advance
│ └── scenes/ Scene presets
├── scripts/
│ └── build.py PyInstaller helper
└── tests/
└── test_core.py
- Fork the repo
- Create a branch:
git checkout -b feature/my-feature - Run tests:
pytest tests/ -v - Open a pull request
MIT — see LICENSE.
Built on obs-websocket-py and FastAPI.
Part of the drop-zone-ops broadcast toolchain.