From ecc946d1c1f9870c3f0a40fa426fa088017bd7cd Mon Sep 17 00:00:00 2001 From: lovegaoshi <106490582+lovegaoshi@users.noreply.github.com> Date: Wed, 1 May 2024 02:20:34 -0700 Subject: [PATCH] feat(android): cache (#3514) * feat: android cache * docs: bufferSize * Revert "docs: bufferSize" This reverts commit 09637b134e121b9ca3ffd78f2f5bc657319ed67a. * fix: cacheSize name * feat: singleton android cache * fix: local cache resolve * fix: lint * docs: android cache * chore: merge conflict * fix: lint * chore: useCache button * chore: fix state in the sample * fix: cache factory * chore: update cacheSizeMB docs --------- Co-authored-by: Olivier Bouillet --- .../exoplayer/ReactExoplayerSimpleCache.kt | 31 ++++++++++++++++ .../exoplayer/ReactExoplayerView.java | 35 +++++++++++++++---- .../exoplayer/ReactExoplayerViewManager.java | 5 ++- docs/pages/component/props.mdx | 24 +++++++------ docs/pages/other/caching.md | 22 ++++++++---- examples/basic/src/VideoPlayer.tsx | 17 +++++++++ src/types/video.ts | 1 + 7 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerSimpleCache.kt 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 00000000..cec4b898 --- /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 6d0d43eb..ba414245 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.BuildConfig; 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 @@ public class ReactExoplayerView extends FrameLayout implements .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 @@ public class ReactExoplayerView extends FrameLayout implements ); 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 class ReactExoplayerView extends FrameLayout implements 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 class ReactExoplayerView extends FrameLayout implements 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 e3482cda..1e9ffd60 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 f955db36..34361e66 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 806fed12..7b858627 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}