BuildWithMatija
  1. Home
  2. Blog
  3. Tools
  4. Fix Android SRT Stream Drops: ADTS, Buffer & Latency

Fix Android SRT Stream Drops: ADTS, Buffer & Latency

Fix ADTS syncword, BUFFER OVERFLOW, and latency drops; use ABR with RootEncoder and MediaMTX to stabilize SRT

21st May 2026·Updated on:28th May 2026··
Tools
Fix Android SRT Stream Drops: ADTS, Buffer & Latency

📚 Get Practical Development Guides

Join developers getting comprehensive guides, code examples, optimization tips, and time-saving prompts to accelerate their development workflow.

No spam. Unsubscribe anytime.

📄View markdown version
0

Frequently Asked Questions

About the author

Matija Žiberna

Matija Žiberna

Full-stack developer, co-founder

AboutResume

Self-taught full-stack developer sharing lessons from building software and startups.

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Contents

  • The Quick Reference Map
  • The ADTS Syncword Error — Audio Sample Rate Mismatch
  • `closed: unable to decode ADTS: invalid syncword`
  • The Silent Kotlin Bug — Swapped Parameters in RootEncoder
  • SRT Latency and ARQ — Why 120ms Is Almost Always Wrong
  • Choppy stream, massive frame drops at high bitrate
  • Buffer Overflow — Bitrate vs Link Capacity
  • `Error send packet, BUFFER OVERFLOW`
  • Implementing ABR with hasCongestion() and setVideoBitrateOnFly()
  • The MPEG-TS Stream Structure Errors
  • `decode error: initial delimiter not found`
  • `java.lang.IllegalStateException` on stream start
  • Version Alignment — RootEncoder 2.7.2 and Kotlin 2.2.0
  • FAQ
  • Conclusion
On this page:
  • The Quick Reference Map
  • The ADTS Syncword Error — Audio Sample Rate Mismatch
  • The Silent Kotlin Bug — Swapped Parameters in RootEncoder
  • SRT Latency and ARQ — Why 120ms Is Almost Always Wrong
  • Buffer Overflow — Bitrate vs Link Capacity
Build with Matija logo

Build with Matija

Modern websites, content systems, and AI workflows built for long-term growth.

Services

  • Headless CMS Websites
  • Next.js & Headless CMS Advisory
  • AI Systems & Automation
  • Website & Content Audit

Resources

  • Case Studies
  • How I Work
  • Blog
  • CMS Hub
  • E-commerce Hub
  • Dashboard

Headless CMS

  • Payload CMS Developer
  • CMS Migration
  • Multi-Tenant CMS
  • Payload vs Sanity
  • Payload vs WordPress
  • Payload vs Contentful

Get in Touch

Ready to modernize your stack? Let's talk about what you're building.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
BuildWithMatija
Get In Touch

If MediaMTX is dropping your Android SRT stream with errors like unable to decode ADTS: invalid syncword or Error send packet, BUFFER OVERFLOW, the cause is almost always one of three things: wrong audio sample rate for your hardware encoder, SRT latency configured too low for your actual network RTT, or a target bitrate that exceeds what your Wi-Fi link can physically push. Each of these produces a distinct error string in the MediaMTX logs, and each one has a specific fix. This article maps every error to its root cause and walks through the solution — including how to implement adaptive bitrate so the stream recovers automatically when the network gets congested.

I hit all of these while building a multicam live streaming setup with Android phones, RootEncoder, and MediaMTX. The debug log grew to eleven tested configurations before everything stabilized. If you are staring at a disconnect loop right now, this article should get you to a working stream faster than I did.


The Quick Reference Map

Before diving into each failure in detail, here is the full error-to-cause-to-fix table. If you already know your error string, jump straight to the relevant section.

