After, the third time I’ve googled for ExoPlayer buffering and caching solution, and it took me a while to find it, decided to put this code sample here. Hopefully to find it on next time 🙂
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:resize_mode="fit"
android:background="@android:color/black"/>
class ExoPlayerController(private var playWhenReady: Boolean = true
) : LifecycleObserver, Player.EventListener {
...
private val url = "put video url here"
private var currentWindow = 0
private var playbackPosition: Long = 0
private fun initializePlayer() {
val player = ExoPlayerFactory.newSimpleInstance(
context,
DefaultRenderersFactory(context),
DefaultTrackSelector(),
getLoadControl()
)
playerView.player = player
val mediaSource = buildMediaSource(Uri.parse(url))
player?.apply {
playWhenReady = this@ExoPlayerController.playWhenReady
seekTo(currentWindow, playbackPosition)
addListener(this@ExoPlayerController)
prepare(mediaSource, false, false)
}
}
private fun releasePlayer() {
player?.let {
playWhenReady = it.playWhenReady
playbackPosition = it.currentPosition
currentWindow = it.currentWindowIndex
it.release()
player = null
}
}
Buffering control:
// Before changing the values consider reading the following article
// https://blogs.akamai.com/2019/10/enhancing-a-streaming-quality-for-exoplayer---part-2-exoplayers-buffering-strategy-how-to-lower-.html
private fun getLoadControl() : LoadControl {
val builder: DefaultLoadControl.Builder =
DefaultLoadControl.Builder()
builder.setBufferDurationsMs(
50000, // DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
50000, / /*.DEFAULT_MAX_BUFFER_MS,
1000, // *.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
1000 // *.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
)
return builder.createDefaultLoadControl()
}
Build a media source:
private fun buildMediaSource(uri: Uri): MediaSource? {
val cacheDataSourceFactory = CacheDataSourceFactory(
VideoCache.get(context),
DefaultHttpDataSourceFactory("exoplayer-demo")
)
return ExtractorMediaSource.Factory(cacheDataSourceFactory).createMediaSource(uri)
}
Video Cache:
object VideoCache {
private var downloadCache: SimpleCache? = null
private const val maxCacheSize: Long = 20 * 1024 * 1024
private const val dirName: String = "media"
fun get(context : Context) : SimpleCache{
if (downloadCache == null) {
val cacheFolder = File(context.cacheDir, dirName)
val cacheEvictor = LeastRecentlyUsedCacheEvictor(maxCacheSize)
downloadCache = SimpleCache(cacheFolder, cacheEvictor)
}
return downloadCache as SimpleCache
}
}
Life Cycle Events:
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
if (Util.SDK_INT >= 24) {
initializePlayer()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
if ((Util.SDK_INT < 24 || player == null)) {
initializePlayer()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
if (Util.SDK_INT < 24) {
releasePlayer()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
if (Util.SDK_INT >= 24) {
releasePlayer()
}
}
Player.EventListener
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
when (playbackState) {
Player.STATE_READY -> {
//Log.d("ZAQ", "STATE_READY")
if (player?.playWhenReady == true) onPlayerPlaying()
}
//Player.STATE_BUFFERING -> Log.d("ZAQ", "STATE_BUFFERING")
//Player.STATE_ENDED -> Log.d("ZAQ", "STATE_ENDED")
//Player.STATE_IDLE -> Log.d("ZAQ", "STATE_IDLE")
}
}
private fun onPlayerPlaying() {
// Do something
}