obs-relay Getting Started
Install the server, connect OBS Studio, verify your first scene switch, configure playlists and overlays, and diagnose common setup errors.
How It Works
obs-relay sits between your content — playlists, scripts, custom dashboards — and OBS Studio. It speaks OBS WebSocket 5.x internally and exposes a clean HTTP + WebSocket API that's easy to call from anything.
Your App / GUI / Script
↓ HTTP / WebSocket
obs-relay (Python, port 8080)
↓ OBS WebSocket 5.x (port 4455)
OBS Studio
The GUI prototyper (obs_relay_gui.html) is just a browser page that calls this same API — every button you click generates the equivalent cURL command, so you can see exactly what to put in your own code.
Port 4455 — OBS's built-in WebSocket server. obs-relay calls this internally. You never touch it directly.
Install & Start the Server
-
1Clone or unzip the project
cd obs-relay-v2
-
2Install dependencies
pip install -r requirements.txt
Python 3.10+ requiredCheck withpython3 --version. Thematchstatement used in the server requires 3.10 minimum. -
3Copy and edit config
cp config.yaml.example config.yaml
Open
config.yamland set your OBS WebSocket password:obs: host: localhost port: 4455 password: "your-obs-password-here"
-
4Test OBS connectivity before starting
python run.py check --password your-obs-password
You should see your OBS version and scene list. If this fails, fix it before starting the server — see Section 3.
-
5Start the server
python run.py start
You should see this startup output:
✓ OBS localhost:4455 (connected) ✓ API http://0.0.0.0:8080 ✓ WS ws://0.0.0.0:8080/ws ✓ Docs http://localhost:8080/docs
If OBS shows "pending reconnect"The server still starts — it will reconnect automatically when OBS is available. But all/obs/*routes will return 503 until it connects.
OBS Setup (Required)
obs-relay controls OBS — it does not create anything inside OBS for you. You need to set up matching names in OBS first.
3a — Enable OBS WebSocket
-
1Open OBS → Tools → WebSocket Server SettingsMake sure "Enable WebSocket server" is checked. Server port should be 4455.
-
2Set a password (recommended)Check "Enable Authentication" and set a password. Copy this to
config.yaml→obs.password.
3b — Create required scenes
The built-in presets expect these exact scene names in OBS. Create them in your OBS Scenes panel:
| OBS Scene Name | Preset | Purpose |
|---|---|---|
Live | live | Main broadcast content |
BRB | brb | Be Right Back screen |
Standby | standby | Holding / pre-show screen |
Intermission | intermission | Break content loop |
EndCard | end_card | Post-show slate |
brb (lowercase) but the preset expects BRB, you'll get an error. Either rename the scene in OBS, or update the preset config in config.yaml under scenes.presets.
You don't need all five scenes to start. Create just Live first and verify that works. Add the others as needed.
3c — Create a Media Source (for playlists)
If you want to use playlists to play video files through OBS:
-
1In OBS, open your Live scene → Sources → Add → Media SourceName it exactly
MediaSource(or changeplaylist.source_namein config.yaml to match whatever you name it) -
2Uncheck "Local File" for now — obs-relay will set the file path at runtimeLeave Loop unchecked. obs-relay manages advancement via the
MediaInputPlaybackEndedevent.
3d — Create a Text Source (for overlays)
For title card overlays — only needed if you want to use the overlay system:
-
1Sources → Add → Text (GDI+) on Windows, Text (FreeType 2) on Mac/LinuxName it exactly
TitleOverlay -
2Click the eye icon to hide it — it must start hiddenobs-relay shows/hides it via the visibility toggle. If it starts visible, overlays won't "appear" — they'll already be on screen.
-
3Style it however you want — font, size, color, position, backgroundobs-relay only sets the text content and visibility. Layout is entirely yours.
Your First Test
With the server running and OBS open, verify things work in this exact order:
Test 1 — Is the server reachable?
curl http://localhost:8080/health
Expected response:
{
"status": "ok",
"obs_connected": true,
"ws_clients": 0,
"version": "1.2.0"
}
python run.py start.
Test 2 — Is OBS connected?
Check "obs_connected" in the health response. If it's false:
python run.py check
This tells you exactly what's wrong — wrong password, wrong port, OBS not open, etc.
Test 3 — List your scenes
curl http://localhost:8080/obs/scenes
You should see your actual OBS scenes listed. Note the exact names — they're case-sensitive. Any preset you call must match a name in this list.
Test 4 — Switch to a scene directly (no preset)
Replace YourSceneName with one of the names from Test 3:
curl -X POST http://localhost:8080/obs/scene/YourSceneName
If this works but presets don't, the issue is a scene name mismatch — your preset's scene_name doesn't match what OBS has.
Test 5 — Try a preset
Only after scene switching works directly:
curl -X POST http://localhost:8080/presets/live/activate
Expected response includes "preset": "live" and a list of actions taken.
Diagnose Errors
Click any error message to expand the cause and fix.
/playlists/auto-advance not /playlists/auto_advance/obs/scene/Live works. /obs/scene?name=Live does not.Quick check: open http://localhost:8080/docs in your browser. FastAPI auto-generates a full interactive endpoint list. If your path isn't there, it doesn't exist.
config.yaml. Check obs.password matches OBS → Tools → WebSocket Server Settings exactly.# Run the connection check tool python run.py check --password your-password # Or test manually curl http://localhost:8080/health
live, brb, standby, intermission, end_card. All lowercase.# See all registered presets
curl http://localhost:8080/presets
scene_name doesn't match any scene in OBS. The live preset tries to switch to a scene called "Live" — capital L. If your OBS scene is called "live" or "LIVE", it will fail silently.# Check exact scene names in OBS curl http://localhost:8080/obs/scenes # Then switch directly to verify the name works curl -X POST http://localhost:8080/obs/scene/ExactNameHere
If the direct switch works but the preset doesn't, edit the preset's scene_name in config.yaml under scenes.presets to match.
python run.py start and check the startup log for errors.playlists/ directory doesn't exist. Create it: mkdir playlistsMediaSource. Check the source name in OBS and set playlist.source_name in config.yaml to match.python run.py validate-playlists to check every path.# Check file paths in your playlists python run.py validate-playlists # Check media source status curl http://localhost:8080/obs/source/MediaSource/media
TitleOverlay exists in OBS. See Section 3d to create it.overlay.scene_name in config.yaml to the exact scene name that has it.TitleOverlay ≠ titleoverlay. Must match exactly.# Check overlay status curl http://localhost:8080/overlay/status # Manually trigger with explicit text to test curl -X POST http://localhost:8080/overlay/trigger \ -H "Content-Type: application/json" \ -d '{"text": "TEST OVERLAY", "hold_sec": 30, "delay_sec": 0}'
If the status shows "active": true and text is set but nothing appears in OBS, the source name or scene name is wrong.
api_key in config.yaml but aren't sending it in requests.# Add to all REST requests: curl -H "Authorization: Bearer YOUR_KEY" \ http://localhost:8080/obs/scenes # WebSocket with auth: ws://localhost:8080/ws?token=YOUR_KEY
In the GUI prototyper: enter your key in the "API key" field in the sidebar before clicking Connect.
Using the GUI Prototyper
Open obs_relay_gui.html directly in your browser — no web server needed.
-
1Set the server URL in the sidebarDefault is
http://localhost:8080. If obs-relay is on another machine, enter its IP address. If you set an API key, enter it in the "API key" field. -
2Click ConnectThis tests the HTTP connection and auto-connects the WebSocket. You'll see green dots when both are live.
-
3Every button generates a cURL commandThe right panel shows the cURL equivalent of every action you take. Use this to build your own integration — copy the command and paste it into your script, Postman collection, or documentation.
-
4Watch the WebSocket panelGo to the WebSocket Relay section. Click "Connect WS". Every OBS event — scene changes, track advances, overlay triggers — shows up here in real time, including changes you make directly in the OBS UI.
-
5Run a Demo WorkflowGo to Demo Workflows and run "Broadcast Show Launch". It will walk through a full startup sequence and show you exactly which calls succeed and which fail, so you can see what still needs to be set up in OBS.
Setting Up Playlists
Playlists are standard .m3u files in the playlists/ folder. Each file becomes a named playlist. obs-relay loads all .m3u files on startup.
Minimal playlist
#EXTM3U #EXTINF:3600,Episode 01 /absolute/path/to/episode01.mp4 #EXTINF:1800,Episode 02 /absolute/path/to/episode02.mp4
C:\Media\episode01.mp4. On Mac/Linux: /Users/you/Media/episode01.mp4
Check your paths before going live
python run.py validate-playlists
This checks every file path in every playlist and tells you what's missing.
Playlist with overlay tags
#EXTM3U #EXTINF:3600,Episode 01 — The Pilot #EXTOVERLAY:text=Now Playing: Episode One ← custom text #EXTOVERLAY:hold=10 ← 10 seconds visible /media/ep01.mp4 #EXTINF:30,Station ID #EXTOVERLAY:skip=1 ← no overlay for this track /media/bumper.mp4 #EXTINF:3600,Episode 02 — The Return #EXTOVERLAY:delay=5 ← wait 5s before showing /media/ep02.mp4
Key playlist endpoints
# Load and start a playlist curl -X POST http://localhost:8080/playlists/main/activate # Next track curl -X POST http://localhost:8080/playlists/next # Jump to track 3 (0-indexed) curl -X POST http://localhost:8080/playlists/seek/3 # Check current status curl http://localhost:8080/playlists/status
Text Overlays
The overlay system shows timed title cards driven by playlist track changes. When a track starts, obs-relay reads the M3U metadata and automatically shows/hides the TitleOverlay source on the configured timing.
How the timing works
Track starts playing ↓ [delay_sec — default: 1s] Overlay text is set + source becomes visible ↓ [hold_sec — default: 8s] Source becomes hidden again ↓ Overlay done. Stays hidden until next track.
Testing overlays
# Trigger a 30-second test overlay with no delay curl -X POST http://localhost:8080/overlay/trigger \ -H "Content-Type: application/json" \ -d '{"text": "TEST — Is This Working?", "hold_sec": 30, "delay_sec": 0}' # Check if it's active curl http://localhost:8080/overlay/status # Hide it curl -X POST http://localhost:8080/overlay/hide
Changing timing at runtime
# Set global defaults: 12s hold, 2s delay, "Now Playing: " prefix
curl -X POST http://localhost:8080/overlay/config \
-H "Content-Type: application/json" \
-d '{"hold_sec": 12, "delay_sec": 2, "prefix": "Now Playing: "}'
Re-show current track without losing position
# Safe to call anytime — reads current PlaylistItem metadata
curl -X POST http://localhost:8080/overlay/trigger-current
PlaylistItem.metadata at the moment a track fires, not at startup. This means no matter how many times you call next/prev/seek, the metadata for each track is always correct. Advancing the playlist never desynchronizes the overlay.
TitleOverlay source in OBS → Transition Override → set a Fade with your desired duration. OBS will then animate it automatically on every show/hide.
Endpoint Quick Reference
All endpoints are prefixed with your server address, e.g. http://localhost:8080. Interactive docs always available at /docs.
{"name":"Fade","duration_ms":500}?position=3 to seek){"enabled": true}{"text":"…","hold_sec":8}{"volume_db": -6}{"muted": true}ws://host:8080/wscURL Cheat Sheet
Every example below works as-is. Replace localhost:8080 with your server address.
Basic pattern
# GET request curl http://localhost:8080/health # POST with no body curl -X POST http://localhost:8080/obs/stream/start # POST with JSON body curl -X POST http://localhost:8080/obs/transition \ -H "Content-Type: application/json" \ -d '{"name": "Fade", "duration_ms": 500}' # With auth token curl -H "Authorization: Bearer YOUR_KEY" \ http://localhost:8080/obs/scenes
Complete flow — broadcast startup
# 1. Validate playlists curl http://localhost:8080/playlists/validate # 2. Standby screen curl -X POST http://localhost:8080/presets/standby/activate # 3. Load main playlist from track 0 curl -X POST http://localhost:8080/playlists/main/activate # 4. Set fade transition curl -X POST http://localhost:8080/obs/transition \ -H "Content-Type: application/json" \ -d '{"name": "Fade", "duration_ms": 300}' # 5. Go live curl -X POST http://localhost:8080/presets/live/activate # 6. Start stream curl -X POST http://localhost:8080/obs/stream/start
Overlay — show title card for current track
# Configure: 10s hold, 2s delay, prefix curl -X POST http://localhost:8080/overlay/config \ -H "Content-Type: application/json" \ -d '{"hold_sec": 10, "delay_sec": 2, "prefix": "Now Playing: "}' # Trigger from current track metadata curl -X POST http://localhost:8080/overlay/trigger-current # Manual override — pop-up ad curl -X POST http://localhost:8080/overlay/trigger \ -H "Content-Type: application/json" \ -d '{"text": "Visit us at example.com", "hold_sec": 12, "delay_sec": 0}' # Force hide curl -X POST http://localhost:8080/overlay/hide
WebSocket (wscat)
# Install: npm install -g wscat wscat -c ws://localhost:8080/ws # With auth wscat -c "ws://localhost:8080/ws?token=YOUR_KEY" # Then send commands as JSON: {"cmd": "get_status"} {"cmd": "playlist_next"} {"cmd": "overlay_trigger", "params": {"text": "Hello", "hold_sec": 8}} {"cmd": "activate_preset", "params": {"name": "live"}}
http://localhost:8080/docs — you can test every endpoint there without writing any cURL at all.