Error / SymptomRoot causeFix
unable to decode ADTS: invalid syncwordAudio sample rate mismatch with hardware encoderSet sample rate to 48000Hz, confirm bitrate parameter order
Error send packet, BUFFER OVERFLOWTarget bitrate exceeds actual Wi-Fi link capacityImplement ABR with hasCongestion() + setVideoBitrateOnFly()
Choppy stream, high frame drops at high bitrateSRT latency buffer too narrow for network RTTSet SRT latency to at least 3–4× RTT (e.g. 2_000_000µs for Wi-Fi)
closed: received unexpected interleaved frameVLC RTSP TCP parser bugSwitch to OBS or ffplay -rtsp_transport tcp for testing
decode error: initial delimiter not foundAudio track declared but not transmitted in MPEG-TSKeep both audio and video streams active
App crash IllegalStateExceptionprepareAudio() never calledAlways call prepareAudio() even when streaming video only
No audio in Chrome WebRTCChrome does not support AAC over WebRTCTest audio via HLS or RTSP — not WebRTC

The ADTS Syncword Error — Audio Sample Rate Mismatch

closed: unable to decode ADTS: invalid syncword

This is the most common disconnect on Samsung hardware. MediaMTX logs this error and immediately drops the client. It looks like a server-side problem but the fault is entirely on the Android encoder side.

AAC audio uses a 12-bit synchronization header (0xFFF) at the start of every frame. When the encoder outputs misaligned audio buffers — because the configured sample rate does not match what the hardware capture pipeline actually runs at — those sync bytes end up in the wrong position. MediaMTX reads the frame, cannot locate the expected header, and terminates the connection.

The Samsung S22 (and most modern Android hardware) runs its native audio capture pipeline at 48000Hz. When you configure 44100Hz, the microphone capture queue runs at a different clock than the encoder expects, producing buffer drift. The fix is straightforward: always configure audio at 48000Hz on Samsung devices.

kotlin
// File: MainActivity.kt
srtStream.prepareAudio(
    sampleRate = 48000,
    isStereo = false,
    bitrate = 128_000
)

Mono (isStereo = false) reduces the data volume per frame and produces cleaner alignment on the capture queue. Start with mono at 128 kbps — you can move to stereo once the stream is stable.


The Silent Kotlin Bug — Swapped Parameters in RootEncoder

This failure deserves its own section because it produces the exact same invalid syncword error as the sample rate mismatch, so it is easy to miss. Kotlin compiles the code successfully. There is no warning. The stream just immediately disconnects.

The prepareAudio signature in RootEncoder is:

kotlin
prepareAudio(sampleRate: Int, isStereo: Boolean, bitrate: Int)

And prepareVideo:

kotlin
prepareVideo(width: Int, height: Int, fps: Int, bitrate: Int)

Both audio parameters after isStereo are Int. If you swap sampleRate and bitrate, the compiler accepts it. The hardware encoder receives a sample rate of 128000Hz (invalid) and a bitrate of 48000bps (effectively nothing). The resulting frames are malformed from the first packet.

kotlin
// File: MainActivity.kt — WRONG (swapped parameters)
srtStream.prepareAudio(128_000, false, 48000)

// File: MainActivity.kt — CORRECT
srtStream.prepareAudio(
    sampleRate = 48000,
    isStereo = false,
    bitrate = 128_000
)

Always use named parameters when calling RootEncoder methods. It costs nothing and eliminates this entire class of bug.

The same problem applies to prepareVideo. The fps and bitrate arguments are both Int and easy to transpose:

kotlin
// File: MainActivity.kt — WRONG
srtStream.prepareVideo(1280, 720, 2_500_000, 30)

// File: MainActivity.kt — CORRECT
srtStream.prepareVideo(
    width = 1280,
    height = 720,
    fps = 30,
    bitrate = 2_500_000
)

SRT Latency and ARQ — Why 120ms Is Almost Always Wrong

Choppy stream, massive frame drops at high bitrate

