Merge pull request #2689 from crunchyroll/cr-android-improvements

Crunchyroll Android Improvements
This commit is contained in:
Eran Hammer 2022-06-08 22:23:07 -07:00 committed by GitHub
commit bf11bb95ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2389 additions and 124 deletions

View File

@ -1,7 +1,23 @@
## Changelog
### Version 6.0.0-alpha1
- Support disabling buffering [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Fix AudioFocus bug that could cause the player to stop responding to play/pause in some instances. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Fix player crashing when it is being cleared. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Add support for customising back buffer duration and handle network errors gracefully to prevent releasing the player when network is lost. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Allow player to be init before source is provided, and later update once a source is provided. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Adds handling for providing a empty source in order to stop playback and clear out any existing content [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Add support for detecting if format is supported and exclude unsupported resolutions from auto quality selection and video track info in RN. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Improve error handling [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Add support for L1 to L3 Widevine fallback if playback fails initially. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Reduce buffer size based on available heap [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Force garbage collection when there is no available memory [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Improve memory usage [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Support disabling screen recording [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Improved error capturing [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Fix DRM init crashes [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Improve progress reporting [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Fix progress loss when network connection is regained [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
- Add Google's maven repository to avoid build error [#2552](https://github.com/react-native-video/react-native-video/pull/2552)
- Fix iOS 15.4 HLS playback race condition [#2633](https://github.com/react-native-video/react-native-video/pull/2633)
- Fix app crash from NPE in Exoplayer error handler [#2575](https://github.com/react-native-video/react-native-video/pull/2575)

1621
README.md

File diff suppressed because it is too large Load Diff

View File

@ -100,6 +100,12 @@ export default class Video extends Component {
}
};
_onPlaybackStateChanged = (event) => {
if (this.props.onPlaybackStateChanged) {
this.props.onPlaybackStateChanged(event.nativeEvent);
}
};
_onLoad = (event) => {
// Need to hide poster here for windows as onReadyForDisplay is not implemented
if (Platform.OS === 'windows') {
@ -308,6 +314,7 @@ export default class Video extends Component {
requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {},
},
onVideoLoadStart: this._onLoadStart,
onVideoPlaybackStateChanged: this._onPlaybackStateChanged,
onVideoLoad: this._onLoad,
onVideoError: this._onError,
onVideoProgress: this._onProgress,
@ -464,6 +471,7 @@ Video.propTypes = {
maxBufferMs: PropTypes.number,
bufferForPlaybackMs: PropTypes.number,
bufferForPlaybackAfterRebufferMs: PropTypes.number,
maxHeapAllocationPercent: PropTypes.number,
}),
stereoPan: PropTypes.number,
rate: PropTypes.number,
@ -473,7 +481,9 @@ Video.propTypes = {
playWhenInactive: PropTypes.bool,
ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']),
reportBandwidth: PropTypes.bool,
contentStartTime: PropTypes.number,
disableFocus: PropTypes.bool,
disableBuffering: PropTypes.bool,
controls: PropTypes.bool,
audioOnly: PropTypes.bool,
currentTime: PropTypes.number,
@ -481,8 +491,10 @@ Video.propTypes = {
fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']),
progressUpdateInterval: PropTypes.number,
useTextureView: PropTypes.bool,
useSecureView: PropTypes.bool,
hideShutterView: PropTypes.bool,
onLoadStart: PropTypes.func,
onPlaybackStateChanged: PropTypes.func,
onLoad: PropTypes.func,
onBuffer: PropTypes.func,
onError: PropTypes.func,

View File

@ -9,16 +9,28 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
public class DefaultReactExoplayerConfig implements ReactExoplayerConfig {
private final DefaultBandwidthMeter bandwidthMeter;
private boolean disableDisconnectError = false;
public DefaultReactExoplayerConfig(Context context) {
this.bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
}
@Override
public LoadErrorHandlingPolicy buildLoadErrorHandlingPolicy(int minLoadRetryCount) {
if (this.disableDisconnectError) {
// Use custom error handling policy to prevent throwing an error when losing network connection
return new ReactExoplayerLoadErrorHandlingPolicy(minLoadRetryCount);
}
return new DefaultLoadErrorHandlingPolicy(minLoadRetryCount);
}
public void setDisableDisconnectError(boolean disableDisconnectError) {
this.disableDisconnectError = disableDisconnectError;
}
public boolean getDisableDisconnectError() {
return this.disableDisconnectError;
}
@Override
public DefaultBandwidthMeter getBandwidthMeter() {
return bandwidthMeter;

View File

@ -41,6 +41,7 @@ public final class ExoPlayerView extends FrameLayout {
private ViewGroup.LayoutParams layoutParams;
private boolean useTextureView = true;
private boolean useSecureView = false;
private boolean hideShutterView = false;
public ExoPlayerView(Context context) {
@ -103,7 +104,15 @@ public final class ExoPlayerView extends FrameLayout {
}
private void updateSurfaceView() {
View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
View view;
if (!useTextureView || useSecureView) {
view = new SurfaceView(context);
if (useSecureView) {
((SurfaceView)view).setSecure(true);
}
} else {
view = new TextureView(context);
}
view.setLayoutParams(layoutParams);
surfaceView = view;
@ -178,6 +187,13 @@ public final class ExoPlayerView extends FrameLayout {
}
}
public void useSecureView(boolean useSecureView) {
if (useSecureView != this.useSecureView) {
this.useSecureView = useSecureView;
updateSurfaceView();
}
}
public void setHideShutterView(boolean hideShutterView) {
this.hideShutterView = hideShutterView;
updateShutterViewVisibility();

View File

@ -9,5 +9,8 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
public interface ReactExoplayerConfig {
LoadErrorHandlingPolicy buildLoadErrorHandlingPolicy(int minLoadRetryCount);
void setDisableDisconnectError(boolean disableDisconnectError);
boolean getDisableDisconnectError();
DefaultBandwidthMeter getBandwidthMeter();
}

View File

@ -0,0 +1,36 @@
package com.brentvatne.exoplayer;
import java.io.IOException;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
import com.google.android.exoplayer2.C;
public final class ReactExoplayerLoadErrorHandlingPolicy extends DefaultLoadErrorHandlingPolicy {
private int minLoadRetryCount = Integer.MAX_VALUE;
public ReactExoplayerLoadErrorHandlingPolicy(int minLoadRetryCount) {
super(minLoadRetryCount);
this.minLoadRetryCount = minLoadRetryCount;
}
@Override
public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) {
if (
loadErrorInfo.exception instanceof HttpDataSourceException &&
(loadErrorInfo.exception.getMessage() == "Unable to connect" || loadErrorInfo.exception.getMessage() == "Software caused connection abort")
) {
// Capture the error we get when there is no network connectivity and keep retrying it
return 1000; // Retry every second
} else if(loadErrorInfo.errorCount < this.minLoadRetryCount) {
return Math.min((loadErrorInfo.errorCount - 1) * 1000, 5000); // Default timeout handling
} else {
return C.TIME_UNSET; // Done retrying and will return the error immediately
}
}
@Override
public int getMinimumLoadableRetryCount(int dataType) {
return Integer.MAX_VALUE;
}
}

View File

@ -2,10 +2,12 @@ package com.brentvatne.exoplayer;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
@ -26,10 +28,13 @@ import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.util.RNLog;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.drm.MediaDrmCallbackException;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
@ -42,7 +47,9 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.MediaDrmCallbackException;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
import com.google.android.exoplayer2.metadata.Metadata;
@ -70,7 +77,15 @@ 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.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.source.dash.DashUtil;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.source.dash.manifest.Descriptor;
import java.net.CookieHandler;
import java.net.CookieManager;
@ -79,6 +94,16 @@ import java.util.ArrayList;
import java.util.Locale;
import java.util.UUID;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.List;
import java.lang.Thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.lang.Integer;
@SuppressLint("ViewConstructor")
class ReactExoplayerView extends FrameLayout implements
@ -90,6 +115,10 @@ class ReactExoplayerView extends FrameLayout implements
MetadataOutput,
DrmSessionEventListener {
public static final double DEFAULT_MAX_HEAP_ALLOCATION_PERCENT = 1;
public static final double DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE = 0;
public static final double DEFAULT_MIN_BUFFER_MEMORY_RESERVE = 0;
private static final String TAG = "ReactExoplayerView";
private static final CookieManager DEFAULT_COOKIE_MANAGER;
@ -128,15 +157,21 @@ class ReactExoplayerView extends FrameLayout implements
private int minLoadRetryCount = 3;
private int maxBitRate = 0;
private long seekTime = C.TIME_UNSET;
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;
private int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
private double maxHeapAllocationPercent = ReactExoplayerView.DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
private double minBackBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE;
private double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
private Handler mainHandler;
// Props from React
private int backBufferDurationMs = DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS;
private Uri srcUri;
private String extension;
private boolean repeat;
@ -148,6 +183,9 @@ class ReactExoplayerView extends FrameLayout implements
private Dynamic textTrackValue;
private ReadableArray textTracks;
private boolean disableFocus;
private boolean disableBuffering;
private long contentStartTime = -1L;
private boolean disableDisconnectError;
private boolean preventsDisplaySleepDuringVideoPlayback = true;
private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false;
@ -388,116 +426,262 @@ class ReactExoplayerView extends FrameLayout implements
view.layout(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight());
}
private class RNVLoadControl extends DefaultLoadControl {
private int availableHeapInBytes = 0;
private Runtime runtime;
public RNVLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs, int targetBufferBytes, boolean prioritizeTimeOverSizeThresholds, int backBufferDurationMs, boolean retainBackBufferFromKeyframe) {
super(allocator,
minBufferMs,
maxBufferMs,
bufferForPlaybackMs,
bufferForPlaybackAfterRebufferMs,
targetBufferBytes,
prioritizeTimeOverSizeThresholds,
backBufferDurationMs,
retainBackBufferFromKeyframe);
runtime = Runtime.getRuntime();
ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(themedReactContext.ACTIVITY_SERVICE);
availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeapAllocationPercent * 1024 * 1024);
}
@Override
public boolean shouldContinueLoading(long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
if (ReactExoplayerView.this.disableBuffering) {
return false;
}
int loadedBytes = getAllocator().getTotalBytesAllocated();
boolean isHeapReached = availableHeapInBytes > 0 && loadedBytes >= availableHeapInBytes;
if (isHeapReached) {
return false;
}
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long freeMemory = runtime.maxMemory() - usedMemory;
long reserveMemory = (long)minBufferMemoryReservePercent * runtime.maxMemory();
long bufferedMs = bufferedDurationUs / (long)1000;
if (reserveMemory > freeMemory && bufferedMs > 2000) {
// We don't have enough memory in reserve so we stop buffering to allow other components to use it instead
return false;
}
if (runtime.freeMemory() == 0) {
Log.w("ExoPlayer Warning", "Free memory reached 0, forcing garbage collection");
runtime.gc();
return false;
}
return super.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed);
}
}
private void startBufferCheckTimer() {
SimpleExoPlayer player = this.player;
VideoEventEmitter eventEmitter = this.eventEmitter;
Handler mainHandler = this.mainHandler;
}
private void initializePlayer() {
ReactExoplayerView self = this;
Activity activity = themedReactContext.getCurrentActivity();
// This ensures all props have been settled, to avoid async racing conditions.
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (player == null) {
ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
trackSelector.setParameters(trackSelector.buildUponParameters()
.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate));
try {
if (player == null) {
// Initialize core configuration and listeners
initializePlayerCore(self);
}
if (playerNeedsSource && srcUri != null) {
exoPlayerView.invalidateAspectRatio();
// DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
ExecutorService es = Executors.newSingleThreadExecutor();
es.execute(new Runnable() {
@Override
public void run() {
// DRM initialization must run on a different thread
DrmSessionManager drmSessionManager = initializePlayerDrm(self);
if (drmSessionManager == null && self.drmUUID != null) {
// Failed to intialize DRM session manager - cannot continue
Log.e("ExoPlayer Exception", "Failed to initialize DRM Session Manager Framework!");
eventEmitter.error("Failed to initialize DRM Session Manager Framework!", new Exception("DRM Session Manager Framework failure!"), "3003");
return;
}
DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
DefaultLoadControl.Builder defaultLoadControlBuilder = new DefaultLoadControl.Builder();
defaultLoadControlBuilder.setAllocator(allocator);
defaultLoadControlBuilder.setBufferDurationsMs(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs);
defaultLoadControlBuilder.setTargetBufferBytes(-1);
defaultLoadControlBuilder.setPrioritizeTimeOverSizeThresholds(true);
DefaultLoadControl defaultLoadControl = defaultLoadControlBuilder.createDefaultLoadControl();
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(getContext())
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF);
player = new SimpleExoPlayer.Builder(getContext(), renderersFactory)
.setTrackSelector(trackSelector)
.setBandwidthMeter(bandwidthMeter)
.setLoadControl(defaultLoadControl)
.build();
player.addListener(self);
player.addMetadataOutput(self);
exoPlayerView.setPlayer(player);
audioBecomingNoisyReceiver.setListener(self);
bandwidthMeter.addEventListener(new Handler(), self);
setPlayWhenReady(!isPaused);
playerNeedsSource = true;
// Initialize handler to run on the main thread
activity.runOnUiThread(new Runnable() {
public void run() {
try {
// Source initialization must run on the main thread
initializePlayerSource(self, drmSessionManager);
} catch (Exception ex) {
self.playerNeedsSource = true;
Log.e("ExoPlayer Exception", "Failed to initialize Player!");
Log.e("ExoPlayer Exception", ex.toString());
self.eventEmitter.error(ex.toString(), ex, "1001");
}
}
});
}
});
} else if (srcUri != null) {
initializePlayerSource(self, null);
}
PlaybackParameters params = new PlaybackParameters(rate, 1f);
player.setPlaybackParameters(params);
} catch (Exception ex) {
self.playerNeedsSource = true;
Log.e("ExoPlayer Exception", "Failed to initialize Player!");
Log.e("ExoPlayer Exception", ex.toString());
eventEmitter.error(ex.toString(), ex, "1001");
}
if (playerNeedsSource && srcUri != null) {
exoPlayerView.invalidateAspectRatio();
// DRM
DrmSessionManager drmSessionManager = null;
if (self.drmUUID != null) {
try {
drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl,
self.drmLicenseHeader);
} catch (UnsupportedDrmException e) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
eventEmitter.error(getResources().getString(errorStringId), e);
return;
}
}
// End DRM
ArrayList<MediaSource> mediaSourceList = buildTextSources();
MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager);
MediaSource mediaSource;
if (mediaSourceList.size() == 0) {
mediaSource = videoSource;
} else {
mediaSourceList.add(0, videoSource);
MediaSource[] textSourceArray = mediaSourceList.toArray(
new MediaSource[mediaSourceList.size()]
);
mediaSource = new MergingMediaSource(textSourceArray);
}
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition);
}
player.prepare(mediaSource, !haveResumePosition, false);
playerNeedsSource = false;
reLayout(exoPlayerView);
eventEmitter.loadStart();
loadVideoStarted = true;
}
// Initializing the playerControlView
initializePlayerControl();
setControls(controls);
applyModifiers();
}
}, 1);
}
private DrmSessionManager buildDrmSessionManager(UUID uuid,
String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
private void initializePlayerCore(ReactExoplayerView self) {
ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
self.trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
self.trackSelector.setParameters(trackSelector.buildUponParameters()
.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate));
DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
RNVLoadControl loadControl = new RNVLoadControl(
allocator,
minBufferMs,
maxBufferMs,
bufferForPlaybackMs,
bufferForPlaybackAfterRebufferMs,
-1,
true,
backBufferDurationMs,
DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME
);
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(getContext())
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF);
player = new SimpleExoPlayer.Builder(getContext(), renderersFactory)
.setTrackSelector(self.trackSelector)
.setBandwidthMeter(bandwidthMeter)
.setLoadControl(loadControl)
.build();
player.addListener(self);
player.addMetadataOutput(self);
exoPlayerView.setPlayer(player);
audioBecomingNoisyReceiver.setListener(self);
bandwidthMeter.addEventListener(new Handler(), self);
setPlayWhenReady(!isPaused);
playerNeedsSource = true;
PlaybackParameters params = new PlaybackParameters(rate, 1f);
player.setPlaybackParameters(params);
}
private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) {
DrmSessionManager drmSessionManager = null;
if (self.drmUUID != null) {
try {
drmSessionManager = self.buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl,
self.drmLicenseHeader);
} catch (UnsupportedDrmException e) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
eventEmitter.error(getResources().getString(errorStringId), e, "3003");
return null;
}
}
return drmSessionManager;
}
private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) {
ArrayList<MediaSource> mediaSourceList = buildTextSources();
MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager);
MediaSource mediaSource;
if (mediaSourceList.size() == 0) {
mediaSource = videoSource;
} else {
mediaSourceList.add(0, videoSource);
MediaSource[] textSourceArray = mediaSourceList.toArray(
new MediaSource[mediaSourceList.size()]
);
mediaSource = new MergingMediaSource(textSourceArray);
}
// wait for player to be set
while (player == null) {
try {
wait();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
Log.e("ExoPlayer Exception", ex.toString());
}
}
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition);
}
player.prepare(mediaSource, !haveResumePosition, false);
playerNeedsSource = false;
reLayout(exoPlayerView);
eventEmitter.loadStart();
loadVideoStarted = true;
finishPlayerInitialization();
}
private void finishPlayerInitialization() {
// Initializing the playerControlView
initializePlayerControl();
setControls(controls);
applyModifiers();
startBufferCheckTimer();
}
private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
return buildDrmSessionManager(uuid, licenseUrl, keyRequestPropertiesArray, 0);
}
private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, int retryCount) throws UnsupportedDrmException {
if (Util.SDK_INT < 18) {
return null;
}
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false));
if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
keyRequestPropertiesArray[i + 1]);
try {
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false));
if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
}
}
FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
if (hasDrmFailed) {
// When DRM fails using L1 we want to switch to L3
mediaDrm.setPropertyString("securityLevel", "L3");
}
return new DefaultDrmSessionManager(uuid, mediaDrm, drmCallback, null, false, 3);
} catch(UnsupportedDrmException ex) {
// Unsupported DRM exceptions are handled by the calling method
throw ex;
} catch (Exception ex) {
if (retryCount < 3) {
// Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason
return buildDrmSessionManager(uuid, licenseUrl, keyRequestPropertiesArray, ++retryCount);
}
// Handle the unknow exception and emit to JS
eventEmitter.error(ex.toString(), ex, "3006");
return null;
}
return new DefaultDrmSessionManager(uuid,
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, false, 3);
}
private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager) {
if (uri == null) {
throw new IllegalStateException("Invalid video uri");
}
int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension
: uri.getLastPathSegment());
config.setDisableDisconnectError(this.disableDisconnectError);
switch (type) {
case C.TYPE_SS:
return new SsMediaSource.Factory(
@ -617,7 +801,6 @@ class ReactExoplayerView extends FrameLayout implements
default:
break;
}
} else {
initializePlayer();
}
@ -757,6 +940,10 @@ class ReactExoplayerView extends FrameLayout implements
onBuffering(false);
startProgressHandler();
videoLoaded();
if (selectTrackWhenReady && isUsingContentResolution) {
selectTrackWhenReady = false;
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
}
// Setting the visibility for the playerControlView
if (playerControlView != null) {
playerControlView.show();
@ -799,14 +986,34 @@ class ReactExoplayerView extends FrameLayout implements
int width = videoFormat != null ? videoFormat.width : 0;
int height = videoFormat != null ? videoFormat.height : 0;
String trackId = videoFormat != null ? videoFormat.id : "-1";
eventEmitter.load(player.getDuration(), player.getCurrentPosition(), width, height,
getAudioTrackInfo(), getTextTrackInfo(), getVideoTrackInfo(), trackId);
// Properties that must be accessed on the main thread
long duration = player.getDuration();
long currentPosition = player.getCurrentPosition();
WritableArray audioTrackInfo = getAudioTrackInfo();
WritableArray textTrackInfo = getTextTrackInfo();
int trackRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_VIDEO);
ExecutorService es = Executors.newSingleThreadExecutor();
es.execute(new Runnable() {
@Override
public void run() {
// To prevent ANRs caused by getVideoTrackInfo we run this on a different thread and notify the player only when we're done
eventEmitter.load(duration, currentPosition, width, height,
audioTrackInfo, textTrackInfo, getVideoTrackInfo(trackRendererIndex), trackId);
}
});
}
}
private WritableArray getAudioTrackInfo() {
WritableArray audioTracks = Arguments.createArray();
if (trackSelector == null) {
// Likely player is unmounting so no audio tracks are available anymore
return audioTracks;
}
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
int index = getTrackRendererIndex(C.TRACK_TYPE_AUDIO);
if (info == null || index == C.INDEX_UNSET) {
@ -827,34 +1034,114 @@ class ReactExoplayerView extends FrameLayout implements
}
return audioTracks;
}
private WritableArray getVideoTrackInfo() {
private WritableArray getVideoTrackInfo(int trackRendererIndex) {
if (this.contentStartTime != -1L) {
WritableArray contentVideoTracks = this.getVideoTrackInfoFromManifest();
if (contentVideoTracks != null) {
isUsingContentResolution = true;
return contentVideoTracks;
}
}
WritableArray videoTracks = Arguments.createArray();
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
int index = getTrackRendererIndex(C.TRACK_TYPE_VIDEO);
if (info == null || index == C.INDEX_UNSET) {
if (info == null || trackRendererIndex == C.INDEX_UNSET) {
return videoTracks;
}
TrackGroupArray groups = info.getTrackGroups(index);
TrackGroupArray groups = info.getTrackGroups(trackRendererIndex);
for (int i = 0; i < groups.length; ++i) {
TrackGroup group = groups.get(i);
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
Format format = group.getFormat(trackIndex);
WritableMap videoTrack = Arguments.createMap();
videoTrack.putInt("width", format.width == Format.NO_VALUE ? 0 : format.width);
videoTrack.putInt("height",format.height == Format.NO_VALUE ? 0 : format.height);
videoTrack.putInt("bitrate", format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
videoTrack.putString("codecs", format.codecs != null ? format.codecs : "");
videoTrack.putString("trackId",
format.id == null ? String.valueOf(trackIndex) : format.id);
videoTracks.pushMap(videoTrack);
if (isFormatSupported(format)) {
WritableMap videoTrack = Arguments.createMap();
videoTrack.putInt("width", format.width == Format.NO_VALUE ? 0 : format.width);
videoTrack.putInt("height",format.height == Format.NO_VALUE ? 0 : format.height);
videoTrack.putInt("bitrate", format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
videoTrack.putString("codecs", format.codecs != null ? format.codecs : "");
videoTrack.putString("trackId", format.id == null ? String.valueOf(trackIndex) : format.id);
videoTracks.pushMap(videoTrack);
}
}
}
return videoTracks;
}
private WritableArray getVideoTrackInfoFromManifest() {
return this.getVideoTrackInfoFromManifest(0);
}
// We need retry count to in case where minefest request fails from poor network conditions
private WritableArray getVideoTrackInfoFromManifest(int retryCount) {
ExecutorService es = Executors.newSingleThreadExecutor();
final DataSource dataSource = this.mediaDataSourceFactory.createDataSource();
final Uri sourceUri = this.srcUri;
final long startTime = this.contentStartTime * 1000 - 100; // s -> ms with 100ms offset
Future<WritableArray> result = es.submit(new Callable<WritableArray>() {
DataSource ds = dataSource;
Uri uri = sourceUri;
long startTimeUs = startTime * 1000; // ms -> us
public WritableArray call() throws Exception {
WritableArray videoTracks = Arguments.createArray();
try {
DashManifest manifest = DashUtil.loadManifest(this.ds, this.uri);
int periodCount = manifest.getPeriodCount();
for (int i = 0; i < periodCount; i++) {
Period period = manifest.getPeriod(i);
for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets.size(); adaptationIndex++) {
AdaptationSet adaptation = period.adaptationSets.get(adaptationIndex);
if (adaptation.type != C.TRACK_TYPE_VIDEO) {
continue;
}
boolean hasFoundContentPeriod = false;
for (int representationIndex = 0; representationIndex < adaptation.representations.size(); representationIndex++) {
Representation representation = adaptation.representations.get(representationIndex);
Format format = representation.format;
if (representation.presentationTimeOffsetUs <= startTimeUs) {
break;
}
hasFoundContentPeriod = true;
WritableMap videoTrack = Arguments.createMap();
videoTrack.putInt("width", format.width == Format.NO_VALUE ? 0 : format.width);
videoTrack.putInt("height",format.height == Format.NO_VALUE ? 0 : format.height);
videoTrack.putInt("bitrate", format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
videoTrack.putString("codecs", format.codecs != null ? format.codecs : "");
videoTrack.putString("trackId",
format.id == null ? String.valueOf(representationIndex) : format.id);
if (isFormatSupported(format)) {
videoTracks.pushMap(videoTrack);
}
}
if (hasFoundContentPeriod) {
return videoTracks;
}
}
}
} catch (Exception e) {}
return null;
}
});
try {
WritableArray results = result.get(3000, TimeUnit.MILLISECONDS);
if (results == null && retryCount < 1) {
return this.getVideoTrackInfoFromManifest(++retryCount);
}
es.shutdown();
return results;
} catch (Exception e) {}
return null;
}
private WritableArray getTextTrackInfo() {
WritableArray textTracks = Arguments.createArray();
@ -898,12 +1185,30 @@ class ReactExoplayerView extends FrameLayout implements
// which they seeked.
updateResumePosition();
}
if (isUsingContentResolution) {
// Discontinuity events might have a different track list so we update the selected track
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
selectTrackWhenReady = true;
}
// When repeat is turned on, reaching the end of the video will not cause a state change
// so we need to explicitly detect it.
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
&& player.getRepeatMode() == Player.REPEAT_MODE_ONE) {
eventEmitter.end();
}
}
@Override
public void onPlaybackStateChanged(int playbackState) {
if (playbackState == Player.STATE_READY && seekTime != C.TIME_UNSET) {
eventEmitter.seek(player.getCurrentPosition(), seekTime);
seekTime = C.TIME_UNSET;
if (isUsingContentResolution) {
// We need to update the selected track to make sure that it still matches user selection if track list has changed in this period
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
}
}
}
@Override
@ -911,12 +1216,6 @@ class ReactExoplayerView extends FrameLayout implements
// Do nothing.
}
@Override
public void onSeekProcessed() {
eventEmitter.seek(player.getCurrentPosition(), seekTime);
seekTime = C.TIME_UNSET;
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
// Do nothing.
@ -937,9 +1236,16 @@ class ReactExoplayerView extends FrameLayout implements
eventEmitter.playbackRateChange(params.speed);
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
eventEmitter.playbackStateChanged(isPlaying);
}
@Override
public void onPlayerError(ExoPlaybackException e) {
String errorString = "ExoPlaybackException type : " + e.type;
String errorCode = "2001"; // Playback error code 2xxx (2001 - unknown playback exception)
boolean needsReInitialization = false;
Exception ex = e;
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
Exception cause = e.getRendererException();
@ -950,30 +1256,82 @@ class ReactExoplayerView extends FrameLayout implements
if (decoderInitializationException.codecInfo == null
|| decoderInitializationException.codecInfo.name == null) {
if (decoderInitializationException.getCause() instanceof MediaCodecUtil.DecoderQueryException) {
errorCode = "2011";
errorString = getResources().getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
errorCode = "2012";
errorString = getResources().getString(R.string.error_no_secure_decoder,
decoderInitializationException.mimeType);
} else {
errorCode = "2013";
errorString = getResources().getString(R.string.error_no_decoder,
decoderInitializationException.mimeType);
}
} else {
errorCode = "2014";
errorString = getResources().getString(R.string.error_instantiating_decoder,
decoderInitializationException.codecInfo.name);
}
}
}
else if (e.type == ExoPlaybackException.TYPE_SOURCE) {
// Re-initialization improves recovery speed and properly resumes
needsReInitialization = true;
errorString = getResources().getString(R.string.unrecognized_media_format);
Exception cause = e.getSourceException();
if (cause instanceof DefaultDrmSessionManager.MissingSchemeDataException) {
errorCode = "3004";
errorString = getResources().getString(R.string.unrecognized_media_format);
} else if(cause instanceof MediaDrmCallbackException || cause instanceof DrmSessionException) {
errorCode = "3005";
errorString = getResources().getString(R.string.unrecognized_media_format);
// DrmSessionExceptions can be caused by a lot internal reasons for failure, in most cases they can be safely retried and playback will recover
if (!hasDrmFailed || cause instanceof DrmSessionException) {
// When DRM fails to reach the app level certificate server it will fail with a source error so we assume that it is DRM related and try one more time
hasDrmFailed = true;
playerNeedsSource = true;
updateResumePosition();
initializePlayer();
setPlayWhenReady(true);
return;
}
} else if (cause instanceof HttpDataSource.HttpDataSourceException) {
// this exception happens when connectivity is lost
updateResumePosition();
initializePlayer();
setPlayWhenReady(true);
return;
} else {
errorCode = "2021";
errorString = getResources().getString(R.string.unrecognized_media_format);
}
if (cause != null) {
Throwable rootCause = cause.getCause();
if (rootCause instanceof MediaDrmCallbackException) {
errorCode = "3005";
errorString = getResources().getString(R.string.unrecognized_media_format);
if (!hasDrmFailed) {
// When DRM fails to reach the app level certificate server it will fail with a source error so we assume that it is DRM related and try one more time
hasDrmFailed = true;
playerNeedsSource = true;
updateResumePosition();
initializePlayer();
setPlayWhenReady(true);
return;
}
}
}
}
eventEmitter.error(errorString, ex);
eventEmitter.error(errorString, ex, errorCode);
playerNeedsSource = true;
if (isBehindLiveWindow(e)) {
clearResumePosition();
initializePlayer();
} else {
updateResumePosition();
if (needsReInitialization) {
initializePlayer();
}
}
}
@ -1015,7 +1373,7 @@ class ReactExoplayerView extends FrameLayout implements
public void setSrc(final Uri uri, final String extension, Map<String, String> headers) {
if (uri != null) {
boolean isSourceEqual = uri.equals(srcUri);
hasDrmFailed = false;
this.srcUri = uri;
this.extension = extension;
this.requestHeaders = headers;
@ -1051,7 +1409,6 @@ class ReactExoplayerView extends FrameLayout implements
public void setRawSrc(final Uri uri, final String extension) {
if (uri != null) {
boolean isSourceEqual = uri.equals(srcUri);
this.srcUri = uri;
this.extension = extension;
this.mediaDataSourceFactory = buildDataSourceFactory(true);
@ -1147,14 +1504,51 @@ class ReactExoplayerView extends FrameLayout implements
int height = value.asInt();
for (int i = 0; i < groups.length; ++i) { // Search for the exact height
TrackGroup group = groups.get(i);
Format closestFormat = null;
int closestTrackIndex = -1;
boolean usingExactMatch = false;
for (int j = 0; j < group.length; j++) {
Format format = group.getFormat(j);
if (format.height == height) {
groupIndex = i;
tracks[0] = j;
closestFormat = null;
closestTrackIndex = -1;
usingExactMatch = true;
break;
} else if (isUsingContentResolution) {
// When using content resolution rather than ads, we need to try and find the closest match if there is no exact match
if (closestFormat != null) {
if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) {
// Higher quality match
closestFormat = format;
closestTrackIndex = j;
}
} else if(format.height < height) {
closestFormat = format;
closestTrackIndex = j;
}
}
}
// This is a fallback if the new period contains only higher resolutions than the user has selected
if (closestFormat == null && isUsingContentResolution && !usingExactMatch) {
// No close match found - so we pick the lowest quality
int minHeight = Integer.MAX_VALUE;
for (int j = 0; j < group.length; j++) {
Format format = group.getFormat(j);
if (format.height < minHeight) {
minHeight = format.height;
groupIndex = i;
tracks[0] = j;
}
}
}
// Selecting the closest match found
if (closestFormat != null && closestTrackIndex != -1) {
// We found the closest match instead of an exact one
groupIndex = i;
tracks[0] = closestTrackIndex;
}
}
} else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
// Use system settings if possible
@ -1170,10 +1564,36 @@ class ReactExoplayerView extends FrameLayout implements
if (groupIndex == C.INDEX_UNSET && trackType == C.TRACK_TYPE_VIDEO && groups.length != 0) { // Video auto
// Add all tracks as valid options for ABR to choose from
TrackGroup group = groups.get(0);
tracks = new int[group.length];
int[] allTracks = new int[group.length];
groupIndex = 0;
for (int j = 0; j < group.length; j++) {
tracks[j] = j;
allTracks[j] = j;
}
// Valiate list of all tracks and add only supported formats
int supportedFormatLength = 0;
ArrayList<Integer> supportedTrackList = new ArrayList<Integer>();
for (int g = 0; g < allTracks.length; g++) {
Format format = group.getFormat(g);
if (isFormatSupported(format)) {
supportedFormatLength++;
}
}
if (allTracks.length == 1) {
// With only one tracks we can't remove any tracks so attempt to play it anyway
tracks = allTracks;
} else {
tracks = new int[supportedFormatLength + 1];
int o = 0;
for (int k = 0; k < allTracks.length; k++) {
Format format = group.getFormat(k);
if (isFormatSupported(format)) {
tracks[o] = allTracks[k];
supportedTrackList.add(allTracks[k]);
o++;
}
}
}
}
@ -1191,6 +1611,25 @@ class ReactExoplayerView extends FrameLayout implements
trackSelector.setParameters(selectionParameters);
}
private boolean isFormatSupported(Format format) {
int width = format.width == Format.NO_VALUE ? 0 : format.width;
int height = format.height == Format.NO_VALUE ? 0 : format.height;
float frameRate = format.frameRate == Format.NO_VALUE ? 0 : format.frameRate;
String mimeType = format.sampleMimeType;
if (mimeType == null) {
return true;
}
boolean isSupported = false;
try {
MediaCodecInfo codecInfo = MediaCodecUtil.getDecoderInfo(mimeType, false, false);
isSupported = codecInfo.isVideoSizeAndRateSupportedV21(width, height, frameRate);
} catch (Exception e) {
// Failed to get decoder info - assume it is supported
isSupported = true;
}
return isSupported;
}
private int getGroupIndexForDefaultLocale(TrackGroupArray groups) {
if (groups.length == 0){
return C.INDEX_UNSET;
@ -1291,6 +1730,32 @@ class ReactExoplayerView extends FrameLayout implements
this.disableFocus = disableFocus;
}
public void setBackBufferDurationMs(int backBufferDurationMs) {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long freeMemory = runtime.maxMemory() - usedMemory;
long reserveMemory = (long)minBackBufferMemoryReservePercent * runtime.maxMemory();
if (reserveMemory > freeMemory) {
// We don't have enough memory in reserve so we will
Log.w("ExoPlayer Warning", "Not enough reserve memory, setting back buffer to 0ms to reduce memory pressure!");
this.backBufferDurationMs = 0;
return;
}
this.backBufferDurationMs = backBufferDurationMs;
}
public void setContentStartTime(int contentStartTime) {
this.contentStartTime = (long)contentStartTime;
}
public void setDisableBuffering(boolean disableBuffering) {
this.disableBuffering = disableBuffering;
}
public void setDisableDisconnectError(boolean disableDisconnectError) {
this.disableDisconnectError = disableDisconnectError;
}
public void setFullscreen(boolean fullscreen) {
if (fullscreen == isFullscreen) {
return; // Avoid generating events when nothing is changing
@ -1329,15 +1794,22 @@ class ReactExoplayerView extends FrameLayout implements
exoPlayerView.setUseTextureView(finallyUseTextureView);
}
public void useSecureView(boolean useSecureView) {
exoPlayerView.useSecureView(useSecureView);
}
public void setHideShutterView(boolean hideShutterView) {
exoPlayerView.setHideShutterView(hideShutterView);
}
public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs) {
public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs, double newMaxHeapAllocationPercent, double newMinBackBufferMemoryReservePercent, double newMinBufferMemoryReservePercent) {
minBufferMs = newMinBufferMs;
maxBufferMs = newMaxBufferMs;
bufferForPlaybackMs = newBufferForPlaybackMs;
bufferForPlaybackAfterRebufferMs = newBufferForPlaybackAfterRebufferMs;
maxHeapAllocationPercent = newMaxHeapAllocationPercent;
minBackBufferMemoryReservePercent = newMinBackBufferMemoryReservePercent;
minBufferMemoryReservePercent = newMinBufferMemoryReservePercent;
releasePlayer();
initializePlayer();
}
@ -1363,7 +1835,7 @@ class ReactExoplayerView extends FrameLayout implements
@Override
public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, Exception e) {
Log.d("DRM Info", "onDrmSessionManagerError");
eventEmitter.error("onDrmSessionManagerError", e);
eventEmitter.error("onDrmSessionManagerError", e, "3002");
}
@Override

View File

@ -49,11 +49,15 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_PAUSED = "paused";
private static final String PROP_MUTED = "muted";
private static final String PROP_VOLUME = "volume";
private static final String PROP_BACK_BUFFER_DURATION_MS = "backBufferDurationMs";
private static final String PROP_BUFFER_CONFIG = "bufferConfig";
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";
private static final String PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs";
private static final String PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent";
private static final String PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent";
private static final String PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent";
private static final String PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK = "preventsDisplaySleepDuringVideoPlayback";
private static final String PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval";
private static final String PROP_REPORT_BANDWIDTH = "reportBandwidth";
@ -62,9 +66,13 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount";
private static final String PROP_MAXIMUM_BIT_RATE = "maxBitRate";
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
private static final String PROP_CONTENT_START_TIME = "contentStartTime";
private static final String PROP_DISABLE_FOCUS = "disableFocus";
private static final String PROP_DISABLE_BUFFERING = "disableBuffering";
private static final String PROP_DISABLE_DISCONNECT_ERROR = "disableDisconnectError";
private static final String PROP_FULLSCREEN = "fullscreen";
private static final String PROP_USE_TEXTURE_VIEW = "useTextureView";
private static final String PROP_SECURE_VIEW = "useSecureView";
private static final String PROP_SELECTED_VIDEO_TRACK = "selectedVideoTrack";
private static final String PROP_SELECTED_VIDEO_TRACK_TYPE = "type";
private static final String PROP_SELECTED_VIDEO_TRACK_VALUE = "value";
@ -294,6 +302,26 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
videoView.setDisableFocus(disableFocus);
}
@ReactProp(name = PROP_BACK_BUFFER_DURATION_MS, defaultInt = 0)
public void setBackBufferDurationMs(final ReactExoplayerView videoView, final int backBufferDurationMs) {
videoView.setBackBufferDurationMs(backBufferDurationMs);
}
@ReactProp(name = PROP_CONTENT_START_TIME, defaultInt = 0)
public void setContentStartTime(final ReactExoplayerView videoView, final int contentStartTime) {
videoView.setContentStartTime(contentStartTime);
}
@ReactProp(name = PROP_DISABLE_BUFFERING, defaultBoolean = false)
public void setDisableBuffering(final ReactExoplayerView videoView, final boolean disableBuffering) {
videoView.setDisableBuffering(disableBuffering);
}
@ReactProp(name = PROP_DISABLE_DISCONNECT_ERROR, defaultBoolean = false)
public void setDisableDisconnectError(final ReactExoplayerView videoView, final boolean disableDisconnectError) {
videoView.setDisableDisconnectError(disableDisconnectError);
}
@ReactProp(name = PROP_FULLSCREEN, defaultBoolean = false)
public void setFullscreen(final ReactExoplayerView videoView, final boolean fullscreen) {
videoView.setFullscreen(fullscreen);
@ -304,6 +332,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
videoView.setUseTextureView(useTextureView);
}
@ReactProp(name = PROP_SECURE_VIEW, defaultBoolean = true)
public void useSecureView(final ReactExoplayerView videoView, final boolean useSecureView) {
videoView.useSecureView(useSecureView);
}
@ReactProp(name = PROP_HIDE_SHUTTER_VIEW, defaultBoolean = false)
public void setHideShutterView(final ReactExoplayerView videoView, final boolean hideShutterView) {
videoView.setHideShutterView(hideShutterView);
@ -320,6 +353,10 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
double maxHeapAllocationPercent = ReactExoplayerView.DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
double minBackBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE;
double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
if (bufferConfig != null) {
minBufferMs = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BUFFER_MS)
? bufferConfig.getInt(PROP_BUFFER_CONFIG_MIN_BUFFER_MS) : minBufferMs;
@ -329,7 +366,13 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
? bufferConfig.getInt(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS) : bufferForPlaybackMs;
bufferForPlaybackAfterRebufferMs = bufferConfig.hasKey(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
? bufferConfig.getInt(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) : bufferForPlaybackAfterRebufferMs;
videoView.setBufferConfig(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs);
maxHeapAllocationPercent = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT)
? bufferConfig.getDouble(PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT) : maxHeapAllocationPercent;
minBackBufferMemoryReservePercent = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT)
? bufferConfig.getDouble(PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT) : minBackBufferMemoryReservePercent;
minBufferMemoryReservePercent = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT)
? bufferConfig.getDouble(PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT) : minBufferMemoryReservePercent;
videoView.setBufferConfig(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, maxHeapAllocationPercent, minBackBufferMemoryReservePercent, minBufferMemoryReservePercent);
}
}

