Preload and cache for faster startup

Short-form feeds, such as reels and episodes, live or die on how fast the next item starts. Arriving at a cold item costs a TLS handshake, a multivariant playlist fetch, a media playlist fetch, and then the first segment: four sequential round trips before a frame can be decoded. The FastPix Android Player SDK gives you four independent levers to cut that delay. Use as many as fit your feed.

  • Fast start tunes buffering so the first frame appears sooner (on by default).
  • Disk caching stores downloaded segments so re-watches and scroll-backs play from disk.
  • Pre-caching warms upcoming feed items before the user reaches them.
  • Next-item preload warms the next queued item when one player holds a queue.

Before you begin

Make sure you have the following:


Fast start (on by default)

Since 2.1.0, the player installs a tuned LoadControl, so playback begins after 500 ms of buffered media rather than Media3’s 1000 ms. There’s nothing to configure, but you can tune it or opt out with BufferConfig.

1FastPixPlayer.Builder(context)
2 .setBufferConfig(BufferConfig.FEED) // reel-tuned: start at 250 ms, shallow buffer
3 // .setBufferConfig(BufferConfig.MEDIA3_DEFAULT) // restore Media3's stock thresholds
4 .build()
PresetbufferForPlaybackMsAhead bufferUse for
BufferConfig.DEFAULT500 ms50 sGeneral playback (applied automatically)
BufferConfig.FEED250 ms20 sReel and short-form feeds
BufferConfig.MEDIA3_DEFAULT1000 ms50 sRestoring pre-2.1.0 behavior

A shallower ahead-buffer is deliberate for feeds: a user who swipes after three seconds never watches the 50 seconds you downloaded, and that is the user’s mobile data.


Cache segments on disk

Disk caching persists downloaded segments, so a re-watch, or a scroll back to an earlier item, plays from disk. Enable it with CacheConfig.

1FastPixPlayer.Builder(context)
2 .setCacheConfig(CacheConfig.enabled()) // 256 MB, LRU-evicted
3 .build()

The cache is process-wide: every player in the app shares one store, and the first config to open it fixes the location and ceiling for the process. Inspect or reset it with MediaCacheProvider.cachedBytes() and MediaCacheProvider.clear().

Opening the cache reads its index from disk. The first build() with caching on does that work on the calling thread, so open it once at startup from a background thread to keep it off the main thread. Every later call returns the already-open cache.

1// Application.onCreate()
2Executors.newSingleThreadExecutor().execute {
3 MediaCacheProvider.getOrCreate(this, cacheConfig)
4}

By default, HLS playlists are not cached, only the segments beneath them. A live media playlist is rewritten by the origin every few seconds, and serving a cached copy would pin the player to a segment list that no longer exists.

NOTE
If all your content is on-demand, use CacheConfig.forOnDemandFeed(). On FastPix, this is what makes segment caching work at all.

1.setCacheConfig(CacheConfig.forOnDemandFeed()) // caches playlists too, VOD only

FastPix re-signs segment URLs on every media-playlist fetch. The same segment comes back as .../<new-signature-blob>/video_270/1.m4s each time, so once the playlist is re-fetched, the segments beneath it are addressed by URLs that were never cached, and every previously downloaded or pre-warmed byte is unreachable. Caching the playlist pins one set of segment URLs, and the segments under it then hit. With playlists uncached, segment reuse is limited to a single continuous playback, and FastPixPreCacher logs a warning saying so.

Cache keys ignore token, signature, expires, and cdn, so a stream re-requested with a freshly minted token still hits. Content-selecting parameters like maxResolution are always part of the key. Override this with CacheConfig.cacheKeyIgnoredQueryParameters.


Pre-cache upcoming feed items

Pre-caching is the lever that removes the swipe delay. FastPixPreCacher warms the next items into the cache while the user is still watching the current one, using bandwidth that would otherwise sit idle.

1// Once per feed, for example in your ViewModel or Application.
2private val cacheConfig = CacheConfig.forOnDemandFeed()
3private val preCacher = FastPixPreCacher.create(context, cacheConfig)
4
5// Every player in the feed must read from the same cache.
6val player = FastPixPlayer.Builder(context)
7 .setBufferConfig(BufferConfig.FEED)
8 .setCacheConfig(cacheConfig)
9 .setAutoplay(true)
10 .build()
11
12// Whenever the visible page changes, hand over the next few URLs.
13viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
14 override fun onPageSelected(position: Int) {
15 preCacher?.preCache(
16 (position + 1..position + 2)
17 .mapNotNull { videos.getOrNull(it)?.playbackUrl }
18 )
19 }
20})
21
22// When the feed goes away.
23preCacher?.release()

