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:
mediaSourceFactory = new ProgressiveMediaSource.Factory( if (uri.toString().startsWith("file://") ||
mediaDataSourceFactory cacheDataSourceFactory == null) {
); mediaSourceFactory = new ProgressiveMediaSource.Factory(
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.
@ -725,14 +727,14 @@ 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 |
| paddingLeft | Adjust the left 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 | | 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 | | 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: Example:

View File

@ -1,19 +1,27 @@
# 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.
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! 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!

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 {