A multi-channel HLS router with hot switching, built with FastAPI and ffmpeg.
[Live Sources] --> [Channel Workers] --> [HLS Segments]
| |
v v
[FastAPI API] --> [Nginx Static] --> [Players]
^
|
[Global Standby Slate]
- Router: FastAPI app managing channels, workers, and API.
- Workers: Per-channel ffmpeg processes normalizing inputs to H.264/AAC 1080p30 HLS.
- Standby: Global SMPTE bars or image + audio slate for fallbacks.
- Nginx: Serves HLS segments statically.
Hot switching: Workers copy/link segments from live or standby, inserting discontinuities.
- Build and run:
docker build -t caupolican .
docker run –rm -it -p 9999:9999 \
-v $(pwd)/out:/out \
caupolican
- API: http://localhost:9999/api/health
- HLS: http://localhost:9999/hls/news/index.m3u8 (after creating channel)
After pulling the image:
docker pull ghcr.io/${{ github.repository_owner }}/caupolican:latest
docker run -d –name cuapolican \
–restart=always \
–ulimit nofile=65536:65536 \
-e TARGET_DURATION=2 \
-e WINDOW_SEGMENTS=15 \
-e STANDBY_TEXT="Caupolican — Standby" \
-e CHANNELS="news,sports" \
-v /tmp/caupolican/out:/out \
-p 80:9999 \
ghcr.io/${{ github.repository_owner }}/caupolican:latest
- API:
/api/health,/api/channels,/api/channels/{id}/set-source,/api/channels/{id}/stop - HLS:
http://host/hls/<channel>/index.m3u8
Channels are created dynamically when setting a source.
GET /health→{"ok": true, "channels": {"news": {"active": false, "media_seq": 0, "window": 6}}}GET /channels→[{"id": "news", "active": false, "media_seq": 0, "window": 6}]GET /channels/{id}/status→{"id": "news", "active": false, "media_seq": 0, "window": 6}POST /channels/{id}/set-sourcebody{"url": "rtmp://..."}→ Switch to livePOST /channels/{id}/stop→ Fallback to standbyDELETE /channels/{id}/segments→ Reset window
Auth: If ROUTER_TOKEN set, include Authorization: Bearer <token> on POST/DELETE.
Deployment is automatic on push to main. The workflow builds the image, pushes to GHCR, and deploys to the on-premises server.
Prerequisites:
- Set GitHub Secrets:
DEPLOYMENT_TOKEN,DEPLOY_HOST,DEPLOY_USER,DEPLOY_SSH_KEY - Environment variables are configured in the GitHub Actions workflow
CHANNELS: Comma-separated channel IDsOUT_ROOT: /outTARGET_DURATION: 2 (seconds)WINDOW_SEGMENTS: 6STANDBY_TEXT: Text for slateSTANDBY_IMAGE: Optional image pathROUTER_TOKEN: Optional bearer tokenRESTART_MAX: 6BACKOFF_BASE_MS: 500BACKOFF_FACTOR: 2.0BACKOFF_CAP_MS: 30000COOLOFF_SEC: 300STALL_FACTOR: 3
- SELinux: Use
:Zon volumes - Health flaps: Check logs, disk space
- ffmpeg crashes: Tune presets, increase mem
- Inodes: Monitor segment cleanup