DEV Community

Mason K
Mason K

Posted on

Prove SRT survives packet loss and RTMP doesn't: MediaMTX, ffmpeg, and tc netem on one laptop

TL;DR

We'll run MediaMTX locally, publish a test pattern to it over RTMP and over SRT, pull both back out, then add packet loss to the network path with tc netem. RTMP stalls and reconnects; SRT retransmits inside its latency window and keeps going. Then we'll size that window properly from a real RTT and push it to a relay.

RTMP rides on TCP, so one lost packet blocks everything behind it until it's retransmitted. SRT rides on UDP with its own selective retransmission and a latency budget you choose. Everyone says this; almost nobody has watched it happen. Let's watch it happen.

You need Linux (netem is a Linux qdisc; on macOS run this inside a Linux VM or container with NET_ADMIN), ffmpeg 7.x or 8.x built with libsrt (check with ffmpeg -protocols | grep srt), and MediaMTX. We'll do everything on loopback so there's nothing to deploy.

1. ๐Ÿ› ๏ธ Start MediaMTX with RTMP and SRT ingest

MediaMTX is a single static binary that speaks SRT, RTMP, RTSP, WebRTC, HLS and more, with no dependencies. Grab the latest release for your platform from the GitHub releases page (1.19.x at the time of writing) and run it:

mkdir srt-lab && cd srt-lab
# download + extract mediamtx for your platform, then:
./mediamtx
Enter fullscreen mode Exit fullscreen mode
2026/10/05 10:02:11 INF MediaMTX v1.19.0
2026/10/05 10:02:11 INF configuration loaded from /home/me/srt-lab/mediamtx.yml
2026/10/05 10:02:11 INF [RTSP] listener opened on :8554 (TCP), :8000 (UDP/RTP), :8001 (UDP/RTCP)
2026/10/05 10:02:11 INF [RTMP] listener opened on :1935
2026/10/05 10:02:11 INF [HLS] listener opened on :8888
2026/10/05 10:02:11 INF [WebRTC] listener opened on :8889 (HTTP), :8189 (ICE/UDP)
2026/10/05 10:02:11 INF [SRT] listener opened on :8890 (UDP)
Enter fullscreen mode Exit fullscreen mode

The shipped mediamtx.yml enables both RTMP (:1935) and SRT (:8890) by default. Leave it alone for now.

๐Ÿ’ก Tip: with Docker, docker run --rm --network host bluenviron/mediamtx does the same thing. Host networking matters later, because we'll shape the host's loopback interface.

2. ๐Ÿ“ก Publish the same source over both protocols

We'll use a synthetic source so the test is repeatable and nobody has to find a sample file: testsrc2 with a burned-in timestamp, plus a sine tone. Two publishers, two paths.

# publish_rtmp.sh
ffmpeg -re -f lavfi -i "testsrc2=size=1280x720:rate=30" -f lavfi -i "sine=frequency=440" \
  -vf "drawtext=text='%{pts\:hms}':fontsize=48:fontcolor=white:x=20:y=20:box=1:boxcolor=black@0.6" \
  -c:v libx264 -preset veryfast -tune zerolatency -b:v 3000k -maxrate 3000k -bufsize 6000k -g 60 \
  -c:a aac -b:a 128k \
  -f flv rtmp://127.0.0.1:1935/rtmp_test
Enter fullscreen mode Exit fullscreen mode
# publish_srt.sh
ffmpeg -re -f lavfi -i "testsrc2=size=1280x720:rate=30" -f lavfi -i "sine=frequency=440" \
  -vf "drawtext=text='%{pts\:hms}':fontsize=48:fontcolor=white:x=20:y=20:box=1:boxcolor=black@0.6" \
  -c:v libx264 -preset veryfast -tune zerolatency -b:v 3000k -maxrate 3000k -bufsize 6000k -g 60 \
  -c:a aac -b:a 128k \
  -f mpegts "srt://127.0.0.1:8890?streamid=publish:srt_test&pkt_size=1316&latency=120000"
Enter fullscreen mode Exit fullscreen mode

