2

I have implemented a custom cache for Exoplayer2 - and am migrating from 2.11.x to 2.12.3. I have reviewed several posts here on SO (ex: here), as well as various parts from the latest Exoplayer2 developer docs (ex: here). I can get the video streamed from a local source or a remote source (ex: Dropbox) and it plays, but I don't see anything being saved in the folder I've designated as my cache other than two files (and both are extremely small):

5eedc471fe18491e.uid
cached_content_index.exi

I am guessing that the .exi file is the index that is supposed to track the cache segments, and the .uid file is a segment - but it is always 0 bytes. So for some reason, Exoplayer recognizes the cache location, but doesn't stream anything into it.

In my app, I do this:

MediaSource mediaSource = getCachedMediaSourceFactory(context, listener).createMediaSource(uri);
exoPlayer.setMediaSource(mediaSource);
exoPlayer.setPlayWhenReady(true);
exoPlayer.prepare();

getCachedMediaSourceFactory() in turn looks like:

private static ProgressiveMediaSource.Factory getCachedMediaSourceFactory(Context context, CacheDataSource.EventListener listener) {
    if ( cachedMediaSourceFactory == null ) {
        cachedMediaSourceFactory = new ProgressiveMediaSource.Factory(
                new InTouchCacheDataSourceFactory(context,
                        DEFAULT_MEDIA_CACHE_FRAGMENT_SIZE_IN_BYTES,
                        listener));
    }
    return cachedMediaSourceFactory;
}

InTouchCacheDataSourceFactory in turn looks like:

public class InTouchCacheDataSourceFactory implements DataSource.Factory {
    private final DefaultDataSourceFactory defaultDatasourceFactory;
    private final CacheDataSource.EventListener listener;
    private final long maxCacheFragmentSize;

    public InTouchCacheDataSourceFactory(Context context, long maxCacheFragmentSize) {
        this(context, maxCacheFragmentSize, null);
    }

    public InTouchCacheDataSourceFactory(Context context, long maxCacheFragmentSize, CacheDataSource.EventListener listener) {
        this.maxCacheFragmentSize = maxCacheFragmentSize;
        this.listener = listener;

        defaultDatasourceFactory =
                new DefaultDataSourceFactory(context,
                                             new DefaultHttpDataSourceFactory(Util.getUserAgent(context, APP_NAME)));

    }

    @Override
    public DataSource createDataSource() {
        return new CacheDataSource(InTouchUtils.getMediaCache(),
                                   defaultDatasourceFactory.createDataSource(),
                                   new FileDataSource(),
                                   new CacheDataSink(InTouchUtils.getMediaCache(), maxCacheFragmentSize),
                             CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
                                   listener);
    }
}

And InTouchUtils.getMediaCache looks like:

public static SimpleCache getMediaCache() {
    if ( mediaCache == null ) {
        Context context = InTouch.getInstance().getApplicationContext();
        long requestedCacheSize = Long.parseLong(PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.pref_default_cache_size_key), String.valueOf(DEFAULT_MEDIA_CACHE_SIZE_IN_MB)));
        File contentDirectory = new File(context.getCacheDir(), MEDIA_CACHE_DIRECTORY);
        mediaCache = new SimpleCache(contentDirectory, new LeastRecentlyUsedCacheEvictor(requestedCacheSize*(1024*1024)));
    }
    return mediaCache;
}

Again, the video plays fine - but it doesn't appear to be actually writing the streamed file into the cache. Why not?

I have also implemented the CacheDataSource.EventListener interface in my app, and have overridden these two methods:

@Override
public void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead) {
    Timber.d("Lifecycle: Cache read, size in bytes is: %d, bytes read was: %d", cacheSizeBytes, cachedBytesRead);
}

@Override
public void onCacheIgnored(int reason) {
    switch (reason) {
        case CacheDataSource.CACHE_IGNORED_REASON_ERROR:
            Timber.d("Lifecycle: Cache ignored due to error");
            break;
        case CacheDataSource.CACHE_IGNORED_REASON_UNSET_LENGTH:
            Timber.d("Lifecycle: Cache ignored due to unset length");
            break;
    }
}

I am not seeing either method getting called.

tfrysinger
  • 1,306
  • 11
  • 26

1 Answers1

2

UPDATE May 2021

I have created a sample app on GitHub that shows this cached data source working, for any readers that are interested to see it.

Tapping the top button in the app launches a SAF-based chooser to select a local video (so no cache is necessary or used). The Uri of the selected file shows in the EditText, and you can tap "Play Video" to see it play.

