Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add caching #99

Closed
jamesfzhang opened this issue Sep 30, 2015 · 75 comments
Closed

Add caching #99

jamesfzhang opened this issue Sep 30, 2015 · 75 comments
Labels
feature stale Closed due to inactivity or lack or resources

Comments

@jamesfzhang
Copy link

How do you cache a video that's downloaded over the network?

@jamesfzhang jamesfzhang changed the title Cache assets? Add caching Sep 30, 2015
@ababol

This comment has been minimized.

@isair isair added feature help wanted The issue has been reviewed and is valid, and is waiting for someone to work on it. labels Nov 18, 2015
@cancan101
Copy link

This might be a good solution: https://github.com/johanneslumpe/react-native-fs

@thsig
Copy link

thsig commented Feb 6, 2016

Another approach would be to write downloaded assets to a temp directory and load them from there when available. I've only worked with the Obj C part of the native code, but this approach would work there (I assume it could work on the Java side as well). Then the relevant cleanup logic would have to be added to the component lifecycle, to make sure react-native-video doesn't start filling up disk space.

Wouldn't it be better to think of this as an implementation detail of the native code and maintain the current API?

@thsig
Copy link

thsig commented Feb 6, 2016

I'd be up for implementing this for iOS to start with. My initial proposal is that caching is disabled by default, but requested on a per-clip basis like so:

// Cache video locally (write to temp directory)
<Video source={{uri: "https://foo.com/clip1.mp4", cache: true}} ... />

// Don't cache video
<Video source={{uri: "https://foo.com/clip1.mp4"}} ... />

The cache option would be ignored for local files, since it's only relevant for files loaded over the network.

The native code would then first look for the asset on disk, loading it from there if found, or otherwise load it remotely as usual.

There are at least a couple of decisions that have to be made around this:

  1. Setting the total maximum size (in MB) of the files cached by the library. This could be a fixed size (30MB?), perhaps with some configuration option to override the default max size on the React class?
  2. When should the temp dir be cleared/deleted? Does it have to be cleaned out at all if the max size of the temp dir is reasonable? Or should the each instance of the component delete assets older than e.g. 10 minutes when it's initialized? This caching should probably be mostly ephemeral, e.g. for switching to full-screen mode or changing the UI hierarchy while conceptually maintaining the same underlying "player context".

Would be great to get some feedback on this. I'll probably start work on this soon at any rate since I need this for an app I'm working on, so the sooner the better.

@jamesfzhang
Copy link
Author

Sounds like a good approach!

  1. It would be nice to be able to configure how much to cache.
  2. It would also be nice to configure a TTL on the cache, and the cache would get purged either when its full or when the TTL is met.

Not sure how I feel about this, but how about being able to configure the params like this?

