When we scroll through Vertical Video on our phones, everything feels effortless. Videos start instantly, scrolling is smooth, and there’s hardly any waiting time. From a user’s point of view, it all feels natural.
Behind the scenes, however, a lot of careful planning goes into making this experience smooth. Videos are large, internet speeds are unpredictable, and users scroll very fast. If an app reacts only after the user swipes, delays and buffering become obvious.
This explains how a Vertical Video experience is designed to feel fast and smooth, and how we can improve the video experience so users can watch Vertical Video seamlessly.
Player Loading Workflow to Improve Performance

A common mistakes made in case of video playback is loading a video player only when the user reaches a video. That’s too late.
In a smooth Vertical Video experience, the app follows a different approach:
While you’re watching one reel, the app is already preparing the next few Vertical Video in the background. The video player is ready, the video is prepared, and everything is set up before you swipe.
So when you swipe to the next reel, it feels instant not because it’s fast, but because the work was already done earlier.
In Android the Vertical Video, is built using a RecyclerView, where each reel appears as a single item on the screen. Every visible media is contained inside a ViewHolder, and the video player is attached directly to that ViewHolder. This approach allows the app to reuse views efficiently as the user scrolls, instead of creating everything from scratch each time.
To make scrolling feel fast and responsive, the RecyclerView is configured with a prefetch count. Prefetching means, even before the user scrolls to the next reel, the system already prepares a few upcoming items in advance. Along with this, we maintain a pool of player instances. The size of this player pool is directly linked to the prefetch count. For example, if the prefetch count is n, the player pool creates (n × 2) + 2 player instances. This ensures that there are always enough players ready to be attached to upcoming Vertical Video without delay.
As the user scrolls through the feed, Vertical Video that move out of the screen are recycled by the RecyclerView. When a ViewHolder is recycled, the video player attached to it is immediately detached and returned back to the player pool instead of being destroyed. This returned player can then be reused for the next reel that comes into view. Because of this reuse mechanism, new Vertical Video can start playing almost instantly, giving the experience a smooth and continuous feel.
Together, this flow of prefetching, player pooling, recycling, and decoder management ensures that users can scroll through Vertical Video seamlessly, without visible loading delays or playback interruptions.
Note: When managing a player pool, it’s important to track the number of decoders currently active on the system, both software and hardware. This ensures the app does not exceed system limits, avoids playback failures, and maintains smooth scrolling in reel-style feeds.

