Self hosted, completely free set of stream tools to fully control audio and video to an OBS overlay. Edit position, scale, start times, stop times, volume, delay, etc. No data collection, no monthly fee, no sign ups, always free!
This has no authentication of any kind. It is designed to run on a trusted home/studio LAN — never expose it to the public internet.
Anyone who can reach the port can:
- trigger any media into your stream,
- upload arbitrary files to your machine (the upload endpoint accepts files and writes them to disk),
- delete any media in your library,
- enumerate your entire library via the API.
There is no rate limiting and no input gating beyond file-extension checks. Do not port-forward this, do not put it on a public VPS, and do not assume "nobody knows the URL" protects you.
Safe ways to run it:
- On the same machine as OBS, accessed only via
localhost(sethost="127.0.0.1"near the bottom ofhexcast.py). - On a LAN, with a firewall rule restricting the port to your local subnet (see Network access & firewall).
- If you genuinely need remote access, put it behind a reverse proxy (nginx/Caddy) that enforces authentication and TLS, on a private network or VPN — that's on you to set up correctly.
- Folder-watch: drop media into
media/audio/ormedia/video/and it appears in the control panel instantly - Drag-and-drop uploads from the control panel — audio extensions route to
audio/, everything visual tovideo/ - Auto-conversion: dropped
.gif/.webp/.apngfiles get transcoded to.mp4on the spot, so every clip is frame-precise seekable - Web search for video clips (Tenor) and audio (Freesound) with one-click import — optional, requires free API keys
- Per-clip editor for video: drag-to-position canvas, scale, volume (when the clip has audio), and a dual-thumb start/end trim slider with ▶ Preview that plays the trimmed range live in the canvas
- Per-clip editor for audio: volume, start/end trim, and live preview through your speakers
- Rename in editor: change a clip's filename without touching the filesystem; the sidecar and poster follow automatically
- Per-clip cooldown: set ms-level cooldown on any clip so spam clicks don't queue up —
0= current spam-friendly behavior - 🔊 audio badge on video buttons that carry an audio track
- Concurrent playback: spam-click to layer multiple clips at once (cooldowns are per-clip, so other clips still overlap freely)
- Edit Mode for tuning, Delete Mode for cleanup — no manual filesystem digging
- Bot API: trigger anything by name via a simple HTTP GET, no auth needed (designed for trusted LAN)
- ⏹ Stop All panic button to clear every visual and stop every playing sound at once
- Python 3.10+ (uses modern type union syntax)
- ffmpeg — for gif/webp → mp4 conversion, thumbnail generation, and duration/audio probing. Hexcast still runs without it, but animated GIFs won't be seekable, thumbnails won't generate, and the audio badge won't appear on video buttons.
- OBS Studio 28+ with browser source support
git clone https://github.com/YOUR_USERNAME/hexcast.git
cd hexcast
chmod +x start.sh
./start.shstart.sh creates a venv, installs dependencies, and launches the server. Subsequent runs just launch.
If you prefer manual setup:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python hexcast.pygit clone https://github.com/YOUR_USERNAME/hexcast.git
cd hexcast
start.batOr double-click start.bat from Explorer.
Manual setup:
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
python hexcast.pyWithout ffmpeg the server still runs, but: animated GIFs/WebPs won't convert to MP4 (they stay as <img> elements with limited functionality), thumbnails won't generate (videos show empty cards), and the 🔊 audio badge won't appear on video buttons.
Linux (Debian/Ubuntu):
sudo apt install ffmpegLinux (Fedora):
sudo dnf install ffmpegmacOS:
brew install ffmpegWindows:
- Download a release build from https://www.gyan.dev/ffmpeg/builds/ (get "release essentials")
- Extract somewhere permanent (e.g.,
C:\ffmpeg\) - Add
C:\ffmpeg\binto your PATH (System Properties → Environment Variables → Path → New) - Open a new terminal and verify:
ffmpeg -version
After running start.sh / start.bat, you'll see:
Control panel: http://localhost:4747/
OBS browser source: http://localhost:4747/overlay
Media root: /path/to/hexcast/media
Canvas: 1920x1080
Posters (ffmpeg): enabled
Tenor search: disabled (set TENOR_API_KEY)
Freesound search: disabled (set FREESOUND_API_KEY)
In OBS: Sources → + → Browser.
- URL:
http://localhost:4747/overlay - Width / Height: match your canvas (typically 1920×1080)
- Control audio via OBS: ON (so audio routes through your OBS mixer)
- Shutdown source when not visible: OFF (kills the WebSocket otherwise)
- Refresh browser when scene becomes active: OFF
After clicking OK, select the source in the canvas and press Ctrl+F to fit to screen.
Three ways:
- Drag-and-drop onto the dropzone in the control panel
- Search Tenor/Freesound (requires API keys, see below)
- Drop directly into
media/audio/ormedia/video/
The folder watcher picks up new files instantly. Animated .gif/.webp/.apng files auto-convert to .mp4 on first sight so the editor can seek into them frame-precisely.
Click any button in the control panel. Multiple clicks layer naturally — spam them, they'll all fire (unless you've set a per-clip cooldown).
Toggle Edit Mode in the top-right (clip buttons get an amber border + ✎). Click any clip to open its editor.
Video editor (any visual file — mp4, gif, png, etc.):
- Position — drag the clip around a 16:9 preview of your OBS canvas, or use the 3×3 quick-position grid
- Scale slider
- Volume slider — only shown for video files that actually have an audio track (the ones with a 🔊 badge on the button)
- Playback range — a dual-thumb slider on a single bar. Drag the start (◀) and end (▶) thumbs to trim. As you drag, the preview canvas scrubs to that frame so you can see exactly where you are.
- ▶ Preview — plays the trimmed range inside the editor canvas with current position, scale, and volume so you can fine-tune everything before committing. ⏸ to stop.
- Rename — change the clip's filename. Sidecar and poster follow automatically.
- Cooldown (ms) — if > 0, after this clip fires, further triggers of this clip are dropped until the clip finishes playing plus the cooldown elapses.
0means no cooldown (current spam-friendly behavior). Per-clip only; other clips still overlap freely.
Audio editor:
- Volume, trim, rename, cooldown — same as above. ▶ Preview plays through your speakers.
Static images (.png, .jpg) can't be seeked; for those the trim becomes a single "Display duration" slider, and Preview is disabled.
Click Test in OBS to fire the current (unsaved) settings to OBS, Save to write them to the sidecar JSON. Defaults are omitted from the JSON to keep it clean: a clip with only a custom end time saves as just {"volume": 1.0, "end": 3.5}.
Toggle Delete Mode (buttons get a red border + ✕). Click any button → confirms → removes the file, its sidecar, and its thumbnail.
The ⏹ Stop All button in the top-right instantly clears every visual from the overlay and stops all playing audio. Also available to bots at GET /api/stop.
By default media lives in media/ next to the script. To store it elsewhere — a different drive, a NAS mount, a shared folder — set the HEXCAST_MEDIA_DIR environment variable. The audio/ and video/ subfolders are created automatically inside it.
If you're upgrading from a previous version with sounds/, gifs/, and videos/ folders, Hexcast migrates them automatically on first run: contents of sounds/ move to audio/, contents of gifs/ and videos/ merge into video/, and the old folders are removed when empty.
Linux/macOS:
export HEXCAST_MEDIA_DIR="/mnt/storage/hexcast"
./start.shWindows:
set HEXCAST_MEDIA_DIR=D:\hexcast-media
start.batTo make it permanent on Windows, set it via System Properties → Environment Variables. On Linux, add the export line to your ~/.bashrc, or use Environment= in the systemd unit (see Running on startup).
Edit the constants at the top of hexcast.py:
PORT = 4747 # change if conflicting with another service
CANVAS_W = 1920 # only affects the editor preview proportions
CANVAS_H = 1080
DEFAULT_X = 50.0 # default position (% of canvas)
DEFAULT_Y = 50.0
DEFAULT_SCALE = 3.0 # default scale multiplier for new gifs/videosAnd the bind host near the bottom of the file:
uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="warning")
# ^^^^^^^^^ change to "127.0.0.1" for local-only (no LAN access)The overlay itself is canvas-agnostic — it uses percentages, so the same setup works whether your OBS canvas is 1080p, 1440p, 4K, or vertical.
Both providers are free with a quick sign-up.
-
https://developers.google.com/tenor/guides/quickstart → create a project, enable Tenor API, generate a key
-
Set the environment variable
TENOR_API_KEYbefore launching:Linux/macOS:
export TENOR_API_KEY="AIza..." ./start.sh
Windows:
set TENOR_API_KEY=AIza... start.bat
Or set it permanently via System Properties → Environment Variables.
- Sign up at https://freesound.org/
- Apply at https://freesound.org/apiv2/apply (instant approval)
- Set
FREESOUND_API_KEYthe same way
The server exposes an HTTP API designed for triggering media from chat bots, stream-deck buttons, scripts, or any HTTP client. No authentication — intended for trusted LAN use.
GET /api → endpoint reference
GET /api/list → JSON: { audio: [...], video: [...] }
GET|POST /api/play/{name} → fuzzy: searches audio, then video
GET|POST /api/play/{kind}/{name} → explicit: kind = audio | video
?x=&y=&scale= → optional position override (video)
?volume= → optional volume override 0.0-1.0 (audio, or video with audio)
?start=&end= → optional trim window in seconds (both kinds; static images use end only)
GET|POST /api/stop → clear all visuals + stop all audio (panic button)
POST /rename → {file, kind, new_stem} → renames media file + sidecar + poster
Names are case-insensitive and match either the filename stem (airhorn) or full filename (airhorn.mp3). If you don't pass overrides, the values saved in the editor (position, scale, volume, trim) are applied automatically.
Cooldowns: if a clip has a non-zero cooldown_ms saved in its sidecar, triggers that arrive while the clip is still in its cooldown window return {"ok": true, "delivered": 0, "suppressed": true, "next_in_ms": N} and don't fire. Cooldowns are per-clip — other clips can still overlap freely.
curl http://localhost:4747/api/list
curl http://localhost:4747/api/play/airhorn
curl http://localhost:4747/api/play/video/wow
curl "http://localhost:4747/api/play/video/cheer?x=80&y=20&scale=2&volume=0.6&end=3"
curl http://localhost:4747/api/stopPython:
import requests
requests.get("http://localhost:4747/api/play/airhorn")Node.js:
await fetch(`http://localhost:4747/api/play/${name}`);There's no dedicated Twitch script — Hexcast stays generic. If you already have a bot monitoring Twitch chat, channel-point redemptions, or events, just have it call GET /api/play/{name} when the relevant trigger fires. Map a redemption title or chat command to a media name and hit the endpoint. The overlay applies the saved position/scale/volume automatically, so the bot only needs to know the clip name.
The server binds to 0.0.0.0, so the control panel is reachable from any device on your network. Limit access with your firewall.
sudo ufw allow from 192.168.1.0/24 to any port 4747 proto tcpReplace 192.168.1.0/24 with your actual LAN range. Find it with ip route.
- Open Windows Defender Firewall with Advanced Security
- Inbound Rules → New Rule → Port → TCP, specific port 4747 → Allow the connection
- In the Scope tab, restrict Remote IP addresses to your LAN range
Create ~/.config/systemd/user/hexcast.service:
[Unit]
Description=Hexcast
After=network.target
[Service]
WorkingDirectory=%h/hexcast
ExecStart=%h/hexcast/.venv/bin/python %h/hexcast/hexcast.py
Restart=on-failure
# Optional API keys:
# Environment=TENOR_API_KEY=AIza...
# Environment=FREESOUND_API_KEY=...
[Install]
WantedBy=default.targetEnable:
systemctl --user daemon-reload
systemctl --user enable --now hexcast
journalctl --user -u hexcast -f # watch logs- Open Task Scheduler → Create Task
- General: name it "Hexcast", select "Run only when user is logged on"
- Triggers: New → "At log on"
- Actions: New → Program/script: full path to
start.bat, Start in: the project folder - Conditions: uncheck "Start only if on AC power" if on a laptop
- Settings: check "Allow task to be run on demand"
Audio: .mp3, .wav, .ogg, .m4a, .flac, .opus
Video — static images: .png, .jpg, .jpeg (no seeking, shown for a fixed duration)
Video — animated images: .gif, .webp, .apng (auto-converted to .mp4 on first sight)
Video — native: .mp4, .webm, .mov, .mkv
For best video compatibility, transcode unfamiliar formats to H.264 + AAC MP4:
ffmpeg -i input.whatever -c:v libx264 -preset fast -crf 23 -c:a aac -b:a 128k -movflags +faststart output.mp4hexcast/
├── hexcast.py # the server
├── static/
│ ├── control.html # control panel UI (HTML + CSS + JS)
│ └── overlay.html # OBS browser-source overlay
├── requirements.txt
├── start.sh / start.bat # launcher scripts
├── README.md
├── LICENSE
└── media/ # auto-created on first run
├── audio/ # .mp3, .wav, .ogg, .m4a, .flac, .opus
└── video/ # .mp4, .webm, .png/.jpg (static), and animated images auto-converted to .mp4
Each clip can have two adjacent files:
airhorn.mp4— the mediaairhorn.json— saved settings (created by Edit Mode)airhorn.poster.jpg— first-frame thumbnail (auto-generated by ffmpeg for video clips)
The sidecar JSON contains only non-default values. A typical video sidecar might be:
{"x": 80, "y": 20, "scale": 2.5, "volume": 0.6, "end": 3.5, "cooldown_ms": 1500}A typical audio sidecar:
{"volume": 0.8, "start": 0.5, "cooldown_ms": 500}You can hand-edit these if you prefer — the watcher ignores .json writes so it won't trigger a reindex loop.
Posters not generating, gifs animate in picker: ffmpeg not in PATH. Run ffmpeg -version to verify.
Audio doesn't play in OBS: enable Control audio via OBS on the browser source. The soundboard appears in your Audio Mixer. Set monitoring to "Monitor and Output" if you want to hear it locally too.
Browser source stays black: right-click the source → Interact → check the DevTools console for errors. Make sure Width/Height match your canvas and the source has been transformed to fit (Ctrl+F).
Changes to overlay CSS don't show up in OBS: OBS aggressively caches. Right-click source → Interact → press Ctrl+Shift+R for a hard reload. Or append ?v=N to the URL and bump N.
Edit Mode opens once then breaks: you have an old version. Update to the latest, which uses static posters in the editor preview (no looping video element).
"unsupported extension" on upload: the file type isn't in the lists above. Convert it first.
Video plays in picker editor but is black/silent in OBS overlay: codec issue, transcode as shown above.
Page in browser shows SSL error: you typed https://. The server only speaks http://. Type the URL explicitly, or disable HTTPS-only mode in your browser for this host.
- Hotkey triggers via OBS WebSocket
- Search/filter box in the control panel for large libraries
- Multi-track audio splitting (separate OBS audio source per clip)
Twitch integration is intentionally out of scope — point any bot at GET /api/play/{name} (see Triggering from a chat bot).
MIT — see LICENSE.