However by default the sample Uri in the EditText is on the Internet, and as it is "not a local Uri" (the semantics of which are not anything really "official", just a simple delineation I created for the purposes of this test app) it uses the custom cache data source factory which causes Exoplayer's cache mechanics to kick into gear.

You can review the differences in LogCat while the app runs, and can see after playing the video streamed from the internet the first time, on the second run you start getting hits from the cache as evidenced by calls to the onCachedBytesRead() callback.

You can also open up your device explorer while running it on a hardware device connected to Android Studio, and you should see something along these lines:

enter image description here

As you can see, after the first time I played the video, it was cached in the location I specified, and thereafter Exoplayer2 grabs the video from there, not from the Internet again.

For streamed videos, make sure you use a Url like:

https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4.

and NOT something unique such as a YouTube "watch" one, i.e.

https://www.youtube.com/watch?v=Cn8PiqIXEjQ

Also, this sample uses the Exoplayer2 "Progressive" type media source to stream Uri from the Internet (which is now the default one since the Extractor type was deprecated a while back). If a url entered into this app points to a file requiring the DASH type, obviously it won't work.

The Exoplayer2 Source Code has an example app which does a much more sophisticated job of trying the various media source implementations against the target Uri until it finds the right onw.

There is a good basic Exoplayer2 tutorial here.

====

It turns out that this not caching is the default behavior, then in the normal course of DataSource management it is turned on in most cases. In my case it was not getting so because the Uri in question was a content:// Uri pointing to Dropbox that gets resolved by a custom SAF provider which uses Dropbox APIs to stream the video back. So the length of the file never gets recognized, thus the default behavior does not get changed.

Oliver Woodman of the Exoplayer team suggested two solutions. The first (quickest), was to change my custom DataSourceFactory implementation from returning a CacheDataSource, to returning a custom DataSource that "wraps" the CacheDataSource - forwarding the method calls from the DataSource interface to the CacheDataSource instance, except for the open() method. In that case it changes the DataSpec argument first, then passes it along:

/**
 * Class specifically to turn off the flag that would not cache streamed docs.
 */
private class InTouchCacheDataSource implements DataSource {
    private CacheDataSource cacheDataSource;
    InTouchCacheDataSource(CacheDataSource cacheDataSource) {
        this.cacheDataSource = cacheDataSource;
    }

    @Override
    public void addTransferListener(TransferListener transferListener) {
        cacheDataSource.addTransferListener(transferListener);
    }

    @Override
    public long open(DataSpec dataSpec) throws IOException {
        return cacheDataSource.open(dataSpec
                .buildUpon()
                .setFlags(dataSpec.flags & ~DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN)
                .build());
    }

    @Nullable
    @Override
    public Uri getUri() {
        return cacheDataSource.getUri();
    }

    @Override
    public void close() throws IOException {
        cacheDataSource.close();
    }

    @Override
    public int read(byte[] target, int offset, int length) throws IOException {
        return cacheDataSource.read(target, offset, length);
    }
}

This class is then used by the custom factory like this:

@Override
public DataSource createDataSource() {
    CacheDataSource dataSource = new CacheDataSource(InTouchUtils.getMediaCache(),
            defaultDatasourceFactory.createDataSource(),
            new FileDataSource(),
            new CacheDataSink(InTouchUtils.getMediaCache(), maxCacheFragmentSize),
            CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
            listener);
    return new InTouchCacheDataSource(dataSource);
}

This works perfectly, and Oliver assures me that Exoplayer will do the right thing even if the stream were to fill the cache (i.e. first envoke the evictor, then if that fails stop trying to write to the cache and just continue to stream).

The other option is to write a custom DataSource using ContentDataSource as a start, and incorporate the customized SAF into it (since in my case at least, I actually do know the size of the file but the standard SAF methods being used to afford the opportunity to pass this back in the standard SAF provider/resolver approach).

Mirwise Khan
  • 1,317
  • 17
  • 25
