Building a production Android media player with Media3
Building a production Android media player means adopting Media3, the current media stack that now contains ExoPlayer, and then solving the problems the getting-started guide skips: audio focus, MediaSession and notification wiring, device-specific codec behaviour, picture-in-picture, and background playback. Media3 gives you a working player in an afternoon. Making it behave like a real app, one that survives a phone call mid-song and a headphone unplug, is where the weeks go. This is written from having just shipped one.
What Media3 actually replaced, and why ExoPlayer knowledge still matters
Media3 is the library that consolidated ExoPlayer and Android's older framework media APIs into one place, so ExoPlayer did not disappear, it moved. If you learned media on Android before Media3, that knowledge still applies; the concepts are the same and the ExoPlayer implementation is now a module you use through the Media3 Player interface.
The practical effect is mostly about imports and mental model. Older articles, sample code, and Stack Overflow answers written for standalone ExoPlayer are still largely correct, but the package names have changed, so you spend the first day translating rather than relearning. The upside of the consolidation is real: one library, one set of components for the player, the session, and the UI, instead of stitching together ExoPlayer and the legacy media framework yourself. The thing to internalise early is that Media3 is not a new playback engine to learn from scratch; it is the same engine with a tidier surface, which means the hard-won ExoPlayer wisdom about buffering, track selection, and adaptive streaming is still exactly what you want.
The architecture decisions that matter before the first line of code
Decide three things before you write any playback code: where the player object lives, how playback state is kept separate from the UI, and whether background playback is a real requirement, because retrofitting any of these later is closer to a rewrite than a refactor. These are architecture choices, not implementation details, and they are the ones teams get wrong by starting with a player embedded in an activity and discovering its limits too late.
The first decision is player lifecycle versus UI lifecycle. A player tied to an activity dies when the activity does, which is fine for a video that should stop when you leave the screen and wrong for audio that should keep going. The second is service versus in-activity: a player that must run in the background, survive configuration changes, or be controlled from a notification belongs in a media session service, not in a screen. The third follows from the first two: if background playback is in scope at all, design for it from the start, because the session, the notification, and the service that hosts them are the backbone everything else hangs off. Getting these three right up front is the difference between adding features smoothly and repeatedly fighting the framework. We treat this decision as the actual start of the project, ahead of any UI.
The problems that eat your weeks
The time sinks in a real media player are almost never playback itself; they are the surrounding behaviours that users notice only when they break. Five recur: audio focus, MediaSession and notification wiring, fragmented device codec behaviour, picture-in-picture, and the gap between local and streaming playback.
Audio focus is the etiquette layer: your player has to duck or pause when a call comes in, a navigation prompt speaks, or another app takes over, and resume correctly afterward. Getting this wrong is the most common way a media app feels broken, and it is invisible until you test with real interruptions. MediaSession and notification wiring is what connects your player to the rest of the system, the lock screen, the notification controls, Bluetooth buttons, and Android Auto; it is fiddly, stateful, and easy to get subtly wrong so that the notification and the player disagree. Device codec fragmentation is the tax of the Android ecosystem: the same file plays perfectly on one manufacturer's phone and stutters or refuses on another, and you only find out by testing on real hardware. Picture-in-picture looks like a small feature and is a whole lifecycle of its own, with its own transitions to handle. And local versus streaming playback are different problems wearing the same interface: buffering, adaptive quality, and error recovery matter enormously for streams and barely at all for local files. Budgeting for these five, rather than for "make it play," is how you avoid the schedule surprise.
How do you test a media player?
You test a media player against interruptions and device variety, not just against "does it play," because the failures that reach users live in the events around playback rather than in playback itself. A player that plays a file cleanly on your desk can still fail the moment a call arrives, a headset is unplugged, the app goes to the background, or it lands on a device whose codecs behave differently.
Concretely, that means a test pass built around real-world disruption: incoming calls and notifications mid-playback, wired and Bluetooth headset connects and disconnects, background and foreground transitions, screen locking and waking, low-memory conditions, and a spread of real devices rather than one emulator. It also means checking that the notification, the session state, and the actual playback never disagree, because that desync is where "it worked on my phone" bugs come from. This is the same discipline our QA team applies to anything with hidden states, and it is worth reading how they think about it in how we test software before you ever see a bug. For a media player, testing is not a final gate; it is the only way to find the class of problems that are structurally invisible to the person who wrote the happy path.
Should you build a custom player or embed a ready one?
Embed a ready-made player when playback is incidental to your product, and build a custom Media3 player when playback is the product or when you need control the packaged options will not give you. This is a genuine fork, not a default, and the honest answer for many apps is to embed.
If your app shows the occasional video or plays a sound and the experience does not need to be special, a ready player or a light wrapper over Media3's default UI is the right call, because you get a maintained, correct player without owning its complexity. You build custom when playback is central, when you need bespoke controls, unusual formats, tight integration with your own background and notification behaviour, or a look that the default UI cannot produce. The table below frames the trade honestly.
| Factor | Embed a ready player | Build custom on Media3 |
|---|---|---|
| Control over UI and behaviour | Limited to what it exposes | Full |
| Time to first working version | Fastest | Slower, front-loaded architecture |
| Ongoing maintenance | Mostly the library's | Yours, including device quirks |
| Best fit | Playback is a feature | Playback is the product |
Neither answer is more sophisticated than the other; forcing a custom build when an embed would do is how teams spend weeks on something a user never notices.
What we would do differently next time
Next time we start from the service-and-MediaSession architecture, budget device testing from week one, and treat audio focus and notifications as first-class features rather than end-of-project polish. Every week we lost, we lost by treating a structural concern as a detail.
The specific lessons are simple to state and easy to skip under deadline pressure. Design the session and background service first, so the UI hangs off a stable core instead of the reverse. Get onto a range of real devices early, because codec and manufacturer differences are not edge cases on Android, they are the terrain. And write the interruption handling, audio focus, and notification sync while the architecture is fresh, not bolted on at the end when every change risks the parts that already work. None of this is exotic; it is the difference between a player that demos well and one that survives contact with real phones and real users. If you are weighing a build like this, our Android development work covers the stack, hiring dedicated Android developers is the fastest way to add the capability, and a scoping conversation is the honest way to size the interruption handling and device coverage before you commit.