Skip to content
This repository has been archived by the owner on May 16, 2024. It is now read-only.

Android exoplayer caching #7

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
21e635a
Add ExoPlayer cache
RalfNieuwenhuizen Jul 6, 2018
b695bc7
Create preloadVideo method
RalfNieuwenhuizen Jul 10, 2018
1d8bca9
Use CacheUtil to preload video
RalfNieuwenhuizen Jul 10, 2018
3b7a47d
Delete CacheDownloadService
RalfNieuwenhuizen Jul 13, 2018
713cffb
Cleanup imports
RalfNieuwenhuizen Jul 13, 2018
dc78bfb
Save video cache to /<cache>/video/
RalfNieuwenhuizen Jul 20, 2018
7662158
Expose exportVideo on Android
RalfNieuwenhuizen Jul 23, 2018
06c3ce8
Log amount of cached bytes
RalfNieuwenhuizen Jul 23, 2018
fdfae80
Add some notes
RalfNieuwenhuizen Jul 23, 2018
9a7f4af
Remove preloading methods
RalfNieuwenhuizen Jul 25, 2018
952909d
Remove autoformat
RalfNieuwenhuizen Jul 26, 2018
941ecc0
Revert exoplayer upgrade
RalfNieuwenhuizen Aug 1, 2018
e657d37
Merge branch 'master' into android-exoplayer-caching
RalfNieuwenhuizen Sep 14, 2018
74dd520
Replace CachingCounters by CacheKeyFactory
RalfNieuwenhuizen Jan 15, 2020
cf94740
Implement cacheKeyFactory correctly
RalfNieuwenhuizen Jan 15, 2020
65a30a7
Retry on video codec error
jochem725 Jul 15, 2020
ff7c1ff
Reject promise on every exception in video export
jaspermeijaard Oct 22, 2020
557e295
Remove unused import
jaspermeijaard Oct 22, 2020
77de90d
Remove test parameter
bryanvanwijk Nov 9, 2020
9a03f8c
Remove unreachable case
bryanvanwijk Nov 10, 2020
ce495d6
Return arguments from exportVideo method (#30)
jaspermeijaard Jun 14, 2021
977261c
Make exo player cache size and directory configurable (#31)
jaspermeijaard Jun 23, 2021
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
49 changes: 38 additions & 11 deletions Video.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import React, {Component} from 'react';
import React, {
Component
} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform} from 'react-native';
import {
StyleSheet,
requireNativeComponent,
NativeModules,
View,
ViewPropTypes,
Image,
Platform
} from 'react-native';
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
import TextTrackType from './TextTrackType';
import VideoResizeMode from './VideoResizeMode.js';
Expand All @@ -11,7 +21,14 @@ const styles = StyleSheet.create({
},
});

export { TextTrackType };
const {
ExoPlayerCache
} = NativeModules;

export {
TextTrackType,
ExoPlayerCache,
};

export default class Video extends Component {

Expand All @@ -26,17 +43,17 @@ export default class Video extends Component {
setNativeProps(nativeProps) {
this._root.setNativeProps(nativeProps);
}

toTypeString(x) {
switch (typeof x) {
case "object":
return x instanceof Date
? x.toISOString()
? x.toISOString()
: JSON.stringify(x); // object, null
case "undefined":
return "";
default: // boolean, number, string
return x.toString();
return x.toString();
}
}

Expand All @@ -59,16 +76,22 @@ export default class Video extends Component {
}
});
} else {
this.setNativeProps({ seek: time });
this.setNativeProps({
seek: time
});
}
};

presentFullscreenPlayer = () => {
this.setNativeProps({ fullscreen: true });
this.setNativeProps({
fullscreen: true
});
};

dismissFullscreenPlayer = () => {
this.setNativeProps({ fullscreen: false });
this.setNativeProps({
fullscreen: false
});
};