tfrysinger
  • 1,306
  • 11
  • 26
  • Could you make a free medium post out of this please – Pemba Tamang Apr 23 '21 at 12:29
  • I am trying to do the same I even copied your code the song is playing but nothing is being cached and even if it does I still don't know how to save the file. – Pemba Tamang Apr 23 '21 at 12:30
  • 1
    Are you also implementing a custom DataSourceFactory? The solution above assumes that case, and then in that factory you return an instance of something like what I wrote above - i.e. a "wrapped" CacheDataSource. You then use that custom DataSourceFactory in your code, something like: – tfrysinger Apr 24 '21 at 15:10
  • 1
    private static ProgressiveMediaSource.Factory getCachedMediaSourceFactory(Context context, CacheDataSource.EventListener listener) { if ( cachedMediaSourceFactory == null ) { cachedMediaSourceFactory = new ProgressiveMediaSource.Factory( new InTouchCacheDataSourceFactory(context, DEFAULT_MEDIA_CACHE_FRAGMENT_SIZE_IN_BYTES, listener)); } return cachedMediaSourceFactory; } – tfrysinger Apr 24 '21 at 15:11
  • `onCachedBytesRead` is not triggered the first time I run the app. Why could that be? – Pemba Tamang Apr 26 '21 at 12:27
  • I want to save the cache as a file when the caching is complete – Pemba Tamang Apr 26 '21 at 12:48
  • 1
    Well, that won't be called until you get your cache working. As to your saving the cache as a file, I don't think that is possible - the cache system is internal to Exoplayer and doesn't "look" like a file. It's a whole bunch of parts that get put together as needed to play the video. – tfrysinger Apr 27 '21 at 19:20
  • I was trying to do that and I was able to save the cache as a file but I am able to either save the cache or play the stream but not both at once. Could you please have a look at this. All I am trying to do is find a way to know if the caching is complete and somehow id the cache to save it properly – Pemba Tamang Apr 28 '21 at 06:33
  • https://stackoverflow.com/questions/67286053/stream-music-and-save-the-cache-after-caching-is-complete-in-exoplayer-using-cac – Pemba Tamang Apr 28 '21 at 06:33
  • And here is my repo where I am saving and caching a single song. I am playing the song the first time and saving it the second time. Still no way to id the cache. https://github.com/PembaTamang/Poc – Pemba Tamang Apr 28 '21 at 06:38
  • 1
    Hi - I downloaded your repo. I'm not a Kotlin developer, so some things in there are a bit foreign to me, but you are doing some things slightly different than me - I don't use download manager for example, and my custom DataSourceFactory is wrapped in a ProgressiveMediaSource.Factory. I'll see if I can work up a full working example and post it. Will likely be a little bit because I'm swamped now, but will try and get to it as quickly as I can. – tfrysinger Apr 28 '21 at 13:57
  • Hey the download manager is incomplete and I am not using it. It would be really really great if you get it to work. Thank you so much for your time and effort. – Pemba Tamang Apr 29 '21 at 04:31
  • 1
    Pemba - I created a sample app - in Java, not Kotlin, sorry! ;) - showing how to create a cached data source for Exoplayer2. It actually shows the difference between playing a local file (no cache needed) and a remote file (cache used). Open up logcat after you play the video the first time and play it again to see the cache getting hit. https://github.com/tfrysinger/ExoPlayerCacheDataSourceSample – tfrysinger May 02 '21 at 22:35
  • 1
    I also edited my answer above with some more details - see at the top. – tfrysinger May 02 '21 at 23:38
  • Thank you so much for your time and effort. It's hard to come across someone who is willing to help like you did. Hey kotlin is the same as java I mean if you know java you know kotlin is what I feel. I just started a project some time ago and chose kotlin instead of java is how I started. – Pemba Tamang May 03 '21 at 05:10
  • Hey @trysinger I am stuck with one thing. After I call clearMediaItems() on exoplayer I add a mediasource and call prepare() but it does not trigger the buffer. What am I missing ? – Pemba Tamang May 03 '21 at 11:49
  • Hard to troubleshoot without seeing code. Did you update your Github project? – tfrysinger May 03 '21 at 12:19
  • I just did. You can have a look at line number 78 here. https://github.com/PembaTamang/Poc/blob/main/ExoPlayerPoc2/app/src/main/java/in_/co/innerpeacetech/exoplayerpoc/SecondActivity.kt – Pemba Tamang May 03 '21 at 13:00
  • I found out that I I need to call stop() before calling clearMediaItems() to reset exoplayer. It's working now in that repo but not not in my main project. Wish I could add you there.Seems like I am missing something. Anyways thanks for the reply man!!!!! – Pemba Tamang May 03 '21 at 13:09
  • Hey man I am kinda embarassed to ask so much of you but could you take a look at this please https://stackoverflow.com/questions/67369657/exoplayer-clear-all-mediaitems-except-the-one-thats-playing – Pemba Tamang May 03 '21 at 13:35