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.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:
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();

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_VOLUME = "volume";
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_MAX_BUFFER_MS = "maxBufferMs";
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)
public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) {
int cacheSize = 0;
int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_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;
if (bufferConfig != null) {
cacheSize = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_CACHE_SIZE, 0);
minBufferMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_MIN_BUFFER_MS, minBufferMs);
maxBufferMs = ReactBridgeUtils.safeGetInt(bufferConfig, PROP_BUFFER_CONFIG_MAX_BUFFER_MS, maxBufferMs);
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);
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);
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 |
| 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 |
| 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:
@ -74,9 +74,12 @@ bufferConfig={{
bufferForPlaybackMs: 2500,
bufferForPlaybackAfterRebufferMs: 5000,
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`
<PlatformsList types={['tvOS']} />
@ -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.
@ -726,7 +728,7 @@ 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 |

View File

@ -1,17 +1,25 @@
# 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](<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.

View File

@ -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"
/>
<ToggleControl
isSelected={this.state.useCache}
onPress={() => {
this.setState({useCache: !this.state.useCache});
}}
selectedText="enable cache"
unselectedText="disable cache"
/>
</View>
) : null}
<ToggleControl
@ -864,6 +874,13 @@ class VideoPlayer extends Component {
selectedTextTrack={this.state.selectedTextTrack}
selectedAudioTrack={this.state.selectedAudioTrack}
playInBackground={false}
bufferConfig={{
minBufferMs: 15000,
maxBufferMs: 50000,
bufferForPlaybackMs: 2500,
bufferForPlaybackAfterRebufferMs: 5000,
cacheSizeMB: this.state.useCache ? 200 : 0,
}}
preventsDisplaySleepDuringVideoPlayback={true}
poster={this.state.poster}
onPlaybackRateChange={this.onPlaybackRateChange}

View File

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