mediamtx
7 min read
Ready-to-use, zero-dependency real-time media server and media proxy. Routes streams between sources and consumers across multiple protocols. Formerly rtsp-simple-server.
Protocol Support
| Protocol | Publish | Read | Default Port |
|---|---|---|---|
| RTSP / RTSPS | ✓ | ✓ | 8554 / 8322 |
| RTMP / RTMPS | ✓ | ✓ | 1935 / 1936 |
| HLS / LL-HLS | — | ✓ | 8888 |
| WebRTC (WHIP/WHEP) | ✓ | ✓ | 8889 |
| SRT | ✓ | ✓ | 8890 |
Install
sh
# Docker (recommended)
docker run --rm -it --network=host bluenviron/mediamtx:latest
# Binary — download from GitHub Releases
curl -L https://github.com/bluenviron/mediamtx/releases/latest/download/mediamtx_linux_amd64.tar.gz | tar xz
./mediamtx
# Nix
nix run nixpkgs#mediamtx
Docker Compose
yaml
services:
mediamtx:
image: bluenviron/mediamtx:latest
restart: unless-stopped
network_mode: host # simplest for RTSP/UDP; avoids NAT issues
volumes:
- ./mediamtx.yml:/mediamtx.yml
- ./recordings:/recordings
Or with explicit port mapping (avoids host networking):
yaml
services:
mediamtx:
image: bluenviron/mediamtx:latest
volumes:
- ./mediamtx.yml:/mediamtx.yml
ports:
- "8554:8554" # RTSP
- "1935:1935" # RTMP
- "8888:8888" # HLS
- "8889:8889" # WebRTC HTTP
- "8890:8890/udp" # SRT
- "8189:8189/udp" # WebRTC UDP
- "9997:9997" # API (optional)
Common Recipes
IP Camera Proxy (RTSP → HLS + WebRTC)
Pull from a camera and expose to browsers:
yaml
paths:
camera:
source: rtsp://admin:password@192.168.1.100:554/stream1
sourceOnDemand: yes # only pull when a client connects
# Read via HLS: http://localhost:8888/camera
# Read via WebRTC: http://localhost:8889/camera
OBS → Restream to Multiple Destinations
Receive RTMP from OBS and push to YouTube + Twitch simultaneously:
yaml
paths:
live:
runOnReady: >
ffmpeg -re -i rtsp://localhost:8554/$MTX_PATH
-c copy -f flv rtmp://a.rtmp.youtube.com/live2/YOUR_KEY
-c copy -f flv rtmp://live.twitch.tv/app/YOUR_KEY
runOnReadyRestart: yes
Recording
yaml
pathDefaults:
record: yes
recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f
recordFormat: fmp4 # fmp4 | mpegts
recordSegmentDuration: 1h
recordDeleteAfter: 168h # keep 7 days; 0s = keep forever
runOnRecordSegmentComplete: notify-script.sh $MTX_SEGMENT_PATH
On-Demand FFmpeg Source
Spin up an ffmpeg process only when a reader connects:
yaml
paths:
desktop:
runOnDemand: ffmpeg -f x11grab -i :0 -f rtsp rtsp://localhost:8554/$MTX_PATH
runOnDemandRestart: yes
runOnDemandStartTimeout: 10s
runOnDemandCloseAfter: 10s
Raspberry Pi Camera
yaml
paths:
picam:
source: rpiCamera
rpiCameraWidth: 1920
rpiCameraHeight: 1080
rpiCameraFPS: 30
rpiCameraCodec: hardwareH264
rpiCameraAfMode: continuous
Redirect Path
Point one path to another:
yaml
paths:
main:
source: redirect
sourceRedirect: rtsp://localhost:8554/camera
Publish via ffmpeg / GStreamer
sh
# Publish a test pattern
ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \
-c:v libx264 -preset ultrafast -f rtsp rtsp://localhost:8554/test
# Publish a file on loop
ffmpeg -re -stream_loop -1 -i video.mp4 \
-c copy -f rtsp rtsp://localhost:8554/loop
# Publish webcam
ffmpeg -f v4l2 -i /dev/video0 -c:v libx264 -f rtsp rtsp://localhost:8554/cam
# GStreamer publish
gst-launch-1.0 videotestsrc ! x264enc ! rtspclientsink location=rtsp://localhost:8554/test
Read / Consume
sh
# VLC
vlc rtsp://localhost:8554/mystream
# ffplay
ffplay rtsp://localhost:8554/mystream
ffplay http://localhost:8888/mystream/index.m3u8
# ffmpeg record
ffmpeg -i rtsp://localhost:8554/mystream -c copy output.mp4
# Browser (HLS)
open http://localhost:8888/mystream
# Browser (WebRTC) — built-in player
open http://localhost:8889/mystream
Frigate / Home Assistant Integration
mediamtx as RTSP proxy for Frigate (avoids pulling stream per detector):
yaml
paths:
doorbell:
source: rtsp://admin:pass@192.168.1.50:554/ch0
sourceOnDemand: no # keep always connected for low latency
In Frigate config:
yaml
cameras:
doorbell:
ffmpeg:
inputs:
- path: rtsp://mediamtx:8554/doorbell
roles: [detect, record]
Authentication
Internal Users
yaml
authMethod: internal
authInternalUsers:
- user: publisher
pass: secret123
permissions:
- action: publish
path: live
- user: viewer
pass: viewpass
permissions:
- action: read
path: "" # any path
- user: any # anonymous
permissions:
- action: read
path: public
- user: any
ips: ["127.0.0.1", "::1"]
permissions:
- action: api
- action: metrics
HTTP Webhook Auth
yaml
authMethod: http
authHTTPAddress: http://auth-server/authenticate
authHTTPExclude:
- action: api
- action: metrics
The server POSTs to the URL with { "user", "password", "ip", "action", "path", "protocol", "id", "query" }. Return 2xx to allow.
JWT Auth
yaml
authMethod: jwt
authJWTJWKS: https://identity-server/.well-known/jwks.json
authJWTClaimKey: mediamtx_permissions
JWT must contain mediamtx_permissions: [{ "action": "publish", "path": "..." }].
Control API
Enable to manage paths and sessions at runtime:
yaml
api: yes
apiAddress: :9997
sh
# List paths
curl http://localhost:9997/v3/paths/list
# List active connections
curl http://localhost:9997/v3/rtspconns/list
curl http://localhost:9997/v3/webrtcsessions/list
# Kick a session
curl -X DELETE http://localhost:9997/v3/rtspconns/kick/SESSION_ID
# Add a path at runtime
curl -X POST http://localhost:9997/v3/config/paths/add/newpath \
-H "Content-Type: application/json" \
-d '{"source": "rtsp://camera/stream"}'
# Remove a path at runtime
curl -X DELETE http://localhost:9997/v3/config/paths/delete/newpath
Metrics
Prometheus-compatible endpoint:
yaml
metrics: yes
metricsAddress: :9998
sh
curl http://localhost:9998/metrics
Hooks (Environment Variables)
All hooks receive these env vars:
| Hook | Variables |
|---|---|
runOnConnect / runOnDisconnect | MTX_CONN_TYPE, MTX_CONN_ID, RTSP_PORT |
runOnInit | MTX_PATH, RTSP_PORT, G1..Gn |
runOnDemand / runOnUnDemand | MTX_PATH, MTX_QUERY, RTSP_PORT |
runOnReady / runOnNotReady | MTX_PATH, MTX_QUERY, MTX_SOURCE_TYPE, MTX_SOURCE_ID |
runOnRead / runOnUnread | MTX_PATH, MTX_QUERY, MTX_READER_TYPE, MTX_READER_ID |
runOnRecordSegmentCreate | MTX_PATH, MTX_SEGMENT_PATH |
runOnRecordSegmentComplete | MTX_PATH, MTX_SEGMENT_PATH, MTX_SEGMENT_DURATION |
Path names that are regex (prefixed with ~) expose capture groups as G1, G2, etc.
Path Wildcards / Regex
yaml
paths:
# Static path
camera1:
source: rtsp://192.168.1.10/stream
# Regex: match cam1, cam2, cam3 ... and route to their IPs
"~^cam([0-9]+)$":
source: rtsp://192.168.1.$G1/stream
# Catch-all for unmatched paths
all_others:
source: publisher
Full Configuration Reference
yaml
###############################################
# Global settings
logLevel: info # error | warn | info | debug
logDestinations: [stdout] # stdout | file | syslog
logFile: mediamtx.log
readTimeout: 10s
writeTimeout: 10s
writeQueueSize: 512
udpMaxPayloadSize: 1472
runOnConnect:
runOnConnectRestart: no
runOnDisconnect:
###############################################
# Authentication
authMethod: internal # internal | http | jwt
authInternalUsers:
- user: any
pass:
ips: []
permissions:
- action: publish
path:
- action: read
path:
- action: playback
path:
- user: any
pass:
ips: ["127.0.0.1", "::1"]
permissions:
- action: api
- action: metrics
- action: pprof
authHTTPAddress:
authHTTPExclude:
- action: api
- action: metrics
- action: pprof
authJWTJWKS:
authJWTClaimKey: mediamtx_permissions
###############################################
# Control API
api: no
apiAddress: :9997
apiEncryption: no
apiServerKey: server.key
apiServerCert: server.crt
apiAllowOrigin: "*"
apiTrustedProxies: []
###############################################
# Metrics (Prometheus)
metrics: no
metricsAddress: :9998
metricsEncryption: no
metricsServerKey: server.key
metricsServerCert: server.crt
metricsAllowOrigin: "*"
metricsTrustedProxies: []
###############################################
# Playback server
playback: no
playbackAddress: :9996
playbackEncryption: no
playbackServerKey: server.key
playbackServerCert: server.crt
playbackAllowOrigin: "*"
playbackTrustedProxies: []
###############################################
# RTSP server
rtsp: yes
protocols: [udp, multicast, tcp]
encryption: "no" # no | strict | optional
rtspAddress: :8554
rtspsAddress: :8322
rtpAddress: :8000
rtcpAddress: :8001
multicastIPRange: 224.1.0.0/16
multicastRTPPort: 8002
multicastRTCPPort: 8003
serverKey: server.key
serverCert: server.crt
rtspAuthMethods: [basic]
###############################################
# RTMP server
rtmp: yes
rtmpAddress: :1935
rtmpEncryption: "no"
rtmpsAddress: :1936
rtmpServerKey: server.key
rtmpServerCert: server.crt
###############################################
# HLS server
hls: yes
hlsAddress: :8888
hlsEncryption: no
hlsServerKey: server.key
hlsServerCert: server.crt
hlsAllowOrigin: "*"
hlsTrustedProxies: []
hlsAlwaysRemux: no # pre-generate HLS even without readers
hlsVariant: lowLatency # mpegts | fmp4 | lowLatency
hlsSegmentCount: 7
hlsSegmentDuration: 1s
hlsPartDuration: 200ms
hlsSegmentMaxSize: 50M
hlsDirectory: "" # empty = RAM; set path to use disk
hlsMuxerCloseAfter: 60s
###############################################
# WebRTC server
webrtc: yes
webrtcAddress: :8889
webrtcEncryption: no
webrtcServerKey: server.key
webrtcServerCert: server.crt
webrtcAllowOrigin: "*"
webrtcTrustedProxies: []
webrtcLocalUDPAddress: :8189
webrtcLocalTCPAddress: ""
webrtcIPsFromInterfaces: yes
webrtcIPsFromInterfacesList: []
webrtcAdditionalHosts: []
webrtcICEServers2: []
# - url: stun:stun.l.google.com:19302
# username: ""
# password: ""
webrtcHandshakeTimeout: 10s
webrtcTrackGatherTimeout: 2s
###############################################
# SRT server
srt: yes
srtAddress: :8890
###############################################
# Default path settings (apply to all paths unless overridden)
pathDefaults:
# Source types:
# publisher — client pushes to this path
# rtsp://... — pull from RTSP source
# rtmp://... — pull from RTMP source
# http://.../.m3u8 — pull from HLS source
# udp://ip:port — receive UDP
# srt://... — pull from SRT source
# whep://... — pull from WebRTC source
# redirect — redirect to sourceRedirect URL
# rpiCamera — Raspberry Pi camera
source: publisher
sourceFingerprint:
sourceOnDemand: no
sourceOnDemandStartTimeout: 10s
sourceOnDemandCloseAfter: 10s
maxReaders: 0
srtReadPassphrase:
fallback:
# Recording
record: no
recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f
recordFormat: fmp4 # fmp4 | mpegts
recordPartDuration: 1s
recordSegmentDuration: 1h
recordDeleteAfter: 24h
# Publisher source
overridePublisher: yes
srtPublishPassphrase:
# RTSP source
rtspTransport: automatic # automatic | udp | multicast | tcp
rtspAnyPort: no
rtspRangeType:
rtspRangeStart:
# Redirect source
sourceRedirect:
# Raspberry Pi Camera source
rpiCameraCamID: 0
rpiCameraWidth: 1920
rpiCameraHeight: 1080
rpiCameraHFlip: false
rpiCameraVFlip: false
rpiCameraBrightness: 0
rpiCameraContrast: 1
rpiCameraSaturation: 1
rpiCameraSharpness: 1
rpiCameraExposure: normal # normal | short | long | custom
rpiCameraAWB: auto # auto | incandescent | tungsten | fluorescent | indoor | daylight | cloudy | custom
rpiCameraAWBGains: [0, 0]
rpiCameraDenoise: "off" # off | cdn_off | cdn_fast | cdn_hq
rpiCameraShutter: 0
rpiCameraMetering: centre # centre | spot | matrix | custom
rpiCameraGain: 0
rpiCameraEV: 0
rpiCameraROI:
rpiCameraHDR: false
rpiCameraTuningFile:
rpiCameraMode:
rpiCameraFPS: 30
rpiCameraAfMode: continuous # auto | manual | continuous
rpiCameraAfRange: normal # normal | macro | full
rpiCameraAfSpeed: normal # normal | fast
rpiCameraLensPosition: 0.0
rpiCameraAfWindow:
rpiCameraFlickerPeriod: 0
rpiCameraTextOverlayEnable: false
rpiCameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX"
rpiCameraCodec: auto # auto | hardwareH264 | softwareH264
rpiCameraIDRPeriod: 60
rpiCameraBitrate: 1000000
rpiCameraProfile: main
rpiCameraLevel: "4.1"
# Hooks
runOnInit:
runOnInitRestart: no
runOnDemand:
runOnDemandRestart: no
runOnDemandStartTimeout: 10s
runOnDemandCloseAfter: 10s
runOnUnDemand:
runOnReady:
runOnReadyRestart: no
runOnNotReady:
runOnRead:
runOnReadRestart: no
runOnUnread:
runOnRecordSegmentCreate:
runOnRecordSegmentComplete:
###############################################
# Per-path overrides
paths:
# my_camera:
# source: rtsp://192.168.1.100/stream
# Catch-all — settings for any path not matched above
all_others: