05 กันยายน 2568
In today's media-centric apps, delivering a smooth, uninterrupted playback experience is key to a delightful user experience. Users expect their videos to start instantly and play seamlessly without pauses.
The core challenge is latency. Traditionally, a video player only starts its work—connecting, downloading, parsing, buffering—after the user has chosen an item for playback. This reactive approach is slow for today's short form video context. The solution is to be proactive. We need to anticipate what the user will watch next and get the content ready ahead of time. This is the essence of preloading.
The key benefits of preloading include:
In this three-part series, we'll introduce and deep dive into Media3’s powerful utilities for (pre)loading components.
The core idea behind preloading is simple: load media content before you need it. By the time a user swipes to the next video, the first segments of the video are already downloaded and available, ready for immediate playback.
Think of it like a restaurant. A busy kitchen doesn't wait for an order to start chopping onions. 🧅 They do their prep work in advance. Preloading is the prep work for your video player.
When enabled, preloading can help minimize join latency when a user skips to the next item before the playback buffer reaches the next item. The first period of the next window is prepared and video, audio and text samples are buffered. The preloaded period is later queued into the player with buffered samples immediately available and ready to be fed to the codec for rendering.
In Media3 there are two primary APIs for preloading, each suited for different use cases. Choosing the right API is the first step.
This is the simple approach, useful for linear, sequential media like playlists where the playback order is predictable (like a series of episodes). You give the player the full list of media items using ExoPlayer's playlist APIs and set the PreloadConfiguration for the player, then it automatically preloads the next items in the sequence as configured. This API attempts to optimize the join latency when a user skips to the next item before the playback buffer already overlaps into the next item.
Preloading is only started when no media is being loaded for the ongoing playback, which prevents it from competing for bandwidth with the primary playback.
If you’re still not sure whether you need preloading, this API is a great low-lift option to try it out!
player.preloadConfiguration = PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)
With the PreloadConfiguration above, the player tries to preload five seconds of media for the next item in the playlist.
Once opted-in, playlist preloading can be turned off again by using PreloadConfiguration.DEFAULT to disable playlist preloading:
player.preloadConfiguration = PreloadConfiguration.DEFAULT
For dynamic UIs like vertical feeds or carousels, where the "next" item is determined by user interaction, the PreloadManager API is appropriate. This is a new powerful, standalone component within the Media3 ExoPlayer library specifically designed to proactively preload. It manages a collection of potential MediaSources, prioritizing them based on proximity to the user's current position and offers granular control over what to preload, suitable for complex scenarios like dynamic feeds of short form videos.
The DefaultPreloadManager is the canonical implementation for PreloadManager.
The builder of DefaultPreloadManager can build both the DefaultPreloadManager and any ExoPlayer instances that will play its preloaded content. To create a DefaultPreloadManager, you will need to pass a TargetPreloadStatusControl, which the preload manager can query to find out how much to load for an item. We will explain and define an example of TargetPreloadStatusControl in the section below.
val preloadManagerBuilder = DefaultPreloadManager.Builder(context, targetPreloadStatusControl) val preloadManager = val preloadManagerBuilder.build() // Build ExoPlayer with DefaultPreloadManager.Builder val player = preloadManagerBuilder.buildExoPlayer()
Using the same builder for both the ExoPlayer and DefaultPreloadManager is necessary, which ensures that the components under the hood of them are correctly shared.
And that's it! You now have a manager ready to receive instructions.
What if you want to preload, say, 10 seconds of video ? You can provide the position of your media items in the carousel, and the DefaultPreloadManager prioritizes loading the items based on how close it is to the item the user is currently playing.
If you want to control how much duration of the item to preload, you can tell that with DefaultPreloadManager.PreloadStatus you return.
For example,
This granular control can help you optimize your resource utilization which is recommended for a seamless playback.
import androidx.media3.exoplayer.DefaultPreloadManager.PreloadStatus class MyTargetPreloadStatusControl( currentPlayingIndex: Int = C.INDEX_UNSET ) : TargetPreloadStatusControl<Int,PreloadStatus> { // The app is responsible for updating this based on UI state override fun getTargetPreloadStatus(index: Int): PreloadStatus? { val distance = index - currentPlayingIndex // Adjacent items (Next): preload 5 seconds if (distance == 1) { // Return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED and suggest loading // 5000ms from the default start position return PreloadStatus.specifiedRangeLoaded(5000L) } // Adjacent items (Previous): preload 3 seconds else if (distance == -1) { // Return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED //and suggest loading 3000ms from the default start position return PreloadStatus.specifiedRangeLoaded(3000L) } // Items two positions away: just select tracks else if (distance) == 2) { // Return a PreloadStatus that is labelled by STAGE_TRACKS_SELECTED return PreloadStatus.TRACKS_SELECTED } // Items four positions away: just select prepare else if (abs(distance) <= 4) { // Return a PreloadStatus that is labelled by STAGE_SOURCE_PREPARED return PreloadStatus.SOURCE_PREPARED } // All other items are too far away return null } }
Tip: PreloadManager can keep both the previous and next items preloaded, whereas the PreloadConfiguration will only look ahead to the next items.
With your manager created, you can start telling it what to work on. As your user scrolls through a feed, you'll identify the upcoming videos and add them to the manager. The interaction with the PreloadManager is a state-driven conversation between your UI and the preloading engine.
1. Add Media Items
As you populate your feed, you must inform the manager of the media it needs to track. If you are starting, you could add the entire list you want to preload. Subsequently you can keep adding a single item to the list as and when required. You have full control over what items are in the preloading list which means you also have to manage what is added and removed from the manager.
val initialMediaItems = pullMediaItemsFromService(/* count= */ 20) for (index in 0 until initialMediaItems.size) { preloadManager.add( initialMediaItems.get(index),index) ) }
The manager will now start fetching data for this MediaItem in the background.
After adding, tell the manager to re-evaluate its new list (hinting that something has changed like adding/ removing an item, or the user switches to play a new item.)
preloadManager.invalidate()
2. Retrieve and Play an Item
Here comes the main playback logic. When the user decides to play that video, you don't need to create a new MediaSource. Instead, you ask the PreloadManager for the one it has already prepared. You can retrieve the MediaSource from the Preload Manager using the MediaItem.
If the retrieved item from the PreloadManager is null, that means the mediaItem is not preloaded yet or added to the PreloadMamager, so you choose to set the mediaItem directly.
// When a media item is about to display on the screen val mediaSource = preloadManager.getMediaSource(mediaItem) if (mediaSource!= null) { player.setMediaSource(mediaSource) } else { // If mediaSource is null, that mediaItem hasn't been added yet. // So, send it directly to the player. player.setMediaItem(mediaItem) } player.prepare() // When the media item is displaying at the center of the screen player.play()
By preparing the MediaSource retrieved from the PreloadManager, you seamlessly transition from preloading to playback, using the data that's already in memory. This is what makes the start time faster.
3. Keep the current index in sync with the UI
Since our feed / list could be dynamic, it's important to notify the PreloadManager of your current playing index so that it can always prioritize items nearest to your current index for preloading.
preloadManager.setCurrentPlayingIndex(currentIndex) // Need to call invalidate() to update the priorities preloadManager.invalidate()
4. Remove an Item
To keep the manager efficient, you should remove items it no longer needs to track, such as items that are far away from the user's current position.
// When an item is too far from the current playing index preloadManager.remove(mediaItem)
If you need to clear all items at once, you can call preloadManager.reset().
5. Release the Manager
When you no longer need the PreloadManager (e.g., when your UI is destroyed), you must release it to free up its resources. A good place to do this is where you’re already releasing your Player’s resources. It’s recommended to release the manager before the player as the player can continue to play if you don't need any more preloading.
// In your Activity's onDestroy() or Composable's onDispose preloadManager.release()
In the demo below , we see the impact of PreloadManager on the right side which has faster load times, whereas the left side shows the existing experience. You can also view the code sample for the demo. (Bonus: It also displays startup latency for every video)
And that's a wrap for Part 1! You now have the tools to build a dynamic preloading system. You can either use PreloadConfiguration to preload the next item of a playlist in ExoPlayer or set up a DefaultPreloadManager, add and remove items on the fly, configure the target preload status, and correctly retrieve the preloaded content for playback.
In Part 2, we'll go deeper on the DefaultPreloadManager. We'll explore how to listen for preloading events, discuss best practices like using a sliding window to avoid memory issues, and peek under the hood at custom shared components of ExoPlayer and DefaultPreloadManager.
Do you have any feedback to share? We are eager to hear from you.
Stay tuned, and go make your app faster! 🚀