
Improving Video Playback with ExoPlayer
Bhavesh Solanki
Sep 8, 20254 min read
In mobile apps, smooth and efficient video playback is crucial for a great user experience, especially in markets with unreliable or costly data connections. At Truecaller, we recently enhanced our video playback experience by integrating ExoPlayer with on-disk caching. This post shares our approach, rationale, and technical implementation for others exploring similar optimizations.
Introduction
In mobile apps, smooth and efficient video playback is crucial for a great user experience, especially in markets with unreliable or costly data connections. At Truecaller, we recently enhanced our video playback experience by integrating ExoPlayer with on-disk caching. This post shares our approach, rationale, and technical implementation for others exploring similar optimizations.
Our goal was twofold:
- Improve playback performance: Start Rate, End Rate, User engagement.
- Optimize data usage: Avoid redundant network requests for the same content.
Problem Statement
Before implementing caching, we noticed the following issues:
- High buffer time for video playback, especially on slower networks.
- Repeated downloads of the same videos within a user session.
- Increased data usage, leading to poor user experience in data-sensitive markets.
These pain points were directly impacting video start, completion rates and engagement.
Solution Overview
We decided to leverage ExoPlayer, a powerful media playback library for Android, which also supports disk-based caching. By integrating its caching infrastructure with our player setup, we achieved:
- Offline-like speed for frequently accessed videos
- Fallback to network only when cache miss occurs
- Automatic cache management with LRU eviction
Architecture
Here's a simplified view of our video playback architecture:

- SimpleCache stores and retrieves cached video segments.
- CacheDataSource decides whether to serve data from cache or fetch from the network.
Implementation
Initialize Cache
val cache = SimpleCache(
File(context.cacheDir, "video_cache"),
LeastRecentlyUsedCacheEvictor(MAX_CACHE_SIZE),
StandaloneDatabaseProvider(context)
)
This sets up a disk-based cache with a maximum size limit.
- LeastRecentlyUsedCacheEvictor automatically removes the least accessed content when space runs out.
- StandaloneDatabaseProvider keeps cache metadata separate for reliability.
Important Notes:
- Instantiate SimpleCache on a background thread to avoid blocking the UI, as it performs file I/O during construction.
- Use a dedicated subdirectory (like "video_cache") instead of sharing cacheDir directly. SimpleCache may delete unknown files in the specified directory if it doesn’t recognize them.

Create a Caching Data Source Factory
val dataSourceFactory = CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(DefaultDataSource.Factory(context))
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
Builds a data source that prefers cached content but falls back to the network if needed. DefaultDataSource.Factory handles multiple URI types seamlessly. The flag ensures playback isn’t interrupted by cache access errors.
Prepare Media Source for Playback
val mediaItem = MediaItem.Builder()
.setUri(url)
.setCustomCacheKey(url.encodeToBase64())
.build()
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(mediaItem)
Creates a media source optimised for progressive streaming formats like MP4/WebM. Using a custom cache key ensures consistent caching and retrieval despite URL variations.

Preload Videos into Cache
Option 1: Preload using CacheWriter (Partial Caching)
fun preloadVideo(context: Context, url: String) {
val cache = getCacheInstance(context)
// Note: This line prevents unnecessary re-caching.
// If the video corresponding to url is already cached (even partially),
// the function returns immediately and skips the caching logic.
if (cache.getCachedSpans(url.encodeToBase64()).isNotEmpty()) return
val dataSource = getDataSourceFactory(context).createDataSource()
val dataSpec = DataSpec.Builder()
.setUri(Uri.parse(url))
.setKey(url.encodeToBase64())
.build()
CacheWriter(dataSource, dataSpec, null).cache()
}
Why use ExoPlayer’s cache()? Here are some key advantages:
- Downloads and stores video data ahead of playback – reducing startup delays and buffering. Useful for preloading popular or initial content.
- Lightweight, no background service needed – ideal for preloading short segments like previews or ads.
Option 2: Preload using ExoPlayer's DownloadManager (Full Download)
val downloadRequest = DownloadRequest.Builder("video-id", Uri.parse(url))
.setCustomCacheKey(url.encodeToBase64())
.build()
downloadManager.addDownload(downloadRequest)
Why use ExoPlayer’s downloadManager?
Handles persistent, full-video downloads. Suitable for offline playback use cases (e.g., courses, long-form content).
When to Choose What?
Depending on your use case, ExoPlayer offers different caching approaches:
- Use CacheWriter for preloading snippets and optimising playback speed.
- Use DownloadManager for full downloads that should persist across sessions or enable offline viewing.
Benefits Observed
After rollout, we observed the following improvements:
- Increase in Start Rate from 16% to 75%(more users starting playback).
- Increase in End Rate (fewer drop-offs).
- Increase in Engagement Rate
- Lower Data Usage: Users no longer re-download the same videos.
What’s Next?
As you build more video-heavy features or ad integrations, caching strategies can be tailored to balance performance and storage. Some directions to explore:
- Smart Cache Eviction: Factor in video popularity, size, and user behaviour rather than relying solely on least-recently-used eviction.
- Dynamic Cache Sizing: Adjust cache size adaptively based on device storage availability.
- Cache Analytics: Track hit/miss ratios to optimise preloading and guide future strategies.
- Preloading with CacheWriter: Experiment with preloading short segments for smoother autoplay experiences.
- Offline Support with DownloadManager: Use ExoPlayer’s DownloadManager for robust offline playback features.
- Performance Benchmarking: Measure playback performance before and after caching to quantify improvements.
Want to go deeper? Check out:

Bhavesh Solanki
Sep 8, 20254 min read