preCache(urls) also cancels any in-flight warm whose URL is not in the list, so a download the user has already swiped past stops competing for bandwidth with the item now on screen. That’s why you pass a window rather than one URL at a time.

To tune the defaults:

1FastPixPreCacher.create(
2 context,
3 cacheConfig,
4 PreCacheConfig(
5 segmentCount = 2, // segments warmed per item
6 maxBytesPerItem = 2 * 1024 * 1024, // hard ceiling per item
7 targetBitrateBps = 1_200_000, // which rendition to warm
8 maxParallelItems = 2,
9 ),
10)

The pre-cacher walks the ladder the way the player will: multivariant playlist, chosen video variant, and the audio rendition that variant points at. That last part matters on FastPix streams, whose ladders are demuxed. The variant’s segments carry video only (video_270/1.m4s), with sound in a separate EXT-X-MEDIA rendition, so warming video alone would still leave a cold round trip for audio. Set includeAudioRendition = false if your streams are muxed.

NOTE
Warming only pays off if playback then picks the rendition you warmed. ABR chooses from measured bandwidth, so on Wi-Fi it may ask for 1080p while the pre-cacher warmed 480p, which is a guaranteed miss. Pin both ends to the same ceiling.

1val capBps = 1_650_000
2FastPixPlayer.Builder(context)
3 .setAbrConfig(
4 AbrConfig(wifiMaxBitrateBps = capBps, cellular5g4gMaxBitrateBps = capBps)
5 )
6 // ...
7FastPixPreCacher.create(context, cacheConfig, PreCacheConfig(targetBitrateBps = capBps))

Cache entries are keyed by full URL, so a FastPix signed URL re-minted with a fresh token is a cache miss. Reuse the same signed URL for the life of its token.


Preload the next queued item

If your feed is one player holding a queue rather than a player per page, ExoPlayer can preload the next item itself with PreloadConfig.

1FastPixPlayer.Builder(context)
2 .setPreloadConfig(PreloadConfig.FEED) // 5 s of the next queued item
3 .build()
4player.setMediaItems(mediaItems, startIndex = 0)

This applies only to media queued with setMediaItems. With a single media item there is no next item and the setting does nothing. Use FastPixPreCacher for the player-per-page shape. The two are complementary and can be enabled together.


Measure the impact

The sample app ships a working reel feed for this purpose: Reel Feed (preload / cache benchmark) on the home screen. It’s a vertical ViewPager2 over the sample streams with a three-player pool and a Turbo toggle that rebuilds the screen either with the 2.1.0 path (FEED buffering, disk cache, and pre-caching) or with pre-2.1.0 behavior (stock Media3 buffering, no cache, no warming). The heads-up display reports the milliseconds from a page becoming current to the player reporting ready, and whether that item was pre-cached.


Frequently asked questions

How do I reduce video startup time on Android?

Start with fast-start buffering, which is on by default in 2.1.0 (playback begins at 500 ms of buffered media). For feeds, add BufferConfig.FEED, enable disk caching with CacheConfig.forOnDemandFeed(), and pre-cache upcoming items with FastPixPreCacher. Together, these remove most of the swipe-to-play delay.

What's the difference between pre-caching and next-item preload?

Use FastPixPreCacher when you have one player per page (a ViewPager2 feed): it warms several upcoming items into the shared disk cache. Use PreloadConfig when one player holds a queue set with setMediaItems: ExoPlayer preloads the next queued item. They’re complementary and can be used together.

Why aren't my cached segments being reused on FastPix?

FastPix re-signs segment URLs on every media-playlist fetch, so with playlists uncached the segments are addressed by new URLs and miss the cache. Use CacheConfig.forOnDemandFeed() to cache the playlist too (VOD only), which pins one set of segment URLs so the segments beneath them hit.

Why does pre-caching sometimes not speed up playback?

Warming only helps if playback then picks the rendition you warmed. ABR selects by measured bandwidth, so it may request 1080p while you warmed 480p. Pin both ends to the same ceiling with AbrConfig and PreCacheConfig(targetBitrateBps = ...).

Does a shallower buffer risk more rebuffering?

A shallower ahead-buffer (as in BufferConfig.FEED) is a deliberate trade-off for feeds: it saves mobile data a user won’t watch if they swipe away. For general-purpose playback, keep BufferConfig.DEFAULT, or restore Media3’s thresholds with BufferConfig.MEDIA3_DEFAULT.


What’s next