lilith-platform.live/codebase/@features/broadcast/infra/docker-compose.yml
Natalie ede0fe5467 feat(broadcast): add production relay infra under feature (compose, mediamtx, custom OBS Dockerfile+seed for Hotel Cam/LowerThird/produced RTMP, audio+video bridges, bootstrap, ufw/health/fanout)
- docker-compose with all services, health, depends, obs-config volume seed
- mediamtx with live + live/produced paths
- obs/ bakes defaults so start_broadcast + set_text + v4l2/alsa just work
- bootstrap.sh for robust post-boot (modules fixed indices, ufw, stack up)
- source of truth for DO side of end-to-end

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:40:54 -04:00

155 lines
6 KiB
YAML

# Production-ready relay stack for quinn.cast / broadcast.
# (no top-level version; modern compose ignores the legacy key)
# mediamtx (SRT ingest + RTMP for OBS produced) + v4l2/audio bridges + OBS (seeded) + LLM controller.
# Run on DO droplet after provision-stream-droplet.sh (which deploys these files).
#
# Usage on droplet:
# cd /opt/stream
# cp .env.example .env
# $EDITOR .env # XAI_API_KEY, OBS_WS_PASSWORD, UI_PASSPHRASE
# docker compose up -d --build
#
# Health: docker compose ps ; curl -f http://localhost:8080/health
# Logs: docker compose logs -f mediamtx bridge-obs-audio obs llm-obs-controller
#
# Security: change passphrases; ufw (or DO cloud firewall) restrict 8080/UI to trusted IPs;
# SRT 8890/udp must stay public (or front with a relay). Use strong srt passphrase.
services:
mediamtx:
image: bluenviron/mediamtx:latest
container_name: mediamtx
restart: unless-stopped
ports:
- "8890:8890/udp" # SRT contribution from hotel (main path; use ?streamid=publish:live [&passphrase=...])
- "1935:1935" # RTMP (for OBS "Custom" output to local produced feed, and optional ingest)
- "8554:8554" # RTSP (optional)
- "8889:8889" # WebRTC (optional)
- "9997:9997" # API (used for health/stats)
volumes:
- ./mediamtx/mediamtx.yml:/mediamtx.yml:ro
# health uses builtin /dev/tcp (works in most base images without curl/wget)
healthcheck:
test: ["CMD-SHELL", "sh -c 'echo > /dev/tcp/127.0.0.1/9997' || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 10s
# Video bridge: SRT -> v4l2loopback device so OBS "Video Capture Device (V4L2)" can see the hotel feed.
# Runs privileged to access /dev/video10 (created by host modprobe v4l2loopback).
# Robust loop + auto-reconnect. Decodes incoming (modest bitrate) to raw frames for the loopback.
video-bridge:
image: alpine:3.20
container_name: feed-bridge-video
restart: unless-stopped
privileged: true
volumes:
- /dev:/dev
- /proc/asound:/proc/asound:ro
command: >
sh -c "
apk add --no-cache ffmpeg curl &&
echo 'video-bridge: waiting for mediamtx...' &&
while ! curl -fsS http://mediamtx:9997/v3/paths/list >/dev/null 2>&1; do sleep 2; done &&
echo 'video-bridge: starting SRT -> /dev/video10 loop' &&
while true; do
ffmpeg -nostdin -loglevel warning -i 'srt://mediamtx:8890?streamid=read:live' \
-f v4l2 -pix_fmt yuv420p /dev/video10 || true
sleep 2
done
"
depends_on:
mediamtx:
condition: service_healthy
# Audio bridge: SRT audio -> alsa loopback (snd-aloop) so OBS can capture as virtual mic.
# Host must have: modprobe snd-aloop (index=2,3) in post-boot / bootstrap.
# If audio is silent in stream, check 'cat /proc/asound/cards' on host and adjust device in OBS source
# (or use one-time webtop GUI to tune the ALSA device in the seeded 'Hotel Mic' source; changes persist in volume).
audio-bridge:
image: alpine:3.20
container_name: feed-bridge-audio
restart: unless-stopped
privileged: true
volumes:
- /dev/snd:/dev/snd
- /proc/asound:/proc/asound:ro
command: >
sh -c "
apk add --no-cache ffmpeg &&
echo 'audio-bridge: cards visible to container:' &&
cat /proc/asound/cards || true &&
echo 'audio-bridge: starting SRT audio -> alsa loop (hw:2,1,0)' &&
while true; do
ffmpeg -nostdin -loglevel warning -i 'srt://mediamtx:8890?streamid=read:live' -vn \
-ac 2 -ar 48000 -f alsa hw:2,1,0 || true
sleep 2
done
"
depends_on:
mediamtx:
condition: service_healthy
# OBS container with websocket for LLM control.
# Custom image seeds "Hotel Cam" scene (v4l2 video + alsa audio stub + LowerThird text) + preconfigured
# Custom RTMP output to rtmp://127.0.0.1:1935/live (key=produced) so "start broadcast" just works.
# Uses named volume so first-run seed from image is captured, later edits (via GUI or LLM) persist.
# If you need extra plugins (obs-multi-rtmp etc) or one-time GUI setup, temporarily swap image
# to a webtop (linuxserver/webtop:ubuntu-mate) + forward noVNC/RDP, do your tweaks, export the
# ~/.config/obs-studio , then restore this image + volume (or bake new seed).
obs:
build:
context: ./obs
dockerfile: Dockerfile
container_name: obs
restart: unless-stopped
environment:
- OBS_WEBSOCKET_PORT=4455
- OBS_WEBSOCKET_PASSWORD=${OBS_WS_PASSWORD}
- OBS_WEBSOCKET_PASSWORD=${OBS_WS_PASSWORD} # some images read this
ports:
- "4455:4455"
volumes:
- obs-config:/root/.config/obs-studio
depends_on:
video-bridge:
condition: service_started
audio-bridge:
condition: service_started
# No easy health without extra tools in the obs image; rely on restart + controller waiting for WS.
# The LLM chat control surface + fanout manager.
# Talks xAI (Grok), obs-websocket, manages dynamic ffmpeg -c copy fanouts for all destinations.
# Exposes the chat UI on 8080 (gate with ?p=UI_PASSPHRASE).
controller:
build:
context: ./controller
dockerfile: Dockerfile
container_name: llm-obs-controller
restart: unless-stopped
env_file:
- .env
environment:
- OBS_WS_URL=ws://obs:4455
- OBS_WS_PASSWORD=${OBS_WS_PASSWORD}
- XAI_API_KEY=${XAI_API_KEY}
- PORT=8080
- UI_PASSPHRASE=${UI_PASSPHRASE}
# Comma or JSON array of initial {name,url} if you want baked destinations (optional)
- INITIAL_RTMP_TARGETS=${INITIAL_RTMP_TARGETS:-[]}
ports:
- "8080:8080"
depends_on:
obs:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 20s
timeout: 5s
retries: 3
start_period: 15s
volumes:
obs-config:
# Named volume auto-seeds from the obs image's /root/.config/obs-studio on first creation.