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 <freeboub@gmail.com>
This commit is contained in:
lovegaoshi 2024-05-01 02:20:34 -07:00 committed by GitHub
parent 518a9a93e0
commit ecc946d1c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 109 additions and 26 deletions

View File

@ -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)
}
}

View File

@ -105,7 +105,6 @@ import com.brentvatne.react.BuildConfig;
import com.brentvatne.react.R; import com.brentvatne.react.R;
import com.brentvatne.receiver.AudioBecomingNoisyReceiver; import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
import com.brentvatne.receiver.BecomingNoisyListener; import com.brentvatne.receiver.BecomingNoisyListener;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
@ -188,7 +187,6 @@ public class ReactExoplayerView extends FrameLayout implements
private boolean hasDrmFailed = false; private boolean hasDrmFailed = false;
private boolean isUsingContentResolution = false; private boolean isUsingContentResolution = false;
private boolean selectTrackWhenReady = false; private boolean selectTrackWhenReady = false;
private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
private int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_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 double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
private Handler mainHandler; private Handler mainHandler;
private Runnable mainRunnable; private Runnable mainRunnable;
private DataSource.Factory cacheDataSourceFactory;
// Props from React // Props from React
private Uri srcUri; private Uri srcUri;
@ -623,8 +622,11 @@ public class ReactExoplayerView extends FrameLayout implements
.setAdEventListener(this) .setAdEventListener(this)
.setAdErrorListener(this) .setAdErrorListener(this)
.build(); .build();
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory); DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory);
if (cacheDataSourceFactory != null) {
mediaSourceFactory.setDataSourceFactory(cacheDataSourceFactory);
}
if (adsLoader != null) { if (adsLoader != null) {
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView); mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
} }
@ -839,9 +841,17 @@ public class ReactExoplayerView extends FrameLayout implements
); );
break; break;
case CONTENT_TYPE_OTHER: case CONTENT_TYPE_OTHER:
if (uri.toString().startsWith("file://") ||
cacheDataSourceFactory == null) {
mediaSourceFactory = new ProgressiveMediaSource.Factory( mediaSourceFactory = new ProgressiveMediaSource.Factory(
mediaDataSourceFactory mediaDataSourceFactory
); );
} else {
mediaSourceFactory = new ProgressiveMediaSource.Factory(
cacheDataSourceFactory
);
}
break; break;
case CONTENT_TYPE_RTSP: case CONTENT_TYPE_RTSP:
if (!BuildConfig.USE_EXOPLAYER_RTSP) { if (!BuildConfig.USE_EXOPLAYER_RTSP) {
@ -2015,7 +2025,7 @@ public class ReactExoplayerView extends FrameLayout implements
exoPlayerView.setHideShutterView(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; minBufferMs = newMinBufferMs;
maxBufferMs = newMaxBufferMs; maxBufferMs = newMaxBufferMs;
bufferForPlaybackMs = newBufferForPlaybackMs; bufferForPlaybackMs = newBufferForPlaybackMs;
@ -2023,6 +2033,17 @@ public class ReactExoplayerView extends FrameLayout implements
maxHeapAllocationPercent = newMaxHeapAllocationPercent; maxHeapAllocationPercent = newMaxHeapAllocationPercent;
minBackBufferMemoryReservePercent = newMinBackBufferMemoryReservePercent; minBackBufferMemoryReservePercent = newMinBackBufferMemoryReservePercent;
minBufferMemoryReservePercent = newMinBufferMemoryReservePercent; minBufferMemoryReservePercent = newMinBufferMemoryReservePercent;
if (cacheSize > 0) {
RNVSimpleCache.INSTANCE.setSimpleCache(
this.getContext(),
cacheSize,
buildHttpDataSourceFactory(false)
);
cacheDataSourceFactory = RNVSimpleCache.INSTANCE.getCacheDataSourceFactory();
} else {
cacheDataSourceFactory = null;
}
backBufferDurationMs = newBackBufferDurationMs; backBufferDurationMs = newBackBufferDurationMs;
releasePlayer(); releasePlayer();
initializePlayer(); initializePlayer();

View File

@ -59,6 +59,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_AUDIO_OUTPUT = "audioOutput"; private static final String PROP_AUDIO_OUTPUT = "audioOutput";
private static final String PROP_VOLUME = "volume"; private static final String PROP_VOLUME = "volume";
private static final String PROP_BUFFER_CONFIG = "bufferConfig"; private static final String PROP_BUFFER_CONFIG = "bufferConfig";
private static final String PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB";
private static final String PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"; private static final String PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs";
private static final String PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"; private static final String PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs";
private static final String PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"; private static final String PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs";
@ -402,6 +403,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
@ReactProp(name = PROP_BUFFER_CONFIG) @ReactProp(name = PROP_BUFFER_CONFIG)
public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) { public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) {
int cacheSize = 0;
int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
@ -412,6 +414,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE; double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
if (bufferConfig != null) { if (bufferConfig != null) {
cacheSize = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_CACHE_SIZE, 0);
minBufferMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_MIN_BUFFER_MS, minBufferMs); minBufferMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_MIN_BUFFER_MS, minBufferMs);
maxBufferMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_MAX_BUFFER_MS, maxBufferMs); maxBufferMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_MAX_BUFFER_MS, maxBufferMs);
bufferForPlaybackMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS, bufferForPlaybackMs); bufferForPlaybackMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS, bufferForPlaybackMs);
@ -420,7 +423,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
minBackBufferMemoryReservePercent = ReactBridgeUtils.safeGetDouble(bufferConfig, PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT, minBackBufferMemoryReservePercent); minBackBufferMemoryReservePercent = ReactBridgeUtils.safeGetDouble(bufferConfig, PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT, minBackBufferMemoryReservePercent);
minBufferMemoryReservePercent = ReactBridgeUtils.safeGetDouble(bufferConfig, PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT, minBufferMemoryReservePercent); minBufferMemoryReservePercent = ReactBridgeUtils.safeGetDouble(bufferConfig, PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT, minBufferMemoryReservePercent);
backBufferDurationMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS, backBufferDurationMs); backBufferDurationMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS, backBufferDurationMs);
videoView.setBufferConfig(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, maxHeapAllocationPercent, minBackBufferMemoryReservePercent, minBufferMemoryReservePercent, backBufferDurationMs); videoView.setBufferConfig(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, maxHeapAllocationPercent, minBackBufferMemoryReservePercent, minBufferMemoryReservePercent, backBufferDurationMs, cacheSize);
} }
} }

