67

I'm looking for any example of implementing cache in ExoPlayer.

ExoPlayer has in its library different classes concerning cache and Google explain in this video that we can implement it with the CacheDataSource class, but Google doesn't provide any demo on it. Unfortunately this seems pretty complicated to use, so I'm currently looking for examples (no success on Google).

Does anyone succeed or has any info that would help ? Thanks.

ilansas
  • 5,899
  • 6
  • 21
  • 27
  • read this doc http://developer.android.com/guide/topics/media/exoplayer.html – rogerwar Feb 25 '15 at 15:32
  • Obviously I read it ... No apparently it's not possible to implement it yet. That's too bad ... https://github.com/google/ExoPlayer/issues/57 – ilansas Feb 26 '15 at 08:43
  • Share your code what have you done so far – rogerwar Feb 26 '15 at 10:30
  • Sadly, but currently that cache works with DASH only. https://github.com/google/ExoPlayer/issues/420 – æ-ra-code Mar 11 '16 at 10:32
  • This is actually a pretty good guide to implementing it and it covers both Java and Kotlin - https://faanghut.com/implement-a-cache-for-exoplayer/ – Vishnu Jul 21 '22 at 16:42

11 Answers11

53

Here is the solution for ExoPlayer 2.+

Create a custom cache data source factory

public class CacheDataSourceFactory implements DataSource.Factory {
    private final Context context;
    private final DefaultDataSourceFactory defaultDatasourceFactory;
    private final long maxFileSize, maxCacheSize;

    public CacheDataSourceFactory(Context context, long maxCacheSize, long maxFileSize) {
        super();
        this.context = context;
        this.maxCacheSize = maxCacheSize;
        this.maxFileSize = maxFileSize;
        String userAgent = Util.getUserAgent(context, context.getString(R.string.app_name));
        DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
        defaultDatasourceFactory = new DefaultDataSourceFactory(this.context,
                bandwidthMeter,
                new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter));
    }

    @Override
    public DataSource createDataSource() {
        LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxCacheSize);
        SimpleCache simpleCache = new SimpleCache(new File(context.getCacheDir(), "media"), evictor);
        return new CacheDataSource(simpleCache, defaultDatasourceFactory.createDataSource(),
                new FileDataSource(), new CacheDataSink(simpleCache, maxFileSize),
                CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, null);
    }
}

And the player

BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory =
        new AdaptiveTrackSelection.Factory(bandwidthMeter);
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);

SimpleExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(this, trackSelector);
MediaSource audioSource = new ExtractorMediaSource(Uri.parse(url),
            new CacheDataSourceFactory(context, 100 * 1024 * 1024, 5 * 1024 * 1024), new DefaultExtractorsFactory(), null, null);
exoPlayer.setPlayWhenReady(true); 
exoPlayer.prepare(audioSource);

It works pretty well.

Abbas
  • 208
  • 1
  • 7
