diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerSimpleCache.kt b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerSimpleCache.kt new file mode 100644 index 0000000000..cec4b898ff --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerSimpleCache.kt @@ -0,0 +1,31 @@ +package com.brentvatne.exoplayer + +import android.content.Context +import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.HttpDataSource +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor +import androidx.media3.datasource.cache.SimpleCache +import java.io.File + +object RNVSimpleCache { + // TODO: when to release? how to check if cache is released? + private var simpleCache: SimpleCache? = null + var cacheDataSourceFactory: DataSource.Factory? = null + + fun setSimpleCache(context: Context, cacheSize: Int, factory: HttpDataSource.Factory) { + if (cacheDataSourceFactory != null || cacheSize == 0) return + simpleCache = SimpleCache( + File(context.cacheDir, "RNVCache"), + LeastRecentlyUsedCacheEvictor( + cacheSize.toLong() * 1024 * 1024 + ), + StandaloneDatabaseProvider(context) + ) + cacheDataSourceFactory = + CacheDataSource.Factory() + .setCache(simpleCache!!) + .setUpstreamDataSourceFactory(factory) + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 6d0d43ebad..ba4142454f 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -105,7 +105,6 @@ import com.brentvatne.react.R; import com.brentvatne.receiver.AudioBecomingNoisyReceiver; import com.brentvatne.receiver.BecomingNoisyListener; -import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -188,7 +187,6 @@ public class ReactExoplayerView extends FrameLayout implements private boolean hasDrmFailed = false; private boolean isUsingContentResolution = false; private boolean selectTrackWhenReady = false; - private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; private int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; @@ -199,6 +197,7 @@ public class ReactExoplayerView extends FrameLayout implements private double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE; private Handler mainHandler; private Runnable mainRunnable; + private DataSource.Factory cacheDataSourceFactory; // Props from React private Uri srcUri; @@ -623,8 +622,11 @@ private void initializePlayerCore(ReactExoplayerView self) { .setAdEventListener(this) .setAdErrorListener(this) .build(); - DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory); + if (cacheDataSourceFactory != null) { + mediaSourceFactory.setDataSourceFactory(cacheDataSourceFactory); + } + if (adsLoader != null) { mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView); } @@ -839,9 +841,17 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi ); break; case CONTENT_TYPE_OTHER: - mediaSourceFactory = new ProgressiveMediaSource.Factory( - mediaDataSourceFactory - ); + if (uri.toString().startsWith("file://") || + cacheDataSourceFactory == null) { + mediaSourceFactory = new ProgressiveMediaSource.Factory( + mediaDataSourceFactory + ); + } else { + mediaSourceFactory = new ProgressiveMediaSource.Factory( + cacheDataSourceFactory + ); + + } break; case CONTENT_TYPE_RTSP: if (!BuildConfig.USE_EXOPLAYER_RTSP) { @@ -2015,7 +2025,7 @@ public void setHideShutterView(boolean hideShutterView) { exoPlayerView.setHideShutterView(hideShutterView); } - public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs, double newMaxHeapAllocationPercent, double newMinBackBufferMemoryReservePercent, double newMinBufferMemoryReservePercent, int newBackBufferDurationMs) { + public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs, double newMaxHeapAllocationPercent, double newMinBackBufferMemoryReservePercent, double newMinBufferMemoryReservePercent, int newBackBufferDurationMs, int cacheSize) { minBufferMs = newMinBufferMs; maxBufferMs = newMaxBufferMs; bufferForPlaybackMs = newBufferForPlaybackMs; @@ -2023,6 +2033,17 @@ public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBuffe maxHeapAllocationPercent = newMaxHeapAllocationPercent; minBackBufferMemoryReservePercent = newMinBackBufferMemoryReservePercent; minBufferMemoryReservePercent = newMinBufferMemoryReservePercent; + if (cacheSize > 0) { + RNVSimpleCache.INSTANCE.setSimpleCache( + this.getContext(), + cacheSize, + buildHttpDataSourceFactory(false) + ); + cacheDataSourceFactory = RNVSimpleCache.INSTANCE.getCacheDataSourceFactory(); + } else { + cacheDataSourceFactory = null; + } + backBufferDurationMs = newBackBufferDurationMs; releasePlayer(); initializePlayer(); diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index e3482cdac1..1e9ffd606b 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -59,6 +59,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager @@ -610,7 +613,7 @@ Pass directly the asset to play (deprecated) ```javascript const sintel = require('./sintel.mp4'); -source={ sintel }; +source = {sintel}; ``` Or by using an uri (starting from 6.0.0-beta.6) @@ -620,7 +623,6 @@ const sintel = require('./sintel.mp4'); source={{ uri: sintel }} ``` - #### URI string A number of URI schemes are supported by passing an object with a `uri` attribute. @@ -725,14 +727,14 @@ source={{ ### `subtitleStyle` -| Property | Description | Platforms | -| ------------- | ----------------------------------------------------------------------- | --------- | -| fontSize | Adjust the font size of the subtitles. Default: font size of the device | Android | -| paddingTop | Adjust the top padding of the subtitles. Default: 0 | Android | -| paddingBottom | Adjust the bottom padding of the subtitles. Default: 0 | Android | -| paddingLeft | Adjust the left padding of the subtitles. Default: 0 | Android | -| paddingRight | Adjust the right padding of the subtitles. Default: 0 | Android | -| opacity | Adjust the visibility of subtitles with 0 hiding and 1 fully showing them. Android supports float values between 0 and 1 for varying opacity levels, whereas iOS supports only 0 or 1. Default: 1. | Android, iOS | +| Property | Description | Platforms | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| fontSize | Adjust the font size of the subtitles. Default: font size of the device | Android | +| paddingTop | Adjust the top padding of the subtitles. Default: 0 | Android | +| paddingBottom | Adjust the bottom padding of the subtitles. Default: 0 | Android | +| paddingLeft | Adjust the left padding of the subtitles. Default: 0 | Android | +| paddingRight | Adjust the right padding of the subtitles. Default: 0 | Android | +| opacity | Adjust the visibility of subtitles with 0 hiding and 1 fully showing them. Android supports float values between 0 and 1 for varying opacity levels, whereas iOS supports only 0 or 1. Default: 1. | Android, iOS | Example: diff --git a/docs/pages/other/caching.md b/docs/pages/other/caching.md index f955db367d..34361e66a8 100644 --- a/docs/pages/other/caching.md +++ b/docs/pages/other/caching.md @@ -1,22 +1,30 @@ # Caching -Caching is currently only supported on `iOS` platforms with a CocoaPods setup. +Caching is supported on `iOS` platforms with a CocoaPods setup, and on `android` using `SimpleCache`. -## Technology +## Android + +Android uses a LRU `SimpleCache` with a variable cache size that can be specified by bufferConfig - cacheSizeMB. This creates a folder named `RNVCache` in the app's `cache` folder. Do note RNV does not yet offer a native call to flush the cache, it can be flushed by clearing the app's cache. + +In addition, this resolves RNV6's repeated source URI call problem when looping a video on Android. + +## iOS + +### Technology The cache is backed by [SPTPersistentCache](https://github.com/spotify/SPTPersistentCache) and [DVAssetLoaderDelegate](https://github.com/vdugnist/DVAssetLoaderDelegate). -## How Does It Work +### How Does It Work The caching is based on the url of the asset. -SPTPersistentCache is a LRU ([Least Recently Used](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU))) cache. +SPTPersistentCache is a LRU ([Least Recently Used]()) cache. -## Restrictions +### Restrictions -Currently, caching is only supported for URLs that end in a `.mp4`, `.m4v`, or `.mov` extension. In future versions, URLs that end in a query string (e.g. test.mp4?resolution=480p) will be support once dependencies allow access to the `Content-Type` header. At this time, HLS playlists (.m3u8) and videos that sideload text tracks are not supported and will bypass the cache. +Currently, caching is only supported for URLs that end in a `.mp4`, `.m4v`, or `.mov` extension. In future versions, URLs that end in a query string (e.g. test.mp4?resolution=480p) will be support once dependencies allow access to the `Content-Type` header. At this time, HLS playlists (.m3u8) and videos that sideload text tracks are not supported and will bypass the cache. You will also receive warnings in the Xcode logs by using the `debug` mode. So if you are not 100% sure if your video is cached, check your Xcode logs! By default files expire after 30 days and the maximum cache size is 100mb. -In a future release the cache might have more configurable options. \ No newline at end of file +In a future release the cache might have more configurable options. diff --git a/examples/basic/src/VideoPlayer.tsx b/examples/basic/src/VideoPlayer.tsx index 806fed1282..7b858627b0 100644 --- a/examples/basic/src/VideoPlayer.tsx +++ b/examples/basic/src/VideoPlayer.tsx @@ -69,6 +69,7 @@ interface StateType { srcListId: number; loop: boolean; showRNVControls: boolean; + useCache: boolean; poster?: string; } @@ -97,6 +98,7 @@ class VideoPlayer extends Component { srcListId: 0, loop: false, showRNVControls: false, + useCache: false, poster: undefined, }; @@ -669,6 +671,14 @@ class VideoPlayer extends Component { }} text="decoderInfo" /> + { + this.setState({useCache: !this.state.useCache}); + }} + selectedText="enable cache" + unselectedText="disable cache" + /> ) : null}