Search code examples
androidhttp-live-streamingexoplayerexoplayer-media-item

How to pre-cache HLS adaptive stream in ExoPlayer?


I am trying to pre-cache/pre-buffer HLS videos to my app. I used CacheWriter to cache the (.mp4) file, But it was not able to cache segments of HLS video. Basically, I have only URL of the master playlist file which has media playlists of different qualities, and that each media playlist has segments (.ts).

So, I have to cache the master playlist and any one media playlist and then some segments and play the cached media to Exoplayer. how can I cache these? I also visited https://github.com/google/ExoPlayer/issues/9337 But this does not have any example to do so.

This is how I cached .mp4 by CacheWriter

    CacheWriter cacheWriter = new CacheWriter( mCacheDataSource,
                        dataSpec,
                        null,
                        progressListener);

    cacheWriter.cache();

Solution

  • I am answering my own question for further users struggling on it. We can pre-cache pre-cache HLS adaptive stream in ExoPlayer By using HlsDownloader provided by Exoplayer.

    Add this Kotlin class to your project ExoPlayerModule.kt.

    //SitaRam
    package com.example.youtpackagename
    
    import android.content.Context
    import android.util.Log
    import com.google.android.exoplayer2.MediaItem
    import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
    import com.google.android.exoplayer2.source.hls.HlsMediaSource
    import com.google.android.exoplayer2.source.hls.offline.HlsDownloader
    import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
    import com.google.android.exoplayer2.upstream.FileDataSource
    import com.google.android.exoplayer2.upstream.cache.CacheDataSource
    import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
    import com.google.android.exoplayer2.upstream.cache.SimpleCache
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.withContext
    import java.io.File
    import java.util.concurrent.CancellationException
    
    //bytes to be downloaded
    private const val PRE_CACHE_AMOUNT = 2 * 1048576L
    
    class ExoPlayerModule(context: Context) {
        
        private var cronetDataSourceFactory =  DefaultHttpDataSource.Factory()
    
        //StaticMember is class which contains cookie in my case, you can skip cookies and use DefaultHttpDataSource.Factory().
      /*val Cookie = mapOf("Cookie" to StaticMember.getCookie())
       private var cronetDataSourceFactory = if (StaticMember.getCookie() != null) {
           DefaultHttpDataSource.Factory().setDefaultRequestProperties(Cookie)
       }else {
           DefaultHttpDataSource.Factory()
       }*/
    
        private val cacheReadDataSourceFactory = FileDataSource.Factory()
        private var cache = simpleCache.SimpleCache(context)
        private var cacheDataSourceFactory = CacheDataSource.Factory()
            .setCache(cache)
    //        .setCacheWriteDataSinkFactory(cacheSink)
            .setCacheReadDataSourceFactory(cacheReadDataSourceFactory)
            .setUpstreamDataSourceFactory(cronetDataSourceFactory)
            .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
    
        fun isUriCached(uri: String, position: Long = 0): Boolean {
            return cache.isCached(uri, position, PRE_CACHE_AMOUNT)
        }
        
        //updating cookies (if you are using cookies).
       /* fun updateDataSourceFactory(){
            val Cookie = mapOf("Cookie" to StaticMember.getCookie())
            cronetDataSourceFactory = if (StaticMember.getCookie() != null) {
                DefaultHttpDataSource.Factory().setDefaultRequestProperties(Cookie)
            }else {
                DefaultHttpDataSource.Factory()
            }
            cacheDataSourceFactory = CacheDataSource.Factory()
                .setCache(cache)
    //        .setCacheWriteDataSinkFactory(cacheSink)
                .setCacheReadDataSourceFactory(cacheReadDataSourceFactory)
                .setUpstreamDataSourceFactory(cronetDataSourceFactory)
                .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
        }*/
    
        // TODO add the same for mp4. Also they might be a much better option, since they only have
        // single track, so no matter what connection you have - loading can't happen twice
        fun getHlsMediaSource(mediaItem: MediaItem): HlsMediaSource {
            return HlsMediaSource.Factory(cacheDataSourceFactory)
                .setAllowChunklessPreparation(true)
                .createMediaSource(mediaItem)
        }
    
        fun releaseCache() = cache.release()
    
        suspend fun preCacheUri(mediaItem: MediaItem) {
            val downloader = HlsDownloader(mediaItem, cacheDataSourceFactory)
            withContext(Dispatchers.IO) {
                try {
                    downloader.download { _, bytesDownloaded, _ ->
                            if (MainActivity.nextUrl==mediaItem){
                              //  Log.e("bytesCaching", "while: same $mediaItem same")
                            }else {
                              //  Log.e("bytesCaching", "while: $mediaItem")
                                downloader.cancel()
                            }
                        if (bytesDownloaded >= PRE_CACHE_AMOUNT) {
    //                        log("video precached at $percent%")
                            downloader.cancel()
                        }
                    }
                } catch (e: Exception) {
                    if (e !is CancellationException) log("precache exception $e")
                }
            }
        }
    
        private fun log(s: String) {
            TODO("Not yet implemented")
        }
    }
    

    Initializing ExoPlayerModule

    ExoPlayerModule PlayerModuleO = new ExoPlayerModule(MainActivity.this);
    

    For Pre-Loading.

    String previousUrl = "";
        public void preLoad(String url) {
            if (previousUrl.equals(url)) {
                return;
            }
    
            previousUrl = url;
            MediaItem mediaItem =MediaItem.fromUri(Uri.parse(url));
            PlayerModuleO.preCacheUri(mediaItem, new Continuation<>() {
                @NonNull
                @Override
                public CoroutineContext getContext() {
                    return EmptyCoroutineContext.INSTANCE;
                }
    
                @Override
                public void resumeWith(@NonNull Object o) {
    
                }
            });
    
        }
    

    Playing cached or non-cached media.

     MediaItem mediaItem = MediaItem.fromUri(Uri.parse(url));
    
            exoPlayer.setMediaSource(PlayerModuleO.getHlsMediaSource(mediaItem));
            exoPlayer.prepare();
            exoPlayer.play();
    

    Releasing cache

    PlayerModuleO.releaseCache();
    

    If you are having any problems then feel free to ask.