Bao Le
  • 16,643
  • 9
  • 65
  • 68
  • 1
    My problem with this solution is that if I use this to cache and plaay multiple videos, multiple players might play the same data (ie it seems that for different uri's the same datastream is returned from cache). This problem does not happen when using the default ExtractorMediaSource. The uri's for the videos are unique – Nino van Hooff Sep 20 '17 at 12:09
  • 18
    Solution to the problem: keep a shared instance of SimpleCache instead of creating it in createDataSource. otherwise multiple Cache objects will write to the same files causing trouble – Nino van Hooff Sep 20 '17 at 15:14
  • thnx.its works but Is there any solution for Encrypt cache? – vishal patel Sep 29 '17 at 08:17
  • Does it support Disk cache ?? or only in memory cache? – IshRoid Nov 24 '17 at 07:46
  • keep a shared instance of SimpleCache instead of creating it in createDataSource. I not understand how will it static can you post your code please. – Ashish Kumawat Feb 16 '18 at 11:15
  • @vishalpatel - you can create a cache that uses any datasource and any datasink object; implementing an encrypting datasource and datasink wouldn't be hard. – Jules Jun 01 '18 at 13:42
  • It's perfect code but how to free/clear the CACHE?!! – Jayesh Rathod Jun 08 '18 at 08:11
  • Would this also be a solution for keeping a local copy of ~50 small mp3 files for some hours? (They will also be available on a LAN server, but only in case a client lost it's cache, since i expect >40 clients and there's another audio broadcast simultaneously.) So my question is basically: Can i use exoplayer2 also for (semi-)persistantly "downloading" a bunch of samples before i use them? I admit that i would at probably need some pseudoCode if above mentioned Nino van Hooff's method is the way to go here… – Melchior Blausand Jun 26 '18 at 06:59
  • @Bao Le Thanks. I am using above code and can see that files are being cached in cache dir but how to use that cache while playing the same video if its cached ? – rupesh Oct 30 '18 at 11:19
  • @NinovanHooff Thank you, this solution also works for looping one cached video. – AloDev Nov 29 '18 at 08:28
  • Thanks @Bao Le Cache working perfectly Only need to change make Singleton of SimpleCache instance. – Mansukh Ahir Jan 29 '19 at 05:15
  • I have implemented this as written modified to use a singleton SimpleCache. What I am seeing is URI's that resolve to the local mobile device ("com.android.providers.media.documents" ) are streamed into the cache. But when a URI points to a different location the video is played just fine but doesn't ultimately wind up in the cache. No errors thrown either (I'm listening for them). Ideas? – tfrysinger Mar 25 '19 at 23:31
  • 2
    @Bao Le, Expected behavior of this implementation should be video playback of cached stream should happen in offline as well right? But I am unable to do playback when network disconnect though it is cached stream. Is it obvious that the video playback will only play in online ? Or did I missed something here? – srikanth Jul 26 '19 at 09:26
40

By default ExoPlayer do not cache media (video, audio, etc...). For example if you want to play an online video file, each time ExoPlayer will open a connection, read data then play it.

Fortunately, it provides us some interfaces and implementation classes to support caching media in our app.

You can write your own cache which implement given interfaces from ExoPlayer. To make it simple I will guide you how to enable cache by using implementation classes.

Step 1: Specify a folder which contains your media files, in Android for smaller cache folder (less than 1MB), you should use getCacheDir, otherwise you can specify your prefer cache folder, getFileDir for example.

Step 2: Specify a size for the cache folder, and policies when the size is reached out. There are 2 APIs

  • NoOpCacheEvictor that doesn't ever evict/remove cache files. Based on location of your cache folder, if it's in internal storage, the folder will be removed when users clear app data or uninstall app.
  • LeastRecentlyUsedCacheEvictor that will evict/remove least recently used cache files first. For example if your cache size is 10MB, when the size is reached out, it will automatically find and remove files which least recently used.

Put it together

val renderersFactory = DefaultRenderersFactory(context.applicationContext)
val trackSelector = DefaultTrackSelector()
val loadControl = DefaultLoadControl()

val player = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl)
player.addListener(this)

// Specify cache folder, my cache folder named media which is inside getCacheDir.
val cacheFolder = File(context.cacheDir, "media")

// Specify cache size and removing policies
val cacheEvictor = LeastRecentlyUsedCacheEvictor(1 * 1024 * 1024) // My cache size will be 1MB and it will automatically remove least recently used files if the size is reached out.

// Build cache
val cache = SimpleCache(cacheFolder, cacheEvictor)

// Build data source factory with cache enabled, if data is available in cache it will return immediately, otherwise it will open a new connection to get the data.
val cacheDataSourceFactory = CacheDataSourceFactory(cache, DefaultHttpDataSourceFactory("ExoplayerDemo"))

val uri = Uri.parse("Put your media url here")
val mediaSource = ExtractorMediaSource.Factory(cacheDataSourceFactory).createMediaSource(uri)

player.prepare(mediaSource)
Son Truong
  • 13,661
  • 5
  • 32
  • 58
  • 1
    The CTOR of ExtractorMediaSource seems deprecated. I think it should be as such instead: `val mediaSource = ExtractorMediaSource.Factory(cacheDataSourceFactory).createMediaSource(uri)` . Can you confirm it should be this way? – android developer Dec 10 '18 at 10:04
  • 1
    Thank you for checking it out. You don't have to let the old answer stay. People don't usually use old versions of SDK on purpose... – android developer Dec 13 '18 at 07:39
  • 1
    Add this line in above method : playerView.setPlayer(player); – Deepak Rajput Aug 02 '19 at 12:34
  • I want to add that when we create LeastRecentlyUsedCacheEvictor we pass in constructor im memory cache size not disk cache size – Valgaal May 17 '20 at 20:38
  • This answer helped me in caching HLS video. Thank you @Son Truong – xrnd Dec 15 '20 at 04:35
12

I answered this similar question here: https://stackoverflow.com/a/58678192/2029134

Basically, I use this library: https://github.com/danikula/AndroidVideoCache To cache file from URL Then put it in ExoPlayer.

Here is the sample code:

String mediaURL = "https://my_cool_vid.com/vi.mp4";
SimpleExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext());
HttpProxyCacheServer proxyServer = HttpProxyCacheServer.Builder(getContext()).maxCacheSize(1024 * 1024 * 1024).build();