View File

@ -63,7 +63,7 @@ Adjust the buffer settings. This prop takes an object with one or more of the pr
| maxHeapAllocationPercent | number | The percentage of available heap that the video can use to buffer, between 0 and 1 | | maxHeapAllocationPercent | number | The percentage of available heap that the video can use to buffer, between 0 and 1 |
| minBackBufferMemoryReservePercent | number | The percentage of available app memory at which during startup the back buffer will be disabled, between 0 and 1 | | minBackBufferMemoryReservePercent | number | The percentage of available app memory at which during startup the back buffer will be disabled, between 0 and 1 |
| minBufferMemoryReservePercent | number | The percentage of available app memory to keep in reserve that prevents buffer from using it, between 0 and 1 | | minBufferMemoryReservePercent | number | The percentage of available app memory to keep in reserve that prevents buffer from using it, between 0 and 1 |
| cacheSizeMB | number | Cache size in MB, enabling this to prevent new src requests and save bandwidth while repeating videos, or 0 to disable. Android only. |
Example with default values: Example with default values:
@ -74,9 +74,12 @@ bufferConfig={{
bufferForPlaybackMs: 2500, bufferForPlaybackMs: 2500,
bufferForPlaybackAfterRebufferMs: 5000, bufferForPlaybackAfterRebufferMs: 5000,
backBufferDurationMs: 120000, backBufferDurationMs: 120000,
cacheSizeMB: 0
}} }}
``` ```
Please note that the Android cache is a global cache that is shared among all components; individual components can still opt out of caching behavior by setting cacheSizeMB to 0, but multiple components with a positive cacheSizeMB will be sharing the same one, and the cache size will always be the first value set; it will not change during the app's lifecycle.
### `chapters` ### `chapters`
<PlatformsList types={['tvOS']} /> <PlatformsList types={['tvOS']} />
@ -610,7 +613,7 @@ Pass directly the asset to play (deprecated)
```javascript ```javascript
const sintel = require('./sintel.mp4'); const sintel = require('./sintel.mp4');
source={ sintel }; source = {sintel};
``` ```
Or by using an uri (starting from 6.0.0-beta.6) Or by using an uri (starting from 6.0.0-beta.6)
@ -620,7 +623,6 @@ const sintel = require('./sintel.mp4');
source={{ uri: sintel }} source={{ uri: sintel }}
``` ```
#### URI string #### URI string
A number of URI schemes are supported by passing an object with a `uri` attribute. A number of URI schemes are supported by passing an object with a `uri` attribute.
@ -726,7 +728,7 @@ source={{
### `subtitleStyle` ### `subtitleStyle`
| Property | Description | Platforms | | Property | Description | Platforms |
| ------------- | ----------------------------------------------------------------------- | --------- | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| fontSize | Adjust the font size of the subtitles. Default: font size of the device | Android | | 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 | | paddingTop | Adjust the top padding of the subtitles. Default: 0 | Android |
| paddingBottom | Adjust the bottom padding of the subtitles. Default: 0 | Android | | paddingBottom | Adjust the bottom padding of the subtitles. Default: 0 | Android |

View File

@ -1,17 +1,25 @@
# Caching # 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). 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. 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](<https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)>)) 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.

View File

@ -69,6 +69,7 @@ interface StateType {
srcListId: number; srcListId: number;
loop: boolean; loop: boolean;
showRNVControls: boolean; showRNVControls: boolean;
useCache: boolean;
poster?: string; poster?: string;
} }
@ -97,6 +98,7 @@ class VideoPlayer extends Component {
srcListId: 0, srcListId: 0,
loop: false, loop: false,
showRNVControls: false, showRNVControls: false,
useCache: false,
poster: undefined, poster: undefined,
}; };
@ -669,6 +671,14 @@ class VideoPlayer extends Component {
}} }}
text="decoderInfo" text="decoderInfo"
/> />
<ToggleControl
isSelected={this.state.useCache}
onPress={() => {
this.setState({useCache: !this.state.useCache});
}}
selectedText="enable cache"
unselectedText="disable cache"
/>
</View> </View>
) : null} ) : null}
<ToggleControl <ToggleControl
@ -864,6 +874,13 @@ class VideoPlayer extends Component {
selectedTextTrack={this.state.selectedTextTrack} selectedTextTrack={this.state.selectedTextTrack}
selectedAudioTrack={this.state.selectedAudioTrack} selectedAudioTrack={this.state.selectedAudioTrack}
playInBackground={false} playInBackground={false}
bufferConfig={{
minBufferMs: 15000,
maxBufferMs: 50000,
bufferForPlaybackMs: 2500,
bufferForPlaybackAfterRebufferMs: 5000,
cacheSizeMB: this.state.useCache ? 200 : 0,
}}
preventsDisplaySleepDuringVideoPlayback={true} preventsDisplaySleepDuringVideoPlayback={true}
poster={this.state.poster} poster={this.state.poster}
onPlaybackRateChange={this.onPlaybackRateChange} onPlaybackRateChange={this.onPlaybackRateChange}

View File

@ -72,6 +72,7 @@ export type BufferConfig = {
maxHeapAllocationPercent?: number; maxHeapAllocationPercent?: number;
minBackBufferMemoryReservePercent?: number; minBackBufferMemoryReservePercent?: number;
minBufferMemoryReservePercent?: number; minBufferMemoryReservePercent?: number;
cacheSizeMB?: number;
}; };
export enum SelectedTrackType { export enum SelectedTrackType {