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

ProtocolPublishReadDefault 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:

HookVariables
runOnConnect / runOnDisconnectMTX_CONN_TYPE, MTX_CONN_ID, RTSP_PORT
runOnInitMTX_PATH, RTSP_PORT, G1..Gn
runOnDemand / runOnUnDemandMTX_PATH, MTX_QUERY, RTSP_PORT
runOnReady / runOnNotReadyMTX_PATH, MTX_QUERY, MTX_SOURCE_TYPE, MTX_SOURCE_ID
runOnRead / runOnUnreadMTX_PATH, MTX_QUERY, MTX_READER_TYPE, MTX_READER_ID
runOnRecordSegmentCreateMTX_PATH, MTX_SEGMENT_PATH
runOnRecordSegmentCompleteMTX_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: