Executive summary
Sports Stream is a monorepo I built to answer a practical question: can spare Android phones replace dedicated SRT camera hardware for multi-cam OBS productions? The stack encodes video on-device, publishes over SRT to a self-hosted MediaMTX router, and exposes RTSP feeds that OBS pulls directly. A small Node API and React dashboard sit on top for QR pairing, connection strings, camera health, and remote start/stop.
It is not a product launch. It is a working technical system — server deployed, ingest validated, production client confirmed — that proves the phone-to-OBS path is viable without custom transcoding infrastructure.
The problem
Running three or more camera angles for a local sports stream typically forces a choice between expensive gear and fragile workarounds. Bonded cellular encoders and dedicated SRT cameras work, but they are overkill for community or semi-pro coverage. Phone apps can stream to YouTube or Facebook, but they do not give OBS clean, switchable RTSP inputs. DIY setups often reinvent routing, auth, and pairing on every event.
The failure mode I was targeting: a producer with OBS and a few Android phones still cannot get stable, independently switchable camera feeds without either proprietary hardware or a brittle chain of RTMP relays and screen captures.
The thesis
The bet was that the hard parts — hardware encoding, SRT transport, and protocol conversion — already exist in mature open components. Android phones handle H.264 via MediaCodec. MediaMTX handles SRT ingest and RTSP output without acting as a production switcher. OBS remains the switcher and overlay engine.
What needed building was the glue: credential distribution so phones are not hardcoded, a control plane for start/stop during a live event, and enough operational tooling to deploy and debug the server reliably. If that glue worked, the total system cost drops to a VPS and phones already in a drawer.
Validation
What exists today:
- A pnpm monorepo with four apps: MediaMTX server (
apps/server), Android camera client (apps/android-app), Express control API (apps/api), and React dashboard (apps/web). - MediaMTX running on a Hetzner VPS with separate dev and prod folder clones on the same machine.
- GitHub Actions SSH deploy for
apps/server/**changes — production MediaMTX updates on push to master without manual intervention. - Confirmed OBS connection to RTSP output (
rtsp://obs:***@host:8554/cam1). - Android client streaming from a Samsung S22 over SRT with hardware H.264, after resolving audio sync, SRT latency, and Wi-Fi throughput issues documented in the repo's debug log.
- Pairing flow: dashboard serves QR codes and JSON credentials; phone scans or enters config manually.
- Remote control: dashboard sends start/stop commands; phone polls every 3 seconds and reports bitrate, battery, and thermal status.
What does not exist yet:
- Public release, Play Store distribution, or paying users.
- Documented use at a live sports event in production.
- Fully automated deploy for the API and web dashboard (still manual
git pull+ tmux restart on the VPS).
What I built
MediaMTX server (apps/server)
- Docker Compose deployment of MediaMTX 1.18.2
- SRT ingest (UDP 8890), RTSP output (TCP 8554), HLS and WebRTC outputs
- Per-camera publish auth (cam1/cam2/cam3) and OBS read credentials
- Automatic segment recording to disk with 7-day retention
- Shell scripts for up/down/logs/healthcheck/deploy
- Separate dev and prod compose files with different port mappings on one VPS
Android camera app (apps/android-app)
- Kotlin app using RootEncoder for Camera2 capture, MediaCodec H.264/H.265 encoding, and SRT publishing
- Connection state machine: idle → connecting → streaming → error
- Codec, resolution, and bitrate selectors; client-side adaptive bitrate when SRT congestion is detected
- QR pairing via ZXing (
PairActivity) with manual credential fallback - StatusReporter: polls API every 3s for remote start/stop, pushes bitrate/battery/thermal back
- OLED power saver mode — black screen at 5% brightness with a pulsing live indicator to extend battery during long streams
Control API (apps/api)
- Express + TypeScript on port 3000
GET /pair/:camandGET /pair/:cam/qr.pngfor credential distributionGET /connections— full SRT/RTSP/HLS/WebRTC URL reference for all camerasPOST /cameras/:id/commandand status polling endpoints for remote control- Syncs live connection state from MediaMTX API every 10 seconds
- Serves the production React build as static files
Web dashboard (apps/web)
- React + Vite + shadcn/ui
- Camera status cards with connection state, bitrate, battery, thermal
- Connection strings panel with copyable SRT/RTSP/HLS URLs and QR links
- Auto-refreshes every 5 seconds
Shared types (packages/shared)
- TypeScript definitions for camera IDs, server config, and SRT/RTSP URL builders
CI/CD
.github/workflows/deploy-server.yml— SSH deploy via appleboy/ssh-action on server changes