Media3 1.11.0-alpha01 dropped on June 24, 2026, and it's the kind of release that doesn't get a big marketing push but quietly changes how you build video playback on Android. The headline is PlayerPool — an official, library-backed primitive for preloading players in sliding window UIs like vertical feeds — but it's shipping alongside a complete Compose player UI suite, a behavior change that makes dynamic scheduling the default, and a breaking change in AudioSink that will bite you if you haven't upgraded your custom audio pipeline code.
I've spent a lot of time building video-heavy apps — Samachar's TikTok-style news feed, Musist's media browser, HailUp's video streaming — and most of the problems this release addresses are ones I've solved by hand before. Having official APIs for them matters, both for correctness and for the fact that I no longer have to maintain the workarounds myself.
PlayerPool and rememberPooledPlayer — the Vertical Feed Problem, Solved Officially
If you've built a vertical video feed — a TikTok-style scroll, a short-form news reel, a music video browser — you've hit the same wall. Creating a new ExoPlayer instance per item is prohibitively expensive. The initialization cost, the codec allocation, the memory overhead — it all adds up and you feel it as jank the moment the user flicks through quickly. The standard solution is a player pool: pre-initialize two or three players, rotate them as items come into and leave view, and let the next item preload while the current one plays.
In my Samachar feed, I built this manually: a fixed-size array of three players, a Map tracking which player was assigned to which list position, custom logic to steal the least-recently-used player when the pool was exhausted. It worked, but it was roughly 200 lines of state management that lived outside any official lifecycle abstraction, and it was brittle every time I wanted to add a feature like skip-to-next or seek-from-notification.
PlayerPool is the library's answer to that pattern. Paired with rememberPooledPlayer, it integrates directly with Compose composition lifecycle — the pool is hoisted outside the pager, and each page composable borrows a player for as long as it's composed:
// Hoist the pool outside the Pager — shared across all pages
val playerPool = rememberPlayerPool(
playerCount = 3,
playerFactory = { context ->
ExoPlayer.Builder(context).build()
}
)
val pagerState = rememberPagerState(pageCount = { items.size })
VerticalPager(state = pagerState) { page ->
val player = rememberPooledPlayer(
pool = playerPool,
key = items[page].id
)
VideoFeedItem(
mediaItem = items[page].toMediaItem(),
player = player
)
}
When a page leaves composition, rememberPooledPlayer releases its player back to the pool automatically — no manual tracking, no DisposableEffect cleanup boilerplate. The pool uses the key parameter to avoid reallocating the same player to a different item if the same page re-enters composition during a fast scroll reversal.
The preloading behavior matters: PlayerPool pre-prepares players for adjacent items even while the current one is playing, using the DefaultPreloadManager internally. You get the smooth transition feel without wiring up preload state yourself.
For apps already running their own pool implementation: this is worth migrating to even though it's alpha. The lifecycle integration alone is worth the switch, and the internal preload management is better than what most hand-rolled pools do. I'd build on a feature branch, run it through your existing Macrobenchmark frame-timing suite before merging.
The Compose Player UI Suite Is Finally Complete
The Media3 Compose UI story has been fragmentary across the last few releases — individual composables shipping as experimental with no cohesive story for how they fit together. 1.11.0-alpha01 fills in the remaining pieces and publishes an opinionated set of defaults that you can either use as-is or override layer by layer.
PlayerDefaults — controls without the from-scratch build
The new PlayerDefaults object ships with four pre-built control regions: TopControls, CenterControls, BottomControls, and ErrorOverlay. The intent is that you replace exactly the region you need to customize and inherit the rest:
PlayerSurface(player = player) {
PlayerDefaults.TopControls(player = player)
PlayerDefaults.CenterControls(player = player)
PlayerDefaults.BottomControls(
player = player,
// override just the progress slider region
progressSlider = {
MyCustomProgressSlider(player = player)
}
)
PlayerDefaults.ErrorOverlay(player = player)
}
This is genuinely useful. The moment you reach for ErrorOverlay in your current setup, you probably realize there's no default anywhere — it falls to you to observe playbackState, detect STATE_IDLE with an error, and show something to the user. Now there's a sensible default you can drop in and override if needed.
MiniController — the notification-bar replacement
MiniController ships in media3-ui-compose-material3 and gives you a Material 3-styled persistent player bar — the equivalent of what Spotify and YouTube Music show at the bottom of every screen when something is playing. It observes the current session from a MediaController and renders playback state, metadata, and playback controls in a compact horizontal bar. If you're building a music or podcast app where playback persists across screens, this is a significant amount of code you no longer have to write.
State composables for metadata and playlists
The new CurrentMediaItemState / rememberCurrentMediaItemState pair gives you a reactive snapshot of the currently playing item's MediaMetadata — title, artist, artwork URI — that recomposes correctly when tracks change. Similarly, PlaylistState / rememberPlaylistState exposes the full playlist as observable state, useful for building a queue screen without manually subscribing to Player.Listener and syncing into your own MutableStateList.
The ErrorState / rememberErrorState / ErrorText trio rounds this out: ErrorText renders a user-facing description of the current playback error, localized, without you needing to map error codes to strings yourself.
DefaultPreloadManager Gets a SimpleRankingDataComparator
The preload manager has always required you to provide a Comparator<MediaItem> to determine which items to preload first. For the common case — preload the items closest to the current playing position — that comparator was simple to write but annoying boilerplate that every team duplicated.
SimpleRankingDataComparator packages that common logic as a concrete class you subclass. You assign an integer rank to each MediaItem (lower rank = preload first), and the comparator handles the rest. This is particularly useful in feed scenarios where preload priority changes dynamically based on scroll velocity — items a few positions ahead get lower ranks than items far away:
val preloadManager = DefaultPreloadManager(
rankingDataComparator = DefaultPreloadManager.SimpleRankingDataComparator(),
trackSelectorFactory = { DefaultTrackSelector(context) },
mediaSourceFactory = progressiveMediaSourceFactory,
)
// Assign rank based on distance from current playing index
fun updatePreloadRanks(currentIndex: Int) {
items.forEachIndexed { index, item ->
val rank = Math.abs(index - currentIndex)
preloadManager.setRankingData(item, rank)
}
}
Dynamic Scheduling Is Now the Default
Dynamic scheduling changes how the ExoPlayer render loop decides when to wake up. Previously, the player would wake on a fixed timer to check whether any renderer work was ready — even if nothing had changed and no data had arrived. Dynamic scheduling flips this: the player only schedules a wakeup when it actually knows work will be ready, based on buffer estimates and codec state. The result is that idle players — playing but buffered, waiting for user interaction — burn meaningfully less CPU between frames.
In Musist, where multiple prepared-but-paused players sit in the background for gapless transition readiness, this was measurable. I had dynamic scheduling manually enabled via ExoPlayer.Builder.setWakeupListener() in earlier versions. Now it's on by default and you don't need to think about it.
Alongside this, the release adds an experimental ExoPlayer.Builder.enablePerStreamMediaProgression() flag. Instead of advancing all media processing as a single pipeline, this processes each stream (audio, video, text) independently. The goal is smoother behavior when one stream's data arrives ahead of another — a common occurrence in adaptive streaming. It's experimental and gated, but worth flagging for teams hitting desync issues on complex DASH streams.
The Breaking Change: AudioSink.configure Is Now a Data Class
This one will catch you if you have a custom AudioSink implementation or a subclass of DefaultAudioSink. The parameters previously passed individually to AudioSink.configure() are now wrapped in a data class. The call signature changes accordingly:
// Before (1.10.x)
audioSink.configure(
inputFormat = format,
specifiedBufferSize = 0,
outputChannels = null
)
// After (1.11.0-alpha01)
audioSink.configure(
AudioSink.Configuration(
inputFormat = format,
specifiedBufferSize = 0,
outputChannels = null
)
)
Check before upgrading: if you have any class that implements AudioSink directly or overrides configure in a subclass of DefaultAudioSink, the compiler will flag it as an error the moment you bump the dependency version. It's a mechanical change — just wrap the parameters — but it's easy to miss if you're doing a batch BOM bump without reading the release notes.
The payoff for this change is that AudioSink.Configuration can now carry additional context — the release also uses it to forward the Timeline and period UID to the sink, which matters for timeline-aware custom sinks. That wasn't possible before without ugly parameter inflation on the method signature.
Three More Additions Worth Knowing About
FLAG_DISABLE_ARTWORK_METADATA for extractors
MP4, MP3, and FLAC extractors now accept a FLAG_DISABLE_ARTWORK_METADATA flag. When set, the extractor skips loading embedded cover art from the file — useful in scenarios where you're playing a large library and the artwork is either unused (audio-only background playback) or loaded from a separate CDN anyway. Embedded artwork from a 500-item MP3 library can add up to a non-trivial amount of memory. This gives you an explicit opt-out rather than having to strip artwork at ingestion time.
MediaSessionManager for querying active sessions
androidx.media3.session.MediaSessionManager is a new class that lets your app enumerate the active MediaSession instances on the device — your own and, if the user has granted permission, others. This is useful for building media routing UIs or cast-to-device features where you need to know what's currently playing without holding a reference to the session directly. It also makes the default onConnect behavior in MediaSession.Callback more relevant: untrusted controllers now get read-only access by default, which is the right security posture for a system that's queryable by third-party apps.
Ktor datasource module
A new media3-datasource-ktor artifact ships KtorDataSource — a drop-in DataSource implementation backed by Ktor's HTTP client instead of OkHttp. For Kotlin Multiplatform projects or teams already standardized on Ktor for their network stack, this removes the friction of maintaining two HTTP clients. It's particularly relevant as Media3's KMP story continues to expand: common network layer, common datasource, less platform-specific glue.
Should You Upgrade Now?
The honest answer depends on what you're building. For the AudioSink breaking change and the dynamic scheduling default: those are reason enough to plan a bump soon, even if you don't touch any of the new APIs immediately. Dynamic scheduling is a free behavioral improvement. The AudioSink change is mechanical to fix and the sooner you do it the easier — it only gets harder if you let more code accumulate that depends on the old signature.
For PlayerPool and the Compose UI suite: these are alpha. The APIs are real and they work, but alpha in Media3 has historically meant signatures shift before stable. I'd build a proof-of-concept on a feature branch, measure it against your existing implementation in Macrobenchmark, and migrate to production once the beta lands — probably a month or two out based on recent release cadence.
The current stable release for production is 1.10.1, which fixes several race conditions in audio session ID handling and a handful of HLS edge cases that were causing crashes. If you're still on 1.9.x, 1.10.1 is an unambiguous upgrade. If you're on 1.10.0, bump to 1.10.1.
The bigger pattern: Media3 is building out the Compose UI story deliberately and the pieces are finally cohesive. PlayerPool, PlayerDefaults, MiniController, the state composables — these weren't in a single release before. If you've been hand-building your media playback UI layer from ExoPlayer primitives, the next six months are probably the time to evaluate what you can delete in favor of library code.
No comments yet. Be the first to leave one!