obs-relay setup
Home
── 1. HOW IT WORKS ───────────────────────────────────────
Section 01

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.

Two ports, two different protocols
Port 8080 — obs-relay's own API. This is what you call.
Port 4455 — OBS's built-in WebSocket server. obs-relay calls this internally. You never touch it directly.
── 2. INSTALL ───────────────────────────────────────────
Section 02

Install & Start the Server

  • 1
    Clone or unzip the project
    cd obs-relay-v2
  • 2
    Install dependencies
    pip install -r requirements.txt
    Python 3.10+ required
    Check with python3 --version. The match statement used in the server requires 3.10 minimum.
  • 3
    Copy and edit config
    cp config.yaml.example config.yaml

    Open config.yaml and set your OBS WebSocket password:

    obs:
      host: localhost
      port: 4455
      password: "your-obs-password-here"
  • 4
    Test 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.

  • 5
    Start 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.
── 3. OBS SETUP ──────────────────────────────────────────
Section 03

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

  • 1
    Open OBS → Tools → WebSocket Server Settings
    Make sure "Enable WebSocket server" is checked. Server port should be 4455.
  • 2
    Set a password (recommended)
    Check "Enable Authentication" and set a password. Copy this to config.yamlobs.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 NamePresetPurpose
LiveliveMain broadcast content
BRBbrbBe Right Back screen
StandbystandbyHolding / pre-show screen
IntermissionintermissionBreak content loop
EndCardend_cardPost-show slate
Names are case-sensitive and must match exactly
If your OBS scene is named 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:

  • 1
    In OBS, open your Live scene → Sources → Add → Media Source
    Name it exactly MediaSource (or change playlist.source_name in config.yaml to match whatever you name it)
  • 2
    Uncheck "Local File" for now — obs-relay will set the file path at runtime
    Leave Loop unchecked. obs-relay manages advancement via the MediaInputPlaybackEnded event.

3d — Create a Text Source (for overlays)

For title card overlays — only needed if you want to use the overlay system:

  • 1
    Sources → Add → Text (GDI+) on Windows, Text (FreeType 2) on Mac/Linux
    Name it exactly TitleOverlay
  • 2
    Click the eye icon to hide it — it must start hidden
    obs-relay shows/hides it via the visibility toggle. If it starts visible, overlays won't "appear" — they'll already be on screen.
  • 3
    Style it however you want — font, size, color, position, background
    obs-relay only sets the text content and visibility. Layout is entirely yours.
── 4. FIRST TEST ─────────────────────────────────────────
Section 04

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"
}
Getting "connection refused" instead?
The server is not running. Go back to Section 2 and start it with 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.

── 5. DIAGNOSE ───────────────────────────────────────────
Section 05

Diagnose Errors

Click any error message to expand the cause and fix.

{"detail": "Not Found"} 404 from any endpoint
The URL path is wrong. Check for typos — all paths are lowercase with hyphens, not underscores. Example: /playlists/auto-advance not /playlists/auto_advance
You're using GET on a POST endpoint or vice versa. Check the method in the endpoint reference (Section 9).
The route parameter is in the wrong place. /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.

{"detail": "OBS not connected"} or 503 OBS WebSocket link is down
OBS is not open. Open OBS Studio first.
Wrong password in config.yaml. Check obs.password matches OBS → Tools → WebSocket Server Settings exactly.
Wrong port. Default is 4455 since OBS 28. Older OBS versions used 4444 with a different plugin.
OBS WebSocket is disabled. Go to OBS → Tools → WebSocket Server Settings and make sure "Enable WebSocket server" is checked.
# Run the connection check tool
python run.py check --password your-password
# Or test manually
curl http://localhost:8080/health
{"detail": "Preset 'live' not found"} Preset activation fails
The preset name doesn't exist. Valid built-in presets: live, brb, standby, intermission, end_card. All lowercase.
# See all registered presets
curl http://localhost:8080/presets
Preset activates but scene doesn't switch in OBS Scene name mismatch
The preset's 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.

{"detail": "Playlist manager not initialized"} Playlist endpoints return 503
This normally shouldn't happen if the server started correctly. Try restarting with python run.py start and check the startup log for errors.
The playlists/ directory doesn't exist. Create it: mkdir playlists
Playlist activates but video doesn't play in OBS Media source not set up correctly
The OBS source is not named MediaSource. Check the source name in OBS and set playlist.source_name in config.yaml to match.
The file paths in your .m3u file don't exist on the machine running OBS. Run python run.py validate-playlists to check every path.
The media source is not in the current active scene. OBS sources are per-scene — the MediaSource must be in the scene you've switched to.
# Check file paths in your playlists
python run.py validate-playlists