Three things to notice in the SRT URL:

  • streamid=publish:<path> is how MediaMTX knows this connection is a publisher and which path it feeds. The same server reads with streamid=read:<path>.
  • pkt_size=1316 keeps MPEG-TS packets inside a single UDP datagram (7 ร— 188 bytes). Leave it.
  • latency=120000 is in microseconds. That's 120 ms, the ffmpeg/OBS default, and it's deliberately too small for this experiment. We'll fix it in step 5.

Run both in separate terminals. MediaMTX logs should show both paths ready:

INF [RTMP] [conn 127.0.0.1:51822] is publishing to path 'rtmp_test', 2 tracks (H264, MPEG-4 Audio)
INF [SRT] [conn 127.0.0.1:51823] is publishing to path 'srt_test', 2 tracks (H264, MPEG-4 Audio)
Enter fullscreen mode Exit fullscreen mode

3. ๐Ÿ‘€ Read both back and log frame arrivals

To see what the viewer sees, pull each path back out and log received frames per second. Plain ffmpeg -f null with -progress is enough; no player needed.

# read_rtmp.sh  (reads over RTMP too, so the impaired leg is encoder -> server -> reader)
ffmpeg -hide_banner -i rtmp://127.0.0.1:1935/rtmp_test -f null - -progress pipe:1 2>/dev/null \
  | grep --line-buffered -E "^(frame|drop_frames)=" | paste - - | ts '%H:%M:%S'
Enter fullscreen mode Exit fullscreen mode
# read_srt.sh
ffmpeg -hide_banner -i "srt://127.0.0.1:8890?streamid=read:srt_test&latency=120000" -f null - -progress pipe:1 2>/dev/null \
  | grep --line-buffered -E "^(frame|drop_frames)=" | paste - - | ts '%H:%M:%S'
Enter fullscreen mode Exit fullscreen mode