View File

@ -15,6 +15,8 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.io.StringWriter;
import java.io.PrintWriter;
class VideoEventEmitter {
@ -42,6 +44,7 @@ class VideoEventEmitter {
private static final String EVENT_RESUME = "onPlaybackResume";
private static final String EVENT_READY = "onReadyForDisplay";
private static final String EVENT_BUFFER = "onVideoBuffer";
private static final String EVENT_PLAYBACK_STATE_CHANGED = "onVideoPlaybackStateChanged";
private static final String EVENT_IDLE = "onVideoIdle";
private static final String EVENT_TIMED_METADATA = "onTimedMetadata";
private static final String EVENT_AUDIO_BECOMING_NOISY = "onVideoAudioBecomingNoisy";
@ -63,6 +66,8 @@ class VideoEventEmitter {
EVENT_RESUME,
EVENT_READY,
EVENT_BUFFER,
EVENT_PLAYBACK_STATE_CHANGED,
EVENT_BUFFER_PROGRESS,
EVENT_IDLE,
EVENT_TIMED_METADATA,
EVENT_AUDIO_BECOMING_NOISY,
@ -87,6 +92,8 @@ class VideoEventEmitter {
EVENT_RESUME,
EVENT_READY,
EVENT_BUFFER,
EVENT_PLAYBACK_STATE_CHANGED,
EVENT_BUFFER_PROGRESS,
EVENT_IDLE,
EVENT_TIMED_METADATA,
EVENT_AUDIO_BECOMING_NOISY,
@ -104,6 +111,8 @@ class VideoEventEmitter {
private static final String EVENT_PROP_STEP_FORWARD = "canStepForward";
private static final String EVENT_PROP_STEP_BACKWARD = "canStepBackward";
private static final String EVENT_PROP_BUFFER_START = "bufferStart";
private static final String EVENT_PROP_BUFFER_END = "bufferEnd";
private static final String EVENT_PROP_DURATION = "duration";
private static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
private static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration";
@ -125,11 +134,14 @@ class VideoEventEmitter {
private static final String EVENT_PROP_ERROR = "error";
private static final String EVENT_PROP_ERROR_STRING = "errorString";
private static final String EVENT_PROP_ERROR_EXCEPTION = "errorException";
private static final String EVENT_PROP_ERROR_TRACE = "errorStackTrace";
private static final String EVENT_PROP_ERROR_CODE = "errorCode";
private static final String EVENT_PROP_TIMED_METADATA = "metadata";
private static final String EVENT_PROP_BITRATE = "bitrate";
private static final String EVENT_PROP_IS_PLAYING = "isPlaying";
void setViewId(int viewId) {
this.viewId = viewId;
@ -206,6 +218,12 @@ class VideoEventEmitter {
receiveEvent(EVENT_BUFFER, map);
}
void playbackStateChanged(boolean isPlaying) {
WritableMap map = Arguments.createMap();
map.putBoolean(EVENT_PROP_IS_PLAYING, isPlaying);
receiveEvent(EVENT_PLAYBACK_STATE_CHANGED, map);
}
void idle() {
receiveEvent(EVENT_IDLE, null);
}
@ -231,9 +249,25 @@ class VideoEventEmitter {
}
void error(String errorString, Exception exception) {
_error(errorString, exception, "0001");
}
void error(String errorString, Exception exception, String errorCode) {
_error(errorString, exception, errorCode);
}
void _error(String errorString, Exception exception, String errorCode) {
// Prepare stack trace
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
exception.printStackTrace(pw);
String stackTrace = sw.toString();
WritableMap error = Arguments.createMap();
error.putString(EVENT_PROP_ERROR_STRING, errorString);
error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.toString());
error.putString(EVENT_PROP_ERROR_CODE, errorCode);
error.putString(EVENT_PROP_ERROR_TRACE, stackTrace);
WritableMap event = Arguments.createMap();
event.putMap(EVENT_PROP_ERROR, error);
receiveEvent(EVENT_ERROR, event);