Cache Media to Improve Startup Time
Internet speed is unpredictable and directly affects video startup time. To minimize delays, the app caches portions of videos locally as they are played.
- Media segments are cached locally while a video plays
- Previously viewed Vertical Video can start instantly from cache
- Overlapping segments between Vertical Video are reused instead of re-fetching
This significantly reduces startup delays and buffering.
A single shared SimpleCache instance is used across the app and wrapped with a CacheDataSource.Factory.
/**
* Singleton object to manage a SimpleCache instance for video playback.
*
* This cache stores downloaded media on disk so that videos can start
* faster and reduce repeated network requests.
*/
internal object PlayerCache {
// Single instance of SimpleCache to be reused throughout the app
private var cache: SimpleCache? = null
/**
* Returns the singleton instance of SimpleCache.
* Initializes it if it hasn't been created yet.
*
* @param context Context required to access app cache directory
* @return SimpleCache instance
*/
fun getInstance(context: Context): SimpleCache? {
if (cache == null) {
// Directory inside app cache where media will be stored
val cacheDir = File(context.cacheDir, "reelMedia")
// Database provider used internally by SimpleCache to track cache metadata
val provider = StandaloneDatabaseProvider(context)
// Create SimpleCache with:
// 1. cacheDir: where the media will be stored
// 2. LeastRecentlyUsedCacheEvictor: evicts old files when cache exceeds 50MB
// 3. provider: stores metadata about cached files
cache = SimpleCache(
cacheDir,
LeastRecentlyUsedCacheEvictor(50 * 1024 * 1024), // 50 MB max cache
provider
)
}
return cache
}
}
Configuring the Player
val httpFactory = DefaultHttpDataSource.Factory()
.setAllowCrossProtocolRedirects(true)
val dataSourceFactory = DefaultDataSource.Factory(mContext, httpFactory)
val cacheDataSourceFactory = CacheDataSource.Factory()
.setUpstreamDataSourceFactory(dataSourceFactory)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
PlayerCache.getInstance(mContext)?.let {
cacheDataSourceFactory.setCache(it)
}
mediaSourceFactory = DefaultMediaSourceFactory(cacheDataSourceFactory)
Result
- Faster video startup
- Reduced buffering
- Lower repeated network usage
This approach is especially effective for short-form, scroll-heavy video feeds where users frequently scroll back and forth.
One way to take this further is by using a Preload Manager. This component can preload media well before it becomes visible to the user. By loading the media source itself in advance, the player has less work to do fetching the initial chunks of the video, resulting in even faster and smoother playback.
Handling Multiple Player Instances
Creating a video player is expensive in terms of time and system resources. If a new player were created for every reel, scrolling would quickly become slow and resource-heavy.
To avoid this, the app uses a player pool. A limited number of player instances are created upfront and reused as the user scrolls through the feed.
When a ViewHolder is bound, it requests a player from the pool instead of creating a new one. The player is attached to the view and prepared with the required media source. This reuse significantly improves loading time and overall scrolling performance.
As a reel goes off-screen and its ViewHolder is recycled, the player is released from the view and returned to the pool. The same player can then be reused for the next visible reel with an updated media source.
When the user exits the Vertical Video screen, all players in the pool are fully released, ensuring no unnecessary resources are held.
Limitation of Multiple Players
Using multiple players works well for most videos, but there’s a key limitation with L1 DRM-protected content. Most mobile devices only have one hardware decoder available for L1 DRM playback. If multiple player instances try to play DRM-protected content simultaneously, the extra players will fail with a “decoder not available” error.
For most Vertical Video, media won’t have L1 DRM, so this issue is rare.
- Preload Manager approach: For DRM-protected content prefetch only the media source and attach it to a single player. This avoids simultaneous decoder conflicts but may slightly reduce performance, as the player releases and reinitializes the hardware decoder for each playback.
- Software decoder fallback: Can be used only for non-DRM content to allow multiple players without hardware restrictions.
Player Analytics Handling
Accurate analytics are critical for understanding how users interact with Vertical Video. Tracking based purely on scroll events or player initialization can easily produce misleading results—especially when players are reused.
In a pooled-player architecture, the video player is not recreated for every reel. The same player instance is reused across multiple ViewHolders, with only the media source changing. Because of this, initializing analytics (for example, Mux Analytics) at player creation time can result in incorrect session data, duplicate events, or mismatched playback metrics.
Instead, analytics are driven by visibility, not by player lifecycle.
The app tracks two simple events:
- When a reel becomes fully visible in the viewport
- When the reel leaves the viewport
Analytics are started only when the media is fully visible on screen and stopped immediately when it goes out of view. This ensures that analytics sessions align with actual user attention, not background preparation or recycled views.
A reel is considered watched only when it is completely visible.
This approach keeps analytics accurate and consistent, even when users scroll rapidly or skip multiple videos in quick succession.
Conclusion
At Diagnal, we’ve built vertical video as a reusable solution that can be rolled out across customer apps with minimal effort. On Android, for example, it’s packaged as a module that customers can integrate easily without significant engineering overhead.
Our implementation focuses on player lifecycle management, intelligent preloading, and cache-aware media delivery. This allows teams to plug vertical video into existing feeds without re-architecting their apps. The solution is configurable, scalable, and designed to work across devices, ensuring consistent playback behavior while keeping integration effort low.
With this approach, product teams can focus on shaping the experience and the content, whether it’s UGC or operator-curated content, while all the heavy lifting around playback, buffering, and performance runs quietly in the background.
As mentioned earlier, a smooth Vertical Video experience isn’t achieved by a single trick, it comes from many thoughtful optimisations:
- Preparing videos before the user requests them
- Reusing video players instead of creating new ones
- Caching video data to avoid delays
- Measuring engagement based on what users actually see
All of this happens quietly in the background, making scrolling through Vertical Video feel effortless.