(ts is from moreutils; apt install moreutils. If you'd rather not, drop the last pipe.)

With no impairment, both tick along at 30 frames per second:

10:05:01 frame=30   drop_frames=0
10:05:02 frame=60   drop_frames=0
10:05:03 frame=90   drop_frames=0
Enter fullscreen mode Exit fullscreen mode

4. ๐Ÿ’ฅ Inject loss with tc netem

netem is the Linux kernel's network emulator. It attaches to an interface's egress and can delay, jitter, reorder and drop packets. We'll put it on loopback, which affects every leg (publisher โ†’ server and server โ†’ reader) in both directions. That's fine for a comparison, because both protocols get the identical impairment; just know that the numbers you see are "loss applied twice".

Start gentle:

sudo tc qdisc add dev lo root netem delay 40ms 10ms loss 2%
Enter fullscreen mode Exit fullscreen mode

That's 40 ms of delay with 10 ms jitter and 2 percent random loss, roughly a bad hotel Wi-Fi. Watch the two reader terminals.

The SRT reader keeps counting, maybe with the odd dropped frame. The RTMP reader starts to hitch: frame counts stall for a second or two, then jump. Now make it realistic. Real loss is bursty, and netem has a state model for that:

sudo tc qdisc change dev lo root netem delay 40ms 10ms loss state 8% 25%
Enter fullscreen mode Exit fullscreen mode

Within a few seconds the RTMP side looks like this:

10:07:14 frame=4110  drop_frames=0
10:07:18 frame=4110  drop_frames=0      <- stalled, TCP waiting on a retransmit
10:07:22 frame=4111  drop_frames=0
Enter fullscreen mode Exit fullscreen mode

Then the counter stops updating entirely. Remove the 2>/dev/null and you'll see why: ffmpeg reports an Input/output error on the RTMP input and exits. The RTMP publisher terminal usually dies around the same time with a broken pipe or a timeout. In OBS this is the moment the status bar goes red and the encoder reconnects.

The SRT reader at 120 ms latency is not happy either, but it's a different kind of unhappy:

10:07:14 frame=4108  drop_frames=14
10:07:15 frame=4131  drop_frames=21
10:07:16 frame=4159  drop_frames=23
Enter fullscreen mode Exit fullscreen mode

It drops frames it couldn't recover in time and keeps going. No stall, no reconnect. Those line values are illustrative of the shape; your counts will differ run to run.

โš ๏ธ Note: netem on lo shapes your own machine's loopback. If you're doing this on a remote box over SSH, do NOT attach to the interface carrying your SSH session; put MediaMTX in a network namespace or use a second interface. And always clean up: sudo tc qdisc del dev lo root.

5. ๐ŸŽ›๏ธ Size the latency window properly

SRT's latency is the budget it's allowed to spend recovering a packet before giving up on it. The OBS project's rule, which matches my experience: at least 2.5 ร— round-trip time, and more if loss comes in bursts. The URL value is in microseconds.

With our 40 ms one-way delay applied in both directions, the RTT is about 80 ms plus jitter, so 2.5ร— is 200 ms. Give it headroom for the bursty model and try 600 ms on both ends (publisher and reader each set their own; the connection negotiates up to the larger):

# publisher
... -f mpegts "srt://127.0.0.1:8890?streamid=publish:srt_test&pkt_size=1316&latency=600000"
# reader
ffmpeg -i "srt://127.0.0.1:8890?streamid=read:srt_test&latency=600000" ...
Enter fullscreen mode Exit fullscreen mode

Re-run the bursty loss. Dropped frames on the SRT reader should fall to near zero while RTMP still dies on schedule. If you still see drops, double the latency; you're trading delay for resilience, and that trade is the whole feature.

For a real deployment, measure instead of guessing:

ping -c 20 ingest.example.com | tail -1
# rtt min/avg/max/mdev = 96.4/101.2/118.9/5.1 ms
Enter fullscreen mode Exit fullscreen mode

2.5 ร— 101 ms โ‰ˆ 250 ms minimum; on a link with bursty loss I'd start at 1 s (latency=1000000) and lower it only if the glass-to-glass delay actually matters for the use case.

6. ๐Ÿ” The relay pattern (because Twitch still wants RTMP)

The big consumer platforms ingest RTMP, not SRT. So in production, SRT covers the bad first mile (venue โ†’ relay) and RTMP covers the good last mile (relay โ†’ platform, from a machine with a solid connection). The OBS wiki documents the ffmpeg version of this:

# on the relay VM, near the platform's ingest
ffmpeg -i "srt://:8890?mode=listener&transtype=live&latency=1000000" \
  -c copy -f flv rtmp://live-xxx.twitch.tv/app/$STREAM_KEY
Enter fullscreen mode Exit fullscreen mode

Or let MediaMTX do it: publish SRT into a path and forward the path to an RTMP destination from mediamtx.yml. In OBS, point Settings > Stream > Custom at srt://relay-host:8890?streamid=publish:live&latency=1000000, key blank. SRT output has been in OBS since 25.0.

Add passphrase=...&pbkeylen=16 to both ends if the relay is on the public internet; SRT encrypts the payload with AES and the passphrase is the shared secret.

7. ๐Ÿงน Clean up

sudo tc qdisc del dev lo root
pkill -f publish_ && pkill mediamtx
Enter fullscreen mode Exit fullscreen mode

Wrapping up

RTMP (TCP) SRT (UDP + ARQ)
Lost packet Blocks everything behind it until retransmitted Only that packet is re-requested
Sustained loss Stall, then disconnect Degrades: late packets are dropped, stream continues
Tuning knob None latency (โ‰ฅ 2.5 ร— RTT, microseconds in URLs)
Platform ingest Twitch, YouTube, etc. Your relay, media servers, managed live APIs that accept SRT
Encryption RTMPS (TLS) Built-in AES via passphrase

What's next

  • Repeat step 4 with loss state 15% 40% and see where SRT's recovery stops keeping up; the OBS wiki quotes roughly 15 percent as the range SRT can recover from. RIST, a sibling protocol, claims higher.
  • Scrape MediaMTX's Prometheus metrics endpoint during the test to graph the two paths side by side instead of eyeballing terminals.
  • If you serve viewers from the same box, add LL-HLS on MediaMTX's :8888 and measure glass-to-glass latency at each SRT latency setting, so the number you pick is a measured trade-off rather than a guess.

Top comments (0)