SRT uses ARQ (Automatic Repeat reQuest) to recover lost packets. When a packet is dropped on the network, the receiver requests a retransmission. The SRT latency buffer defines how long the receiver holds a slot open waiting for that retransmission before it gives up and drops the frame.

The default SRT latency in RootEncoder is 120_000 microseconds — 120ms. On a typical Wi-Fi network with an RTT of around 55ms, a 120ms buffer gives the ARQ mechanism less than two full round trips to recover a lost packet. At high bitrates, where packet loss is more frequent, the buffer empties faster than retransmissions can arrive. The player discards frames and the stream stutters visibly.

The rule of thumb: set SRT latency to at least 3–4× your measured RTT. On Wi-Fi, 2_000_000µs (2 seconds) is a safe default.

kotlin
// File: MainActivity.kt
val srtUrl = "srt://192.168.1.100:8890?streamid=publish:cam1&latency=2000000"
srtStream.connect(srtUrl)

A 2-second latency buffer does not mean 2 seconds of end-to-end delay in all cases — it means the receiver will wait up to 2 seconds for a retransmission before discarding. On a clean local network, actual latency stays well below that ceiling.


Buffer Overflow — Bitrate vs Link Capacity

Error send packet, BUFFER OVERFLOW

Increasing the SRT latency buffer solves the retransmission problem but surfaces a different one. If the encoder is producing 20 Mbps and the Wi-Fi link can only push 5–8 Mbps, the internal send queue in SrtSender fills continuously. Once it hits capacity, RootEncoder throws the overflow exception and aborts the stream.

The latency buffer and the bitrate ceiling are two separate knobs. Widening the latency buffer gives ARQ more time to work. It does nothing about the upstream bandwidth ceiling. Those need to be managed independently.

The correct solution is client-side adaptive bitrate — monitoring queue congestion and scaling the encoder down when the link is saturated.


Implementing ABR with hasCongestion() and setVideoBitrateOnFly()

RootEncoder 2.7.2 exposes two methods that make client-side flow control straightforward:

  • hasCongestion() returns true when the send queue is backing up
  • setVideoBitrateOnFly(bitrate) adjusts the encoder output without restarting the stream

The pattern is a simple polling loop on a background coroutine that steps the bitrate down when congestion is detected, and gradually recovers when the link clears.

kotlin
// File: MainActivity.kt
private val bitrateSteps = listOf(20_000_000, 15_000_000, 12_000_000, 8_000_000, 5_000_000)
private var currentBitrateIndex = 0

private fun startAbrMonitor() {
    CoroutineScope(Dispatchers.IO).launch {
        while (srtStream.isStreaming) {
            delay(3000)
            if (srtStream.getStreamClient().hasCongestion()) {
                if (currentBitrateIndex < bitrateSteps.lastIndex) {
                    currentBitrateIndex++
                    val newBitrate = bitrateSteps[currentBitrateIndex]
                    srtStream.setVideoBitrateOnFly(newBitrate)
                    Log.d("ABR", "Congestion detected — stepping down to $newBitrate bps")
                }
            } else {
                if (currentBitrateIndex > 0) {
                    currentBitrateIndex--
                    val newBitrate = bitrateSteps[currentBitrateIndex]
                    srtStream.setVideoBitrateOnFly(newBitrate)
                    Log.d("ABR", "Link clear — stepping up to $newBitrate bps")
                }
            }
        }
    }
}

Call startAbrMonitor() immediately after the stream connects. The 3-second polling interval gives the encoder time to stabilize between steps — stepping too aggressively in both directions causes visible quality oscillation.

With ABR running, the stream starts at 20 Mbps and steps down automatically if the access point gets saturated. When the link recovers, it climbs back up. The buffer overflow exception stops occurring entirely because the encoder output stays within what the link can sustain.


The MPEG-TS Stream Structure Errors

Two errors relate to the MPEG-TS container structure rather than individual encoder parameters.

decode error: initial delimiter not found

