Automatic video streaming server with web-based stream control and hot-reload file discovery. Each video is published three ways at once: RTSP and HLS (through MediaMTX, for playback) and MPEG-TS over UDP (which preserves KLV/MISB timed metadata that RTSP and HLS cannot carry).
-
Place your videos in the
videos/directory:cp your-video.mp4 videos/
-
Build and deploy via Docker Compose:
make build # Only needed once, or after code changes make compose-up
Autostream automatically:
- Scans the
videos/directory on startup - Starts streaming each video file via RTSP/HLS (through MediaMTX) and KLV-preserving MPEG-TS/UDP, looping infinitely by default
- Watches for new files added at runtime
- Removes streams when files are deleted
Each stream has a per-stream mode, chosen in the UI. Changing it (or the loop setting) on a running stream restarts the stream with the new value; on a stopped stream the choice is applied at the next Start. The modes:
- Passthrough (
Video: Passthrough): the source video is sent unmodified — no decode, no encode, near-zero CPU. Only H.264 and H.265 can pass through (that is all MediaMTX carries). H.264 files default to passthrough; H.265 files default to transcode because browser HLS playback of H.265 is spotty, but passthrough can be selected for RTSP-centric consumers. Files with any other video codec always transcode; a passthrough request on them is kept but runs as transcode, with the reason shown in the UI. Note that passthrough streams follow the source file's keyframe cadence (players join and recover at source keyframes) instead of the enforced 1-second GOP, andMAX_VIDEO_BITRATEdoes not apply. - Transcode (
Video: Transcode): re-encode to H.264 with a clean 1-second GOP (fixes loop-point artifacts) and the bitrate cap applied, using the hardware encoder when available (see below).
Audio is handled automatically in both modes: copied when the source is already AAC, re-encoded to AAC otherwise.
Fully automatic, no configuration. make build and make compose-up run
gpuconfig, which looks for the CPU's integrated GPU (Intel iGPU or AMD APU —
discrete cards are never used) and records its access details in
.generated/gpu.env; composeup then chains docker-compose.gpu.yml into the
compose invocation only when that file exists. Inside the container, each
stream start runs a one-frame trial encode and picks the first tier that
works: h264_qsv (Intel Quick Sync), then h264_vaapi (AMD), then libx264
in software. Macs and machines without an integrated GPU always get the
software path.
To see what happened: the container log shows the CPU and GPU devices the
container sees at startup, and the chosen encoder is logged at the top of each
stream's ffmpeg log (/tmp/ffmpeg-logs/<stream>.log inside the container).
Other containers on octo-cx-network reach each stream through the autostream
service on the ports configured in .env:
- RTSP:
rtsp://autostream:${MEDIAMTX_RTSP_PORT}/<stream-name>(default8554) - HLS:
http://autostream:${MEDIAMTX_HLS_PORT}/<stream-name>/index.m3u8(default8888) - UDP (KLV):
udp://${OUTPUT_HOST}:<port>— MPEG-TS with KLV/data streams preserved. Each stream gets its own port from${UDP_BASE_PORT}through${UDP_LAST_PORT}; the exact port per stream is shown in the control UI and the/api/streamsoutput. A port stays assigned for the life of the supervisor so a consumer's registration keeps working across restarts, so a run that cycles through more videos than the range holds will use it up — later streams then publish RTSP/HLS only and say so in the UI.
For access from the host, see "Port Mappings" below.
RTSP and HLS go through MediaMTX, whose track model only carries H.264/AAC — KLV timed-metadata data streams are dropped (ffmpeg's RTP muxer cannot carry them either). To preserve KLV, autostream also stream-copies the data streams into an MPEG-TS feed pushed over UDP, the standard MISB/STANAG-4609 transport.
In the octo-cx stack, OUTPUT_HOST defaults to the cx-search video-streaming
service (on octo-cx-network), which listens on udp://0.0.0.0:<port>, decodes
the KLV, and publishes it to the AMQP stream.klv topic that cx-edge consumes
for geolocation. Add each feed to cx-search like any other stream, using the
udp://0.0.0.0:<port> the UI/API reports for it — there is no separate
registration step beyond adding the stream. Keep UDP_BASE_PORT/UDP_LAST_PORT
inside video-streaming's own ingest range (UDP_START_PORT..UDP_END_PORT,
40000-40100 by default) or it will not be listening where autostream pushes.
UDP is push, not pull. If OUTPUT_HOST does not resolve (for example the
cx-search stack isn't running), autostream logs a warning and streams RTSP/HLS
only rather than failing — the KLV feed simply starts once the consumer is
reachable and the stream restarts. For standalone use, set OUTPUT_HOST to any
reachable consumer or a multicast group like 239.0.0.1. Verify a feed with, for
example, ffprobe udp://<host>:<port> — the KLV stream appears as Data: klv (KLVA).
Example: If you add sailboat.mp4 to the videos/ directory:
rtsp://autostream:8554/sailboat
Stream names are sanitized from filenames:
My Video (1080p).mp4→my_video_1080ptest-stream.mkv→test-stream
Edit .env file:
Docker Compose reads .env directly and passes the whole file through to the
container; mediamtx.yml is mounted in as-is. .env is the single source of
truth: every variable must be set, and the only one that may be empty is
MAX_VIDEO_BITRATE (empty disables the cap). The Default column shows the
shipped .env values.
| Variable | Description | Default |
|---|---|---|
CONTAINER_NAME |
Image name and service/hostname used in stream URLs | autostream |
VERSION |
Image version tag | 2.2.0 (see .env) |
MAX_VIDEO_BITRATE |
Cap video bitrate (for example 3M, 5M); transcode mode only — passthrough sends the source bitrate unchanged |
2M |
OUTPUT_HOST |
Host/service the KLV UDP feeds are pushed to (cx-search video-streaming, or an IP/multicast group) |
video-streaming |
UDP_BASE_PORT |
First UDP port; each stream gets the next one | 40000 |
UDP_LAST_PORT |
Last UDP port; streams arriving after the range is used up run RTSP/HLS only | 40100 |
MEDIAMTX_RTSP_PORT |
RTSP listener port | 8554 |
MEDIAMTX_HLS_PORT |
HLS HTTP port | 8888 |
MEDIAMTX_RTP_PORT |
RTP UDP port | 8000 |
MEDIAMTX_RTCP_PORT |
RTCP UDP port | 8001 |
STREAM_API_PORT |
Stream control UI/API port | 8080 |
LOG_LEVEL |
Supervisor log level; debug also shows ffmpeg output |
info |
make build # Build the container image
make compose-up # Start via Docker Compose
make compose-down # Stop the Docker Compose stack
make compose-logs # Tail Docker Compose logs
make clean # Remove the config cache
# systemd service (optional; runs the stack on boot)
make systemd-install # Build the image, then install the systemd unit
make systemd-uninstall # Remove the systemd unitPlain docker compose up -d also works after the image is built; make compose-up adds config-change detection so mediamtx.yml edits recreate the
container when needed, plus the GPU detection described under "Hardware video
encoding" (a plain up runs without the GPU mapping, so streams encode in
software).
Note: Docker Compose uses an external network octo-cx-network. Create it first if it doesn't exist:
docker network create octo-cx-network| Service | External Port | Internal Port |
|---|---|---|
| RTSP | 9554 | 8554 |
| HLS | 9322 | 8888 |
| RTP | 9000/udp | 8000/udp |
| RTCP | 9001/udp | 8001/udp |
| Web UI | 9080 | 8080 |
Access streams at:
- RTSP:
rtsp://localhost:9554/<stream-name> - HLS:
http://localhost:9322/<stream-name>/index.m3u8 - Web UI:
http://localhost:9080
Every file in videos/ gets a stream except hidden files and READMEs — there
is no format allowlist. FFmpeg must be able to decode the file (MP4, MKV, AVI,
MOV, WEBM, FLV, TS, and so on); a non-video file gets a stream slot whose
ffmpeg process fails and is retried periodically.