_assignRoot = (component) => {
Expand Down Expand Up @@ -101,7 +124,9 @@ export default class Video extends Component {

_onSeek = (event) => {
if (this.state.showPoster && !this.props.audioOnly) {
this.setState({showPoster: false});
this.setState({
showPoster: false
});
}

if (this.props.onSeek) {
Expand Down Expand Up @@ -165,7 +190,9 @@ export default class Video extends Component {

_onPlaybackRateChange = (event) => {
if (this.state.showPoster && event.nativeEvent.playbackRate !== 0 && !this.props.audioOnly) {
this.setState({showPoster: false});
this.setState({
showPoster: false
});
}

if (this.props.onPlaybackRateChange) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.brentvatne.exoplayer;

import android.content.Context;
import android.net.Uri;
import android.util.Log;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.upstream.cache.CacheUtil;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import java.io.IOException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

public class ExoPlayerCache extends ReactContextBaseJavaModule {

private static SimpleCache instance = null;
private static final String CACHE_KEY_PREFIX = "exoPlayerCacheKeyPrefix";
private static int maxCacheSizeBytes = -1; // Default no maximum size
private static String cacheSubDirectory = "";

public ExoPlayerCache(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public String getName() {
return "ExoPlayerCache";
}

@ReactMethod
public void setMaxCacheSize(final int bytes, final Promise promise) {
maxCacheSizeBytes = bytes;
promise.resolve(maxCacheSizeBytes);
}

@ReactMethod
public void setCacheSubDirectory(final String directory, final Promise promise) {
cacheSubDirectory = directory;
promise.resolve(cacheSubDirectory);
}

@ReactMethod
public void exportVideo(final String url, final Promise promise) {
Log.d(getName(), "exportVideo");

Thread exportThread = new Thread(new Runnable() {
@Override
public void run() {
Log.d(getName(), "Exporting...");
Log.d(getName(), url);
final Uri uri = Uri.parse(url);
final DataSpec dataSpec = new DataSpec(uri, 0, 100 * 1024 * 1024, null); // TODO won't work for video's over 100 MB
final SimpleCache downloadCache = ExoPlayerCache.getInstance(getReactApplicationContext());
CacheKeyFactory cacheKeyFactory = ds -> CACHE_KEY_PREFIX + "." + CacheUtil.generateKey(ds.uri);;

try {
CacheUtil.getCached(
dataSpec,
downloadCache,
cacheKeyFactory
);

DataSourceInputStream inputStream = new DataSourceInputStream(createDataSource(downloadCache), dataSpec);

File targetFile = new File(ExoPlayerCache.getCacheDir(getReactApplicationContext()) + "/" + uri.getLastPathSegment());
OutputStream outStream = new FileOutputStream(targetFile);

byte[] buffer = new byte[8 * 1024];
int bytesRead;
try {
while ((bytesRead = inputStream.read(buffer)) != -1) {
outStream.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
// TODO this exception should not be thrown
Log.d(getName(), "Read error");
e.printStackTrace();

throw e;
}

CacheUtil.getCached(
dataSpec,
downloadCache,
cacheKeyFactory
);

if (!targetFile.exists()) {
throw new Exception("Target file not present after writing bytes");
}

Log.d(getName(), "Export succeeded");
Log.d(getName(), targetFile.getPath());

WritableMap result = Arguments.createMap();
result.putString("path", targetFile.getPath());

promise.resolve(result);
} catch (Exception e) {
Log.d(getName(), "Export error");
e.printStackTrace();

String className = e.getClass().getSimpleName();
promise.reject(className, className + ": " + e.getMessage());
return;
}
}
}, "export_thread");
exportThread.start();
}

public static SimpleCache getInstance(Context context) {
if(instance == null) {
instance = new SimpleCache(
new File(ExoPlayerCache.getCacheDir(context) + cacheSubDirectory),
maxCacheSizeBytes == -1
? new NoOpCacheEvictor()
: new LeastRecentlyUsedCacheEvictor(maxCacheSizeBytes)
);
}
return instance;
}

private static String getCacheDir(Context context) {
return context.getCacheDir().toString() + "/video";
}

private DataSource createDataSource(Cache cache) {
return new CacheDataSourceFactory(cache, DataSourceUtil.getDefaultDataSourceFactory(
getReactApplicationContext(),
null,
null
)).createDataSource();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
import com.google.android.exoplayer2.metadata.Metadata;
Expand All @@ -60,7 +59,9 @@
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;

import java.net.CookieHandler;
Expand Down Expand Up @@ -96,6 +97,7 @@ class ReactExoplayerView extends FrameLayout implements
private Handler mainHandler;
private ExoPlayerView exoPlayerView;

private SimpleCache downloadCache;
private DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player;
private DefaultTrackSelector trackSelector;
Expand All @@ -109,6 +111,7 @@ class ReactExoplayerView extends FrameLayout implements
private boolean isPaused;
private boolean isBuffering;
private float rate = 1f;
private int codecRetries = 0;

private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
Expand Down Expand Up @@ -179,6 +182,8 @@ public void setId(int id) {
private void createViews() {
clearResumePosition();
mediaDataSourceFactory = buildDataSourceFactory(true);
downloadCache = ExoPlayerCache.getInstance(getContext());

mainHandler = new Handler();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
Expand Down Expand Up @@ -292,8 +297,8 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
case C.TYPE_HLS:
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, null);
case C.TYPE_OTHER:
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
mainHandler, null);
return new ExtractorMediaSource.Factory(new CacheDataSourceFactory(downloadCache, mediaDataSourceFactory))
.createMediaSource(uri);
default: {
throw new IllegalStateException("Unsupported type: " + type);
}
Expand Down Expand Up @@ -654,6 +659,28 @@ else if (e.type == ExoPlaybackException.TYPE_SOURCE) {
ex = e.getSourceException();
errorString = getResources().getString(R.string.unrecognized_media_format);
}

// Assumption: If we have a codec error it could be that it is still busy because another component has not yet unmounted.
// We retry this max 3 times with some delay and see if the error decreases.
int MAX_CODEC_RETRIES = 3;
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
if (this.codecRetries < MAX_CODEC_RETRIES) {
this.codecRetries += 1;

// Release the current player.
releasePlayer();

// Retry init after some delay.
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
initializePlayer();
}
}, 500);
return;
}
}

if (errorString != null) {
eventEmitter.error(errorString, ex);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package com.brentvatne.react;

import com.brentvatne.exoplayer.ReactExoplayerViewManager;
import com.brentvatne.exoplayer.ExoPlayerCache;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ReactVideoPackage implements ReactPackage {

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
List<NativeModule> modules = new ArrayList<>();

modules.add(new ExoPlayerCache(reactContext));

return modules;
}

// Deprecated RN 0.47
Expand Down