Hey, I'm Nick, an Android Engineer in Uptech Product Development Studio. Recently, my team has had a lot of challenges on one of Uptech projects. We were trying to display videos, a list of videos, nested videos, and things like that.
Showing a list of videos was the trickiest one. I have spent a lot of time optimizing system resources and correctly handling playback. Unfortunately, searching for solutions on the Internet yielded little positive results, so I had to come up with possible solutions on my own.
In this article, I'll explain how to play multiple videos using the RecyclerView and ExoPlayer. The main points we will consider are:
- handle playback during scrolling;
- handle playback when the application is in the background;
- optimize system resources.
I'll share with you my first-hand experience and hope many Android Engineers will find it helpful.
Why a List of Videos is Better Than Just One Video
That's a reasonable question to ask. Indeed, in most cases, the user pays attention to one video at a time. However, playing multiple videos simultaneously provides a more fluent user experience.
Here's how it works: the user scrolls the feed and instantly sees all the content instead of going from one video to another and waiting for the playback to start.
In our case, we showed a short loop video demonstrating apparel in the marketplace. We discovered that the video provides better UX than just an image because it gives more information about each product. Also, videos show better performance compared to gifs.
The Challenges of Creating a List of Videos
Ok, playing multiple videos creates a better user experience, but it has its challenges and drawbacks.
The device resources are limited
First of all, creating a player instance takes time and device resources. It may slow down our video list and cause less fluent UX. Careless creation of multiple player instances will significantly affect UX and waste device memory and CPU time.
The media codecs are limited
Moreover, each device has limited media codecs responsible for video decoding. The app will crash after all available media codecs run out.
How We Created the List of Video Views: 3 Uptech Tips
To solve all the mentioned challanges, we have used a few tricks:
- Object pool;
- Release unused resources;
- Managing activity lifecycle events.
Below you can see the general scheme of how it works:
We reused player instances to avoid new instantiations every time a user opens the app. It led to better performance and a smoother recycler view UX. The object pool pattern came in handy here.
How it works:
Once the view is attached and ready to play the video, its view holder acquires the player from the pool. And otherwise, if the view is detached, the view holder releases the player.
The number of pool players we can instantiate is limited to prevent app crashes. It’s related to available hardware-accelerated video codecs (which are device-specific).
So we had to implement the limit of the players on the pool level. Nevertheless, there can be situations when all pool players are in use. In this case, each following view holder acquiring player is in a pending state. The pending view holder is added to the waiting queue of the player pool. The user receives the acquired player once the pool releases one of the players. Then the view holder can bind the player to the view and start playback immediately. The process is very well structured.
Release unused resources
There are 2 cases when a player can be released for the sake of saving resources:
- The video view is scrolled out from the recycler view and is no longer visible;
- Container activity is no longer in the foreground.
In these cases, the user doesn’t see video views; thus, corresponding players can be stopped or released. By doing so, we give a chance for another app to use available media codecs.
Now let’s take a look at releasing player resources when activity loses foreground.
Uptech Note: consider multiple window support starting from API version 24. The app can be visible in the split window mode but not active, so we should initialize/release the player in onStart/onStop correspondingly.
Once players are released player.release() we can't add them again. So there is no reason to keep released players in the pool. When the user returns to the app, we should notify view holders and make them play.
restartPlayers() forces view holders to acquire players and start playing.
You can also save the playback position right before releasing the player. So the user can continue from the same place when either the video view attaches to the RecyclerView or the container Activity goes to the foreground.
Handling lifecycle events
The third thing I did was manage lifecycle events. I used a coroutines flow to simplify events handling in the ViewHolder. There are two kinds of events requiring the release of player resources:
- minimizing the app (exiting Activity);
- scrolling view out of the window.
To make things work correctly and avoid memory leaks, these events should be observed in specific CoroutineScopes:
- LifecycleScope is used for releasing/restarting all the players. It’s bound to activities’ onCreate()/onDestroy() lifecycle callbacks, which means all events we emit in onStart(), onResume(), onPause(), onStop() will be handled by ViewHolder;
- VideoScope is specific for each separate ViewHolder. It starts right after the bounded view becomes visible (onViewAttachedToWindow()) and is canceled after the view scrolled out of the screen (onViewDetachedFromScreen()). VideoScope can be instantiated and canceled multiple times for the same view holder during the activity lifetime (it depends on how actively the user scrolls the RecyclerView). This scope is responsible for acquiring players from the pool and start playing videos.
The following ULM diagram demonstrates the project structure:
Bonus: The codecs number for .mp4
The number of codecs for each video format on the device is limited. And they can be of two types: hardware-accelerated and software-accelerated.
Hardware-accelerated codecs are recommended to use. Nowadays, most devices have 16+ codec instances for most formats. But usually, the app doesn't need so many codecs at a time. So you can hardcode player pool size depending on your needs. (e.g., in the sample app, I use 4 player instances). Here is the snippet to find out the codecs number for .mp4.
I hope implementing a list of video views seems easy now. Keep in mind everything will work like a charm if you care about using system resources, handling lifecycle events, and consider system limitations. Visit our GitHub for the full project code.