String proxyURL = proxyServer.getProxyUrl(mediaURL);


DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getContext(),
                Util.getUserAgent(getContext(), getActivity().getApplicationContext().getPackageName()));


exoPlayer.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory)
                .createMediaSource(Uri.parse(proxyURL)););

Hope that helps.

Dũng Trần Trung
  • 6,198
  • 3
  • 24
  • 20
7

To resolve the problem of multiple videos or processes trying to access the same cache, you need a true Singleton. A reliable way would be to do it this way:

object VideoCache {
    private var sDownloadCache: SimpleCache? = null
    private const val maxCacheSize: Long = 100 * 1024 * 1024

    fun getInstance(context: Context): SimpleCache {
        val evictor = LeastRecentlyUsedCacheEvictor(maxCacheSize)
        if (sDownloadCache == null) sDownloadCache = SimpleCache(File(context.cacheDir, "koko-media"), evictor)
        return sDownloadCache as SimpleCache
    }
}

which you can now use:

private val simpleCache: SimpleCache by lazy {
        VideoCache.getInstance(context)
    }
Otieno Rowland
  • 2,182
  • 1
  • 26
  • 34
  • I know this answer is old but I'd like to say that this is _not_ a true singleton. It's still possible to unnecessarily create two instances of SimpleCache – Zun Nov 19 '22 at 15:48
6

Here is an example which replaces demo data source with OkHttp, default is no cache https://github.com/b95505017/ExoPlayer/commit/ebfdda8e7848a2e2e275f5c0525f614b56ef43a6 https://github.com/b95505017/ExoPlayer/tree/okhttp_http_data_source So, you just need to configure OkHttp cache properly and requests should be cached.

Jacek Tymicki
  • 61
  • 1
  • 6
  • 2
    I got OkHttpDataSource from exoplayer 2.2.0 demo app.Can you please share some links for configuring OkHttp cache too. – Pranesh Sahu Mar 16 '17 at 06:17
4

In addition to Bao Le's answer, here's ready to use Kotlin version of CacheDataSourceFactory that keeps one instance of SimpleCache to solve the problem of multiple Cache objects writing to the same directory.

class CacheDataSourceFactory(private val context: Context,
                                      private val maxCacheSize: Long,
                                      private val maxFileSize: Long) : DataSource.Factory {

    private val defaultDatasourceFactory: DefaultDataSourceFactory
    private val simpleCache: SimpleCache by lazy {
        val evictor = LeastRecentlyUsedCacheEvictor(maxCacheSize)
        SimpleCache(File(context.cacheDir, "media"), evictor)
    }

    init {
        val userAgent = Util.getUserAgent(context, context.packageName)
        val bandwidthMeter = DefaultBandwidthMeter()
        defaultDatasourceFactory = DefaultDataSourceFactory(context,
                bandwidthMeter,
                DefaultHttpDataSourceFactory(userAgent, bandwidthMeter))
    }

    override fun createDataSource(): DataSource {
        return CacheDataSource(simpleCache,
                defaultDatasourceFactory.createDataSource(),
                FileDataSource(),
                CacheDataSink(simpleCache, maxFileSize),
                CacheDataSource.FLAG_BLOCK_ON_CACHE or CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
                null)
    }
}
Dmide
  • 6,422
  • 3
  • 24
  • 31
3

I've implemented it like this in the renderer builder

private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int BUFFER_SEGMENT_COUNT = 160;

final String userAgent = Util.getUserAgent(mContext, appName);
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
final Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);*

Cache cache = new SimpleCache(context.getCacheDir(), new LeastRecentlyUsedCacheEvictor(1024 * 1024 * 10));
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
CacheDataSource cacheDataSource = new CacheDataSource(cache, dataSource, false, false);
ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri
                , cacheDataSource
                , allocator
                , BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE
                , new Mp4Extractor());
S.Prapagorn
  • 621
  • 6
  • 14
  • 3
    This code compiles and runs, but seems not to write any video in the specified cache folder. Is it working for you? Does it play from cache without internet connection? Deeper info would be appreciated. Thanks – voghDev Oct 05 '15 at 11:15
  • Also added this code, but as above, it doesn't look like it is caching anything. Is there something we are missing here ? – Gilad Eshkoli Oct 26 '15 at 07:49
  • 3
    As per https://github.com/google/ExoPlayer/issues/420 this answer is only valid for DASH streams. For MP4 files, OkHttpDataSource seems to be yielding good results (according to the people on that thread). – David Ferrand Apr 22 '16 at 10:15
