Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): cache #3514

Merged
merged 25 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
57a1654
feat: android cache
lovegaoshi Feb 1, 2024
09637b1
docs: bufferSize
lovegaoshi Feb 1, 2024
1734794
Revert "docs: bufferSize"
lovegaoshi Feb 5, 2024
47ddca6
fix: cacheSize name
lovegaoshi Feb 5, 2024
e61fe19
feat: singleton android cache
lovegaoshi Feb 19, 2024
f7cc2ad
fix: local cache resolve
lovegaoshi Mar 1, 2024
450f618
Merge branch 'master' of https://github.com/lovegaoshi/react-native-v…
lovegaoshi Mar 1, 2024
88b4bff
Merge branch 'master' of https://github.com/lovegaoshi/react-native-v…
lovegaoshi Mar 1, 2024
55c8ab1
Merge branch 'master' of https://github.com/lovegaoshi/react-native-v…
lovegaoshi Mar 8, 2024
e36de4e
fix: lint
lovegaoshi Mar 8, 2024
b4a4346
docs: android cache
lovegaoshi Mar 8, 2024
3eae85d
chore: merge conflict
lovegaoshi Mar 8, 2024
426d08c
fix: lint
lovegaoshi Mar 11, 2024
7830a2e
Merge branch 'master' into dev-android-cache
lovegaoshi Mar 11, 2024
b2de1b7
Merge branch 'master' into dev-android-cache
lovegaoshi Mar 13, 2024
59e39f7
Merge branch 'master' of https://github.com/lovegaoshi/react-native-v…
lovegaoshi Apr 1, 2024
f41ebd6
chore: useCache button
lovegaoshi Apr 1, 2024
eb0262e
Merge branch 'master' into dev-android-cache
lovegaoshi Apr 8, 2024
67c78f9
chore: fix state in the sample
freeboub Apr 16, 2024
51aac8c
fix: cache factory
lovegaoshi Apr 18, 2024
2ee85f1
Merge pull request #1 from freeboub/dev-android-cache-fix
lovegaoshi Apr 18, 2024
aae5647
Merge branch 'master' of https://github.com/lovegaoshi/react-native-v…
lovegaoshi Apr 18, 2024
230106d
Merge branch 'dev-android-cache' of https://github.com/lovegaoshi/rea…
lovegaoshi Apr 18, 2024
f1f27ce
Merge branch 'master' of https://github.com/lovegaoshi/react-native-v…
lovegaoshi Apr 27, 2024
629349e
chore: update cacheSizeMB docs
lovegaoshi Apr 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
freeboub marked this conversation as resolved.
Show resolved Hide resolved
// 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
freeboub marked this conversation as resolved.
Show resolved Hide resolved
simpleCache = SimpleCache(
File(context.cacheDir, "RNVCache"),
LeastRecentlyUsedCacheEvictor(
cacheSize.toLong() * 1024 * 1024
),
StandaloneDatabaseProvider(context)
)
cacheDataSourceFactory =
CacheDataSource.Factory()
.setCache(simpleCache!!)
.setUpstreamDataSourceFactory(factory)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -839,9 +841,17 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi
);
break;
case CONTENT_TYPE_OTHER:
mediaSourceFactory = new ProgressiveMediaSource.Factory(
freeboub marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand Down Expand Up @@ -2015,14 +2025,25 @@ 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;
bufferForPlaybackAfterRebufferMs = newBufferForPlaybackAfterRebufferMs;
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -402,6 +403,7 @@ public void setShutterColor(final ReactExoplayerView videoView, final Integer co

@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;
Expand All @@ -412,6 +414,7 @@ public void setBufferConfig(final ReactExoplayerView videoView, @Nullable Readab
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);
Expand All @@ -420,7 +423,7 @@ public void setBufferConfig(final ReactExoplayerView videoView, @Nullable Readab
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);
}
}

Expand Down
24 changes: 13 additions & 11 deletions docs/pages/component/props.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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']} />
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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:

Expand Down
22 changes: 15 additions & 7 deletions docs/pages/other/caching.md
Original file line number Diff line number Diff line change
@@ -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](<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!

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.
In a future release the cache might have more configurable options.
17 changes: 17 additions & 0 deletions examples/basic/src/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ interface StateType {
srcListId: number;
loop: boolean;
showRNVControls: boolean;
useCache: boolean;
poster?: string;
}

Expand Down Expand Up @@ -97,6 +98,7 @@ class VideoPlayer extends Component {
srcListId: 0,
loop: false,
showRNVControls: false,
useCache: false,
poster: undefined,
};

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions src/types/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type BufferConfig = {
maxHeapAllocationPercent?: number;
minBackBufferMemoryReservePercent?: number;
minBufferMemoryReservePercent?: number;
cacheSizeMB?: number;
};

export enum SelectedTrackType {
Expand Down
Loading