This appears when audio is prepared but not transmitted. In MPEG-TS, the Program Map Table declares which tracks are present. If the PMT declares an audio track that never sends packets, the demuxer receives mismatched timecodes and empty sync buffers. MediaMTX cannot locate frame boundaries and logs this error.

Keep both video and audio streams active. If you genuinely need video-only output, strip the audio track at the MediaMTX or OBS layer rather than suppressing it on the Android side.

java.lang.IllegalStateException on stream start

If you remove the prepareAudio() call entirely, RootEncoder's StreamBase crashes on stream start. The library calls startSources() on all encoders during initialization. AudioEncoder must be initialized — even for video-only configurations. Always call prepareAudio().


Version Alignment — RootEncoder 2.7.2 and Kotlin 2.2.0

If you are upgrading RootEncoder to 2.7.2 for the ABR methods, the Kotlin compiler needs to match. Mismatched metadata versions produce build errors that look unrelated to RootEncoder. Update both together:

kotlin
// File: build.gradle.kts (project level)
plugins {
    kotlin("android") version "2.2.0"
}

// File: build.gradle.kts (app level)
dependencies {
    implementation("com.github.pedroSG94.RootEncoder:library:2.7.2")
}

FAQ

Why does the stream work in OBS but disconnect in VLC?

MediaMTX is configured for RTSP over TCP only. VLC has a known bug with interleaved RTSP/TCP frame parsing — under network latency or buffer gaps, it misreads the stream sync and drops the connection, then prompts for credentials as if it were an auth failure. Use OBS Studio or ffplay -rtsp_transport tcp for testing RTSP streams.

Why is there no audio when playing the stream in Chrome via WebRTC?

Chrome does not support AAC audio over WebRTC. The WebRTC standard requires Opus or G.711. When MediaMTX forwards an AAC stream over WebRTC, Chrome silently discards the audio track. Audio plays correctly over HLS (http://<host>:8888/<path>/index.m3u8) and RTSP in compatible players.

Can I append ?pkt_size=1316 to the SRT URL in RootEncoder?

No. RootEncoder's URL parser tokenizes credentials from the path using colons as delimiters. It does not strip query parameters before parsing, so ?pkt_size=1316 gets appended to the password string. The server rejects the malformed credential. Configure packet size at the MediaMTX server level instead.

What sample rate should I use on non-Samsung Android devices?

48000Hz is the standard hardware capture rate on virtually all modern Android devices. 44100Hz is a software resampling target that introduces buffer drift on most hardware encoders. Default to 48000Hz unless you have a specific reason to do otherwise.

At what SRT latency should I start for a local Wi-Fi setup?

Measure your RTT first with a ping test between the Android device and the MediaMTX server. Multiply by 4 and convert to microseconds. For typical home or venue Wi-Fi with 40–60ms RTT, 2_000_000µs (2 seconds) is a safe starting point.


Conclusion

Most Android SRT stream drops trace back to one of three misconfigurations: audio sample rate set to 44100Hz instead of the hardware-native 48000Hz, SRT latency too narrow to give ARQ enough room to retransmit on a Wi-Fi link, or a fixed high bitrate that exceeds actual upstream capacity. The unable to decode ADTS: invalid syncword error points to the first two. The BUFFER OVERFLOW error points to the third.

The stable configuration for a production setup on a Samsung S22 or similar device: 48000Hz mono audio, named parameters in all prepareAudio and prepareVideo calls, SRT latency at 2_000_000µs, and client-side ABR using hasCongestion() with setVideoBitrateOnFly() to handle network variation automatically.

If you are building toward a full multicam production setup on top of this foundation, the Multicam Live Broadcast Setup with Android Phones, OBS, and MediaMTX guide covers the full architecture — stream routing, OBS scene switching, and CDN output.

Let me know in the comments if you hit an error string that is not covered here, and subscribe for more practical development guides.

Thanks, Matija