// Use smart defaults (30MB cache size & purge in 10 minute )
<Video source={{uri: "https://foo.com/clip1.mp4", cache: true } ... />

// Configure cache to 50MB & purge in 1 hour
<Video source={{uri: "https://foo.com/clip1.mp4", cache: { size: 50, expiresIn: 3600 }} ... />

@thsig
Copy link

thsig commented Feb 6, 2016

@jamesfzhang The thing with that approach is that these parameters should probably be set globally for the app. For example, if one writes something like this:

<View>
  <Video source={{uri: "https://foo.com/clip1.mp4", cache: { size: 50, expiresIn: 3600 }} ... />
  <Video source={{uri: "https://foo.com/clip1.mp4", cache: { size: 40, expiresIn: 4800 }} ... />
</View>

Which set of cache settings should be used for caching https://foo.com/clip1.mp4? This could work fine if users are instructed to always use the same cache settings for the same clip/s, but it feels a bit hacky — I think it would be better to globally configure this somewhere so that it applies to all instances of Video, but I'm not sure what's the best way to go about that. Modifying the class directly?

@jamesfzhang
Copy link
Author

I was under the impression that the configurations would be set per video, as opposed to global. I imagine there are some videos you want to cache and others you don't.

But I guess just using global configs would be fine too? Maybe you'd need to add a VideoConfiguration class?

@thsig
Copy link

thsig commented Feb 6, 2016

They could be set per video, but it's not clear which settings should take precedence when there are several Video instances mounted using the same clip, which is OK (if we can assume that users stick to providing the same config via props to each instance) but not ideal. Otherwise there's no way to know e.g. whether https://foo.com/clip1.mp4 should be invalidated in 3600 seconds or 4800 seconds above.

Something like this would work (maybe not particularly pretty, but then again it only has to be written once):

// require this file instead of require('react-native-video') where Video is to be used
var Video = class extends (require('react-native-video').default) {
  cacheOptions() { return {size: 50, expiresIn: 3600, key: 'settings'} };
}
module.exports = Video;

Then the cache settings would be applied globally through the app, and only once. This could also be done several times with different settings, if the user of the library is confident that there won't be conflicts (i.e. two Video components caching the same clip with differing settings). E.g. for two different contexts within the app with different kinds of clips, with different caching needs:

// LongClipPlayer.js
var LongVideo = class extends (require('react-native-video').default) {
  cacheOptions() { return {size: 50, expiresIn: 3600, key: 'longVideoCache'} };
}
module.exports = LongVideo;

// ShortClipPlayer.js
var ShortVideo = class extends (require('react-native-video').default) {
  cacheOptions() { return {size: 10, expiresIn: 1200, key: 'shortVideoCache'} };
}
module.exports = ShortVideo;

Then the implementation would put videos created with key in a temp folder distinct from those used for other keys, so that the invalidation logic only operates on the appropriate caches.

Thoughts on this?

@thsig
Copy link

thsig commented Feb 6, 2016

The second case (multiple configs) may be overkill to start with, we could just go for a single global configuration for starters. The defaults should be fine for most users too, so I don't think this should add to the complexity of using the library, unless more control is needed.

@ababol
Copy link

ababol commented Apr 9, 2016

A really easy solution is the one that @cancan101 mentioned, using react-native-fs, but yeah as @thsig mentioned there will huge issue with the usage of the disk space if we are just storing everything without an expire limit.

Otherwise I agree with @thsig , let's keep it simple first, nah?

Here is the code that is doing the thing to cache Image, but it is permanent storage, it is never removed :/
https://github.com/mohebifar/react-native-ximage/blob/master/src/Storage.js#L64-L66

One super easy solution to clean the cache is to make a function which check all cached video of the specific application/folder and which deletes the old videos.

Something like this:

module.exports.cleanCache = function (cacheKey, time) {
  const videos = readCache(cacheKey);
  videos.forEach((video) => {
    // is my video older than `time`?
    if (isMyVideoOld(video, time) {
      deleteVideo(video);
    }
  });
}

The function will be accessible for the developer and we don't have to care if he/she is going to use it.
By default, we can just disable the cache and if the developer wants to use it, he should be aware that he has to use this function to clean it.

@thsig
Copy link

thsig commented Apr 9, 2016

Maybe it would be better to separate the caching implementation from react-native-video, but have an API for react-native-video to receive pre-loaded AVAsset-s (and the equivalent for Android) under the covers. It seems to me that this library should primarily concern itself with playback and offload other concerns to other libraries where possible.

What about something like this?

<Video
  source={{uri: "https://foo.com/clip1.mp4"}}
  cacheName='StevesCache' />

When receiving new sources, the native code would publish a notification to the channel named cacheName (e.g. via the NSNotificationCenter system in iOS, and the Java equivalent on Android), containing the source uri/s which would then be received by the StevesCache native module class (or whatever the class is named - it's decoupled from the channel name provided).

The cache module implementation would then publish a message to the ReactNativeVideo channel, where the payload would be an array whose elements are either AVAsset* (or the Java equivalent) or nil, depending on whether the requested URI was found by the cache implementation's cache (in the same order as the source array sent to it in the previous step).

I'm suggesting an array assuming that multiple-video playback will be added to the library at some point (whether my branch is merged or not, this is probably on the roadmap anyway), so that would pre-empt having to change the API later, possibly after other libraries start depending on it.

Then react-native-video would treat the cached assets as fully buffered and proceed from there, buffering non-cached assets as usual.

Thoughts, @brentvatne? We might need functionality like this soon, so I'd probably be up for writing the initial iOS implementation. I'm also very open to alternate implementation ideas.

@sjchmiela
Copy link
Contributor

sjchmiela commented Apr 18, 2016

I have a feeling there may be a better way – proxying requests to server. One could set himself as AVAssetResourceLoader and then check in his own cache if such video has been cached or else load from URL. This approach has been described:

Based on uri we could initialize under the hood one cache for every custom protocol, eg.:

<Video
  source={{uri: "stevesCache://foo.com/clip1.mp4"}}
/>

...

<Video
  source={{uri: "globalCache://foo.com/clip1.mp4"}}
/>

Maybe we could use uri as stevesCache://https://foo.com... to be able to specify target request, I don't know how NSURL would handle such URL.

Customizing cache capacity would require calls to some kind of Cache.setCacheCapacity('stevesCache', 40 * 1024 * 1024)

@hongri
Copy link

hongri commented Jul 13, 2016

Hi, all:

I want to know when to publish this function? It waste a lot of flow now.

@getnashty
Copy link

Any updates here? @thsig, definitely think you should take over the repo :)

@brentvatne
Copy link
Contributor

good point @getnashty! @thsig I have added you as a collaborator on this repo. let me know your npm username and I can give you npm publish access, brentvatne at gmail dot com

@duhseekoh
Copy link
Contributor

This hasn't been touched for a while, but since iOS natively supports offline caching now it's worth reviving: https://developer.apple.com/reference/avfoundation/avassetdownloadurlsession/1650938-makeassetdownloadtask

Video explaining the process. Starts at about 16:20: https://developer.apple.com/videos/play/wwdc2016/504/

Looks like this could all be handled behind the scenes in this package, and all the user has to decide is if the video should be cached and provide it the key name + download location that would be needed to look the task up again.

From the video:
image

image
He points out that the video can be played at ANY time during the download process, and it's smart enough to use whats been downloaded already and continue downloading to the cache as the video plays.

Sidenote: A sibling package to this that only does the downloading could be helpful as well. Would be used to load videos even without a react-native-video on the screen. (app is in background, preload video, etc..)

@ndbroadbent
Copy link

ndbroadbent commented Apr 17, 2017

@duhseekoh That's fantastic, thanks for posting that! I should pay more attention to updates.

I've been working on a native iOS app with Swift (which I started a few years ago), and now I'm going rewrite it in React Native. I have been wanting to cache downloaded videos for a very long time, but it was going to be a ton of work. But it looks like AVAssetDownloadTask changes everything, and video caching should be pretty easy to implement now. (Although I'm nervous about Java and Windows.)

I'm going to need this for my app, so I would like to contribute caching for iOS. I might be able to figure out Android, too. Worst case would be that caching only works on iOS and is just ignored on other platforms.

Maybe it would be better to separate the caching implementation from react-native-video

@thsig - I'm not sure about that, I think most developers would love to pass cache={true}, and just have it use some sane defaults. E.g. A LRU cache with a max size of 100MB would be great for my app (lots of short videos), and I think it would probably work well for most video sharing apps. And probably a default TTL of 24 hours, with a customizable cacheTTL.

In the case where you have several Video instances playing the same video, then the last one would have precedence, and we could just show a yellow box warning during development.

@jlongster
Copy link

@duhseekoh From the looks of it AVAssetDownloadTask requires HLS, right? So you can't use it for any arbitrary video. That seems different than what this issue is about.

@juergengunz

This comment has been minimized.

@vvavepacket
Copy link

if we use react-native-fs approach,, does it mean the video has to be downloaded fully, before react-native-video can access it? Please correct me if I'm wrong,, but what I want to happen is while the video is playing (and at the same time downloading) in the backgroud, the video file is cache somewhere.

@LeeZC

This comment has been minimized.

@SuhairZain

This comment has been minimized.

@raschan

This comment has been minimized.

@janreyho

This comment has been minimized.

2 similar comments
@itaydr

This comment has been minimized.

@freddiecabrera

This comment has been minimized.

@hkdahal
Copy link

hkdahal commented Jul 4, 2018

@DavitVosk I am not sure how others have solved it (looks like my way is similar to @rnowm), but the way I solved it on my project is I started writing my own downloader based off of this project: https://github.com/kfiroo/react-native-cached-image. Once you make that downloader work, you could load the local file path as your source uri.

Something like:

const mediaUrl = MEDIA.cached ? `file://${MEDIA.path}` : MEDIA.url
<Video source={ uri: mediaUrl } />

@DavitVosk
Copy link

@hkdahal thanks for your answer. Can you please share your created downloader file that we can use it? Thanks

@DavitVosk
Copy link

@hkdahal Also can you please explain how that work? You use https://github.com/kfiroo/react-native-cached-image package with kfiroo/react-native-cached-image@31ad016 PR, but how react-native-video sees that the videos are cached? How they interact with each other?

@BunHouth

This comment has been minimized.

@hkdahal
Copy link

hkdahal commented Jul 6, 2018

@DavitVosk I will create a gist or share a separate project that does the interaction between these packages later this weekend. I will mention you over there.

@n1ru4l

This comment has been minimized.

@roycclu

This comment has been minimized.

@n1ru4l

This comment has been minimized.

@n1ru4l

This comment has been minimized.

@cobarx
Copy link
Contributor

cobarx commented Jul 18, 2018

Thanks for making these changes @n1ru4l! I will add feedback on the PR.

This is a pretty impactful change and as @n1ru4l has mentioned, we need testing and feedback to make sure this is ready to go. If you want to use this feature, please start testing it and letting us know how well it works. Once I have a couple thumbs up, I will do a review and get it merged.

@darekg11
Copy link

As caching is not yet availalbe out of the box for react-native-video on both Android and IOS, we have been using:
https://github.com/joltup/rn-fetch-blob to download our videos from S3, (it supports progress callback so you can add progress-bars etc), save it to a device storage and point react-native-video to URI like so:

    rfblob.config({
        fileCache: true,
        path: filePath // where you want to save a file
      }).fetch('GET', 'your-hosting-url/file.mp4')
        .then(result => {
           const filePath = result.path();
           const sourceForVideoPlayer = {
               uri: 'file://' + filePath
           }
        })
    }

And when Video Player component is mounted, we check if there is already a file on device storage with matching name and if so, we just load video from storage instead of downloading it once more.
Of course, you also need to clean your cache directory by yourself but this is a small trade off for solution that works on both IOS and Android.
Maybe it will help someone else, as soon as caching is supported on both platforms, we will be happy to switch

@webbin
Copy link

webbin commented Dec 25, 2018

As caching is not yet availalbe out of the box for react-native-video on both Android and IOS, we have been using:
https://github.com/joltup/rn-fetch-blob to download our videos from S3, (it supports progress callback so you can add progress-bars etc), save it to a device storage and point react-native-video to URI like so:

    rfblob.config({
        fileCache: true,
        path: filePath // where you want to save a file
      }).fetch('GET', 'your-hosting-url/file.mp4')
        .then(result => {
           const filePath = result.path();
           const sourceForVideoPlayer = {
               uri: 'file://' + filePath
           }
        })
    }

And when Video Player component is mounted, we check if there is already a file on device storage with matching name and if so, we just load video from storage instead of downloading it once more.
Of course, you also need to clean your cache directory by yourself but this is a small trade off for solution that works on both IOS and Android.
Maybe it will help someone else, as soon as caching is supported on both platforms, we will be happy to switch

hello, is there any way to download the video with rn-fetch-blob, and play the video while downloading?It's so long for waiting the downloading task complete.

@n1ru4l
Copy link
Contributor

n1ru4l commented Dec 26, 2018

@webbin No, there is not.

For android video caching I am currently using this: n1ru4l@0e1c7c2 (uses a HTTP proxy to cache the file).

The idea came from this article: https://instagram-engineering.com/improving-video-playback-on-android-2f6c6a0058d

I use this is my private fork and hope I will have the time to someday do a proper implementation for react-native-video.

@gazedash

This comment has been minimized.

@n1ru4l
Copy link
Contributor

n1ru4l commented Jan 23, 2019

@gazedash I am still using it with patch-package on react-native-video@4.3.1. Had no time to clean it up and release it as part of react-native-video.

patches/react-native-video+4.3.1.patch

patch-package
--- a/node_modules/react-native-video/android-exoplayer/build.gradle
+++ b/node_modules/react-native-video/android-exoplayer/build.gradle
@@ -31,5 +31,5 @@ dependencies {
         exclude group: 'com.squareup.okhttp3', module: 'okhttp'
     }
     implementation 'com.squareup.okhttp3:okhttp:3.12.1'
-
+    implementation 'com.danikula:videocache:2.7.1'
 }
--- a/node_modules/react-native-video/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.java
+++ b/node_modules/react-native-video/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.java
@@ -2,6 +2,7 @@ package com.brentvatne.exoplayer;
 
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.net.Uri;
 
 import com.facebook.react.bridge.ReactContext;
 import com.facebook.react.modules.network.CookieJarContainer;
@@ -17,6 +18,7 @@ import com.google.android.exoplayer2.util.Util;
 import okhttp3.Cookie;
 import okhttp3.JavaNetCookieJar;
 import okhttp3.OkHttpClient;
+import com.danikula.videocache.HttpProxyCacheServer;
 import java.util.Map;
 
 
@@ -27,6 +29,7 @@ public class DataSourceUtil {
 
     private static DataSource.Factory rawDataSourceFactory = null;
     private static DataSource.Factory defaultDataSourceFactory = null;
+    private static HttpProxyCacheServer proxy = null;
     private static String userAgent = null;
 
     public static void setUserAgent(String userAgent) {
@@ -84,4 +87,14 @@ public class DataSourceUtil {
 
         return okHttpDataSourceFactory;
     }
+
+    public static Uri getCacheUri(Uri uri, Context context) {
+        if (proxy == null) {
+            proxy = new HttpProxyCacheServer.Builder(context)
+                .maxCacheSize(1024 * 1024 * 512)
+                .maxCacheFilesCount(20)
+                .build();
+        }
+        return Uri.parse(proxy.getProxyUrl(uri.toString()));
+    }
 }
--- a/node_modules/react-native-video/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
+++ b/node_modules/react-native-video/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
@@ -366,13 +366,7 @@ class ReactExoplayerView extends FrameLayout implements
     }
 
     private boolean requestAudioFocus() {
-        if (disableFocus) {
-            return true;
-        }
-        int result = audioManager.requestAudioFocus(this,
-                AudioManager.STREAM_MUSIC,
-                AudioManager.AUDIOFOCUS_GAIN);
-        return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+        return true;
     }
 
     private void setPlayWhenReady(boolean playWhenReady) {
@@ -759,7 +753,7 @@ class ReactExoplayerView extends FrameLayout implements
             boolean isOriginalSourceNull = srcUri == null;
             boolean isSourceEqual = uri.equals(srcUri);
 
-            this.srcUri = uri;
+            this.srcUri = DataSourceUtil.getCacheUri(uri, themedReactContext);
             this.extension = extension;
             this.requestHeaders = headers;
             this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, BANDWIDTH_METER, this.requestHeaders);

@eddyjusufi

This comment has been minimized.

@igor-lemon
Copy link

Hi there! What about caching video files when a url ends like https://.../file.mp4?access_token=xxx ?

@n1ru4l
Copy link
Contributor

n1ru4l commented Sep 26, 2019

@igor-lemon feel free to send a pull request that allows registering a cache key normalizer.

@vargajacint
Copy link

Any update?

@antoinemaillard
Copy link

@vargajacint We just created a fork to add basic caching feature to the <Video/> component (Android and ExoPlayer only, since caching is already native on iOS)

You can find it here: https://github.com/jowapp/react-native-video

(@n1ru4l if you think it's worth it, we can send a PR)

tl;dr: just reference our repo in your package.json react-native-video entry and add the useCache prop to your <Video/> component. For more details and advanced features, have a look at our updated README

[Note: We are Jow, a wonderful startup based in Paris, offering delighful recipes and helping do your grocery shopping according to what you like to eat. And we're also hiring talented people, especially back & front developers!)]

@AndresTIY
Copy link

Hi @antoinemaillard . Thanks for providing this repo!

Does this work for HLS?

If useCache prop is set, cache will be used for all non-streamed content (ie. except for Microsoft SmoothSteaming, DASH, HLS)

I see that it does support HLS but when I try to console log the cache stats with:

ExoPlayerCache.getCacheStats()
  .then(cacheStats => {
    console.log(cacheStats)
  })

It shows an empty object. Yet, with an mp4, I see all of the stats.

Also, does this handle deleting the cached video as well?

Thanks :)

@eqlion
Copy link

eqlion commented Mar 3, 2021

@antoinemaillard Thank you for your fork enabling caching on Android! Is there any way to imperatively cache video ahead of time in order to be able to play it instantly? (Something like Image.prefetch() in React Native or FastImage.preload() in React Native FastImage)
Nevermind, I managed to achieve it by downloading the videos in advance with React Native FS and playing them from local source.

@mirceaciu
Copy link

Hi @antoinemaillard . Thanks for providing this repo!

Does this work for HLS?

looking at the code there is support for those types of media but for some reason the cache does not work

case C.TYPE_DASH:
    DataSource.Factory dataSourceFactory = buildDataSourceFactory(false);

    mediaSource = new DashMediaSource.Factory(
            new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
            buildDataSourceFactory(false)
    ).setLoadErrorHandlingPolicy(
            config.buildLoadErrorHandlingPolicy(minLoadRetryCount)
    ).createMediaSource(uri);
    break;

given a url like https://fictive-api.com/dash/1766e4a8f33d49a497e9ff0fe59c130c.mp4/manifest.mpd it will detect that the type is DASH, play the video but won't cache :(

@hueniverse hueniverse added stale Closed due to inactivity or lack or resources and removed help wanted The issue has been reviewed and is valid, and is waiting for someone to work on it. labels Apr 22, 2022
@AdityaVernekar
Copy link

Thank you for your fork enabling caching on Android! Is there any way to imperatively cache video ahead of time in order to be able to play it instantly? (Something like Image.prefetch() in React Native or FastImage.preload() in React Native FastImage)
Nevermind, I managed to achieve it by downloading the videos in advance with React Native FS and playing them from local source.

Can you tell me how you did this ??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature stale Closed due to inactivity or lack or resources
Projects
None yet
Development

No branches or pull requests