1

Here's my sample in Kotlin (project available here) :

class MainActivity : AppCompatActivity() {
    private var player: SimpleExoPlayer? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (cache == null) {
            cache = SimpleCache(File(cacheDir, "media"), LeastRecentlyUsedCacheEvictor(MAX_PREVIEW_CACHE_SIZE_IN_BYTES))
        }
        setContentView(R.layout.activity_main)
    }

    override fun onStart() {
        super.onStart()
        playVideo()
    }

    private fun playVideo() {
        player = ExoPlayerFactory.newSimpleInstance(this@MainActivity, DefaultTrackSelector())
        playerView.player = player
        player!!.volume = 1f
        player!!.playWhenReady = true
        player!!.repeatMode = Player.REPEAT_MODE_ALL
        player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/240/big_buck_bunny_240p_20mb.mkv", cache!!)
//        player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv", cache!!)
//        player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv")
//        player!!.playRawVideo(this,R.raw.videoplayback)
    }

    override fun onStop() {
        super.onStop()
        playerView.player = null
        player!!.release()
        player = null
    }

    companion object {
        const val MAX_PREVIEW_CACHE_SIZE_IN_BYTES = 20L * 1024L * 1024L
        var cache: com.google.android.exoplayer2.upstream.cache.Cache? = null

        @JvmStatic
        fun getUserAgent(context: Context): String {
            val packageManager = context.packageManager
            val info = packageManager.getPackageInfo(context.packageName, 0)
            val appName = info.applicationInfo.loadLabel(packageManager).toString()
            return Util.getUserAgent(context, appName)
        }
    }

    fun SimpleExoPlayer.playRawVideo(context: Context, @RawRes rawVideoRes: Int) {
        val dataSpec = DataSpec(RawResourceDataSource.buildRawResourceUri(rawVideoRes))
        val rawResourceDataSource = RawResourceDataSource(context)
        rawResourceDataSource.open(dataSpec)
        val factory: DataSource.Factory = DataSource.Factory { rawResourceDataSource }
        prepare(LoopingMediaSource(ExtractorMediaSource.Factory(factory).createMediaSource(rawResourceDataSource.uri)))
    }

    fun SimpleExoPlayer.playVideoFromUrl(context: Context, url: String, cache: Cache? = null) = playVideoFromUri(context, Uri.parse(url), cache)

    fun SimpleExoPlayer.playVideoFile(context: Context, file: File) = playVideoFromUri(context, Uri.fromFile(file))

    fun SimpleExoPlayer.playVideoFromUri(context: Context, uri: Uri, cache: Cache? = null) {
        val factory = if (cache != null)
            CacheDataSourceFactory(cache, DefaultHttpDataSourceFactory(getUserAgent(context)))
        else
            DefaultDataSourceFactory(context, MainActivity.getUserAgent(context))
        val mediaSource = ExtractorMediaSource.Factory(factory).createMediaSource(uri)
        prepare(mediaSource)
    }
}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
1

Use

  ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(
    new CacheDataSource.Factory()
        .setCache(ApplicationClass.getInstance().simpleCache)
        .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory()
            .setUserAgent("ExoplayerDemo"))
        .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
    ).createMediaSource(MediaItem.fromUri(mediaURI));

playerView.setPlayer(player);
player.setMediaSource(mediaSource);
player.prepare();

I have a good article on this. please read this.Very simple and effective implementation

Cashpi App
  • 11
  • 3
-1

Exoplayer's documentation list's the class DashDownloader and has some example code for that type of source. (Click [Frames] to get back navigation of the documentation. I had to remove it to get the deep link.)

  • 1
    This post doesn't look like an attempt to answer this question. Every post here is expected to be an explicit attempt to *answer* this question; if you have a critique or need a clarification of the question or another answer, you can [post a comment](//stackoverflow.com/help/privileges/comment) (like this one) directly below it. Please remove this answer and create either a comment or a new question. See: [Ask questions, get answers, no distractions](//stackoverflow.com/tour) – Filnor Jun 26 '18 at 06:49
  • For sure i wanted to help answer the original question. AFAIR the mentioned class DashDownloader was my solution to the cache problem since i needed caching of a set of media files completely. Since some people might arrive here for that same reason you may want to take back the downvoting; thanks. – Melchior Blausand Mar 23 '20 at 18:06
-2
SimpleCache simpleCache = new SimpleCache(new File(context.getCacheDir(), "media/"+id), evictor);

Here, id must be unique.

David Buck
  • 3,752
  • 35
  • 31
  • 35