# Check media source status
curl http://localhost:8080/obs/source/MediaSource/media
Overlay triggers but nothing shows in OBS TitleOverlay source not found or wrong scene
No source named TitleOverlay exists in OBS. See Section 3d to create it.
The source exists in a different scene than the current program scene. Either add it to the current scene or set overlay.scene_name in config.yaml to the exact scene name that has it.
Case sensitivity: TitleOverlaytitleoverlay. 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.

401 Unauthorized or 403 Forbidden API key required
You set an 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.

── 6. GUI ────────────────────────────────────────────────
Section 06

Using the GUI Prototyper

Open obs_relay_gui.html directly in your browser — no web server needed.

  • 1
    Set the server URL in the sidebar
    Default 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.
  • 2
    Click Connect
    This tests the HTTP connection and auto-connects the WebSocket. You'll see green dots when both are live.
  • 3
    Every button generates a cURL command
    The 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.
  • 4
    Watch the WebSocket panel
    Go 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.
  • 5
    Run a Demo Workflow
    Go 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.
The GUI is just a test tool — not required to use the API
Everything in the GUI is an HTTP call to obs-relay. Your real integration — a custom dashboard, a show control script, a hardware controller — makes the same calls with the same cURL commands shown in the right panel.
── 7. PLAYLISTS ──────────────────────────────────────────
Section 07

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
File paths must be absolute on the OBS machine
Relative paths don't work. OBS needs the full path to load the file. On Windows: 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
── 8. OVERLAYS ───────────────────────────────────────────
Section 08

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
Position safety — how it works
The overlay manager reads 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.
Fade in / fade out requires an OBS filter
obs-relay shows/hides the source instantly at the OS level. For a smooth visual fade, right-click the TitleOverlay source in OBS → Transition Override → set a Fade with your desired duration. OBS will then animate it automatically on every show/hide.
── 9. ENDPOINTS ──────────────────────────────────────────
Section 09

Endpoint Quick Reference

All endpoints are prefixed with your server address, e.g. http://localhost:8080. Interactive docs always available at /docs.

System
GET/healthOBS connected, WS clients, version
GET/healthzReturns 503 when OBS disconnected — use for monitoring
GET/obs/versionOBS Studio + WebSocket version
Scenes
GET/obs/scenesAll scenes in OBS
GET/obs/scene/currentActive program scene
POST/obs/scene/{scene_name}Switch to scene directly
GET/obs/transitionCurrent transition name + duration
POST/obs/transitionSet transition — body: {"name":"Fade","duration_ms":500}
Presets
GET/presetsAll registered presets
POST/presets/{name}/activateSwitch scene + run side effects
Playlists
GET/playlistsAll loaded playlists with track lists
GET/playlists/statusActive playlist, position, current track
GET/playlists/validatePreflight check — all files exist on disk
POST/playlists/{name}/activateStart playlist at position (add ?position=3 to seek)
POST/playlists/nextAdvance one track
POST/playlists/prevGo back one track
POST/playlists/seek/{position}Jump to track index (0-based)
POST/playlists/auto-advanceToggle auto-advance — body: {"enabled": true}
POST/playlists/createBuild playlist from file paths
DEL/playlists/{name}Remove playlist from memory + disk
Text Overlays v1.2
GET/overlay/statusActive, text, timer remaining, config
GET/overlay/configAll current settings
POST/overlay/configUpdate settings — only pass fields to change
POST/overlay/triggerShow any text — body: {"text":"…","hold_sec":8}
POST/overlay/trigger-currentRe-show current track title from M3U metadata
POST/overlay/hideCancel timer and hide immediately
Audio
GET/obs/source/{name}/mediaMedia source state, cursor, duration
POST/obs/source/{name}/volumeSet volume dB — body: {"volume_db": -6}
POST/obs/source/{name}/muteMute — body: {"muted": true}
Streaming
GET/obs/stream/statusActive, timecode, bytes sent
POST/obs/stream/startBegin streaming
POST/obs/stream/stopStop streaming
Recording v1.1
GET/obs/record/statusActive, paused, timecode, bytes
POST/obs/record/startStart recording
POST/obs/record/stopStop — response includes output file path
POST/obs/record/pausePause
POST/obs/record/resumeResume
Realtime
GET/wsWebSocket endpoint — connect with ws://host:8080/ws
GET/osc/statusTouchOSC bridge status
── 10. CURL ───────────────────────────────────────────────
Section 10

cURL 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"}}
Interactive docs at /docs
FastAPI generates a full Swagger UI at http://localhost:8080/docs — you can test every endpoint there without writing any cURL at all.