Kohii 301 - Clone TikTok using Kohii
This tutorial shows you a way to build the full-screen vertical Video player like the TikTok application using the Kohii library.
Source code
Source code: eneim/kohii-tutorial-301
Objective
Our objective is to replicate the Video playback UI of the TikTok application:
- Each video is played in full-screen, and the user can swipe the App vertically to switch to other videos.
- It starts playing the video automatically, if the user touches the video, it will pause/resume the video accordingly.
Requirement
You should go through some basic steps introduced in other tutorials before trying this one. Also, make sure you have the right to use the videos in your application.
Hands-on
Before we start, it's worth noting that in this tutorial, we only replicate the video playback, but not try to implement other features like user profile, following, sharing, etc.
Step 1: Create new project
This step is the same as in this tutorial, but in this project, we will use the Bottom Navigation Activity template of Android Studio:
This is our project after step 1: kohii-tutorial-301/tree/step-1
Step 2: Adding dependencies
We will use ViewPager2 in this tutorial. It is the next version of the ViewPager as we knew, but way better. It supports vertical orientation which the ViewPager doesn't, and it uses RecyclerView internally so it supports both Fragment (via the FragmentStateAdapter) and View (via the RecyclerView Adapter) which gives us a lot of flexibility.
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
+
+ def kohii_version = "1.1.0.2011003"
+ implementation "im.ene.kohii:kohii-core:${kohii_version}"
+ implementation "im.ene.kohii:kohii-exoplayer:${kohii_version}"
+ implementation "com.google.android.exoplayer:exoplayer:2.11.3"
+
+ implementation "androidx.viewpager2:viewpager2:1.0.0"
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
We also have a small update to remove obsolete usage of the ViewModelProviders
Here is our code after step 2: kohii-tutorial-301/tree/step-2
Step 3: Completing basic implementation
We reuse what has been done in the other tutorial to have a basic working implementation: kohii-tutorial-301/tree/step-3
In this step, I have some changes to the way we prepare data to simplify the implementations that don't relate to Kohii usage. You should build your own data source in your application.
Step 4: Optimizing the UX
After step 3, our App looks like this →
As you can see, there are a lot of rooms for improvement:
- If user changes the tab of the Bottom Navigation Bar back and fort, the Fragment is recreated without any saved state. As a result, your list will restart from the first video after user switches the tab. It is not a thing that Kohii can help, but you need to do it by your own implementation.
- It takes a while before the playback starts. It is because by default, the player just starts loading the video when it is shown and plays it when it has enough buffer. Kohii provides you mechanism to customize this behavior. This tutorial will focus on this area.
Another thing we found in the TikTok app is that a video would repeat after it finishes. In this step 4, we will optimize the loading parameters of Kohii to archive 2 objectives:
- Video takes less time to start playing.
- Pre-load videos ahead so that it can start playing even earlier.
- Let the video repeat.
You can make it by 2 steps:
- First step is a setup for the ViewPager2: setting its off screen page limit to be non-default. By doing so, your ViewPager will bind some off-screen pages and keep it alive, but paused. This is the pre-condition for our pre-loading optimization:
pager.offscreenPageLimit = 1
- Let Kohii pre-load the video. At the same time we can set the repeat options like below:
class HomeAdapter(
@@ -21,6 +22,8 @@
val videoItem = VideoItem(position = position)
kohii.setUp(videoItem.videoUrl) {
tag = "${videoItem.videoUrl}+${position}"
+ preload = true
+ repeatMode = Common.REPEAT_MODE_ONE
}.bind(holder.playerView)
}
}
After these steps, we are closer to the target, but the videos still take time for the loading. The next things we do are advance usages of Kohii, but once you are familiar with it, it will be a powerful tool for your need: customizing the Kohii instance and using MemoryMode to improve the playback UX.
By default, Kohii wraps the ExoPlayer implementation with default parameters. These parameters affect how much time it takes a player to load the resource before it starts rendering the video. Depending on the video we play, we can adjust them to meet our requirement. For example, if your videos are high-quality movies (e.g. Netflix videos), you may want to buffer more content before the first rendering, so that it always has enough buffer to keep the playback smooth. In the case of TikTok's videos, they are generally short and light-weight, we can shorten the loading time to start playing the video quickly.
The snippet below shows you how to construct a singleton instance of Kohii that use custom parameters to shorten the loading time by a factor of 10:
val kohii = createKohii(
requireContext(), ExoPlayerConfig(
minBufferMs = DEFAULT_MIN_BUFFER_MS / 10,
maxBufferMs = DEFAULT_MAX_BUFFER_MS / 10,
bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10
)
)
createKohii
is an extension method to help you build a custom instance of Kohii quickly. Kohii recommends you use its instance as a singleton. Because that way, all the resources can be reused and passed around for multiple components. The snippet below is one way to use the Capsule
helper class in the library to build a singleton easily:
// A singleton holder
object KohiiProvider {
private val capsule = Capsule<Kohii, Context>(creator = { context ->
createKohii(context, ExoPlayerConfig.FAST_START)
})
fun get(context: Context): Kohii = capsule.get(context)
}
// Obtain the Kohii singleton later
val kohii = KohiiProvider.get(context)
Now about using MemoryMode, this class is another useful feature of Kohii: It allows the client to decide how much memory it allows the library to use for the playback. At the same time, it follows the system's callback so that regardless of the configuration you made, if the system runs out of memory, Kohii will release the resource it uses to gain back the performance. You need to remember that, using more memory can have better UX, but it also increases GC count, consumes more energy so it may impact your app's performance.
The usage is simple. In this tutorial, we want to allow it to use higher memory than usual, so that the next and previous video can stay in the memory and be able to resume as soon as possible:
- kohii.register(this).addBucket(pager)
+ kohii.register(this, memoryMode = HIGH)
+ .addBucket(pager)
With this setup, you allow more videos to be cached after it is paused. Let's take a look at our app before and after the changes:
Before the optimization | After the optimization |
---|---|
As you can see, the playback performance was significantly improved.
Below is the source code of our project after this step: kohii-tutorial-301/tree/step-4
Step 5: User interaction
This step will add the following behavior to the App: if the user clicks a video while it is playing, the video will be paused, and if it is already paused by another click, it will be resumed. This behavior requires a setup to enable the manual playback controller that is discussed in this tutorial. But unlike that tutorial where we use the PlayerView's PlayerControlView for the playback controller, we only need to handle the click event here. To do so, we need to use a custom Controller
like below:
kohii.setUp(videoItem.videoUrl) {
tag = "${videoItem.videoUrl}+${position}"
preload = true
repeatMode = Common.REPEAT_MODE_ONE
+ controller = object : Controller {
+ override fun kohiiCanStart(): Boolean = true
+ override fun kohiiCanPause(): Boolean = true
+ override fun setupRenderer(playback: Playback, renderer: Any?) {
+ holder.playerContainer.setOnClickListener {
+ val playable = playback.playable ?: return@setOnClickListener
+ if (playable.isPlaying()) {
+ playback.manager.pause(playable)
+ } else {
+ playback.manager.play(playable)
+ }
+ }
+ }
+
+ override fun teardownRenderer(playback: Playback, renderer: Any?) {
+ holder.playerContainer.setOnClickListener(null)
+ }
+ }
}.bind(holder.playerView)
}
}
Here we have another field for the ViewHolder called playerContainer
. The reason is PlayerView may intercept the click event, so we wrap it by a FrameLayout and let this layout to receive the click of the user:
<?xml version="1.0" encoding="utf-8"?>
<!-- Please check the full source code in the repository. -->
<FrameLayout
android:clickable="true"
android:focusable="true"
>
<com.google.android.exoplayer2.ui.PlayerView />
</FrameLayout>
After this step, our App is closer to our objective now ->
And here is the source code of our project at this step: kohii-tutorial-301/tree/step-5
Wrap-up
In this tutorial, we learn how to use Kohii to build a replica of the TikTok's video player. With some optimizations, you can see our app could perform very well. In the real-life scenario, it also relies heavily on your server implementation. So keep in mind that, while Kohii cannot do all the work for you, it can be really useful in the specific scenarios.
If you want to learn more about how to use Kohii, please take a look here:
Last but not least
Kohii is an open source project I build after work. It has been under development for nearly 2 years and I'm improving it everyday. If you are using Kohii and finding it useful for your work, you can send me some help either by staring the repository or/and buying me a cup of coffee at my sponsor page. Your help is always appreciated.
Thanks for using Kohii and happy coding.