Two Android Audio Decoders. Same File. Different Answer.
I had an Android app that needed to do two things with the same MP3 file. Play it, and run it through a separate transcription pipeline.
The audio played cleanly the whole way through. The transcription stopped about a third of the way in. No error. No exception. The decoder just stopped returning bytes. Every time, the same place, the same file.
The shape of the bug
Android ships two production-quality MP3 decode paths. They are not interchangeable.
ExoPlayer's Mp3Extractor re-reads the MP3 frame header on every single frame as it walks the stream. Each frame carries its own format metadata, and if the format changes mid-file, the decoder just adapts.
MediaExtractor + MediaCodec, the lower-level path most non-playback code uses, parses the track header once at setDataSource() and locks in (sample rate, channel count) for the life of the session. If a later frame disagrees with what the first frame said, the extractor treats it as the end of the stream. No error returned. The next call to readSampleData() just returns -1.
Both are correct against their specs. They are answering different questions. ExoPlayer is asking "what does this next frame say it is." MediaExtractor is asking "does this next frame match what I committed to."
Why most MP3 files don't trip this
The MP3 spec allows every frame to declare its own sample rate and bitrate independently. In practice, almost every MP3 you have ever played is uniform. One encoder ran once, produced one set of frame-level parameters, applied them consistently to every frame. Both decoders give the same answer because there is nothing to disagree about.
The files that surface this bug are concatenations. A long file stitched together from multiple shorter MP3s that were originally encoded at different settings. The stitch is silent. There is no marker in the file saying "format changes here." The bytes from each source just sit next to each other in the container, each frame still carrying its own original header from whatever encoder produced it.
ExoPlayer reads each header on the way past and keeps going. MediaExtractor reads the first one and stops the moment the stream stops matching it.
Why you don't find this in testing
You can run this app for months on hand-encoded test files and never see the failure. Anything you encode yourself, with any normal toolchain, will be uniform. Anything you download from a single source that re-encoded the content end-to-end will be uniform. Single-path test coverage will never catch it, because the file plays fine through one path and you never compare the outputs of two paths against each other.
The bug needs three things to fire simultaneously: real-world content that happens to be non-uniform, an app that runs two different decode paths against the same file, and an output you can compare between them. Pull any one of the three and the failure is invisible.
A team that only does playback will never see this. A team that only does single-consumer ML processing, but on content the team encoded themselves, will never see this. It needs the combination, and the combination is unusual enough that the bug has been sitting in the standard Android audio stack for the entire lifetime of the platform without anyone visibly tripping over it.
The fix
Stop using MediaExtractor for MP3 entirely.
MP3 frames are easy to walk by hand. Each frame begins with a four-byte header that carries everything you need: sync word, MPEG version, sample rate, bitrate, channel mode. Read the header, compute the frame length, read that many bytes, repeat. The full parser is a few hundred lines of code and has no platform dependencies.
Once you are reading frames yourself, the fix for the format-change case is mechanical. Each frame carries the format it was encoded with. When the format on the next frame differs from the format the current MediaCodec was configured for, drain the codec, release it, build a new one for the new format, feed the new frame. The codec lifecycle gets noisier, but the decoder never silently stops.
The structural cost is small. The win is that the consumer of the decoded audio now sees every frame the file actually contains, regardless of whether the file was uniform or stitched.
The generalizable part
Platform abstractions are correct for the cases they were designed for. The shape of "correct" almost always assumes the input meets some property the platform never bothers to state out loud. For MediaExtractor, that property is "the format declared in the track header is the format every frame uses." It is true for nearly every MP3 the platform was tested against. It is not true for every MP3 that exists.
When two layers of the same platform disagree about a file, neither one is wrong. They are operating under different assumptions about what the file is allowed to look like. The hard part is finding out that the assumptions differ at all, because the platform documentation rarely names them. You usually only find out by running both paths against the same content and watching their outputs drift apart.
Most production audio code is single-path. It picks one decoder and trusts it. The interesting bugs live in the cases where you have to run two paths against the same input. Worth knowing the cases exist, in case you ever end up in one.