2022-09-26 01:51:18 +01:00

1909 lines
76 KiB
Java

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;
import android.view.View;
import android.view.Window;
import android.view.accessibility.CaptioningManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import androidx.activity.OnBackPressedCallback;
import com.brentvatne.react.R;
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
import com.brentvatne.receiver.BecomingNoisyListener;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReadableArray;
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.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.ExoMediaDrm;
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;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.android.exoplayer2.trackselection.TrackSelectionOverride;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
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;
import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.List;
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
LifecycleEventListener,
Player.Listener,
BandwidthMeter.EventListener,
BecomingNoisyListener,
AudioManager.OnAudioFocusChangeListener,
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;
private static final int SHOW_PROGRESS = 1;
static {
DEFAULT_COOKIE_MANAGER = new CookieManager();
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
private final VideoEventEmitter eventEmitter;
private final ReactExoplayerConfig config;
private final DefaultBandwidthMeter bandwidthMeter;
private PlayerControlView playerControlView;
private View playPauseControlContainer;
private Player.Listener eventListener;
private ExoPlayerView exoPlayerView;
private FullScreenPlayerView fullScreenPlayerView;
private DataSource.Factory mediaDataSourceFactory;
private ExoPlayer player;
private DefaultTrackSelector trackSelector;
private boolean playerNeedsSource;
private int resumeWindow;
private long resumePosition;
private boolean loadVideoStarted;
private boolean isFullscreen;
private boolean isInBackground;
private boolean isPaused;
private boolean isBuffering;
private boolean muted = false;
private boolean hasAudioFocus = false;
private float rate = 1f;
private float audioVolume = 1f;
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;
private String audioTrackType;
private Dynamic audioTrackValue;
private String videoTrackType;
private Dynamic videoTrackValue;
private String textTrackType;
private Dynamic textTrackValue;
private ReadableArray textTracks;
private boolean disableFocus;
private boolean focusable = true;
private boolean disableBuffering;
private long contentStartTime = -1L;
private boolean disableDisconnectError;
private boolean preventsDisplaySleepDuringVideoPlayback = true;
private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false;
private Map<String, String> requestHeaders;
private boolean mReportBandwidth = false;
private UUID drmUUID = null;
private String drmLicenseUrl = null;
private String[] drmLicenseHeader = null;
private boolean controls;
// \ End props
// React
private final ThemedReactContext themedReactContext;
private final AudioManager audioManager;
private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver;
// store last progress event values to avoid sending unnecessary messages
private long lastPos = -1;
private long lastBufferDuration = -1;
private long lastDuration = -1;
private final Handler progressHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_PROGRESS:
if (player != null) {
long pos = player.getCurrentPosition();
long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
long duration = player.getDuration();
if (lastPos != pos
|| lastBufferDuration != bufferedDuration
|| lastDuration != duration) {
lastPos = pos;
lastBufferDuration = bufferedDuration;
lastDuration = duration;
eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos));
}
msg = obtainMessage(SHOW_PROGRESS);
sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
}
break;
}
}
};
public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) {
Timeline.Window window = new Timeline.Window();
if(!player.getCurrentTimeline().isEmpty()) {
player.getCurrentTimeline().getWindow(player.getCurrentMediaItemIndex(), window);
}
return window.windowStartTimeMs + currentPosition;
}
public ReactExoplayerView(ThemedReactContext context, ReactExoplayerConfig config) {
super(context);
this.themedReactContext = context;
this.eventEmitter = new VideoEventEmitter(context);
this.config = config;
this.bandwidthMeter = config.getBandwidthMeter();
createViews();
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
themedReactContext.addLifecycleEventListener(this);
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
}
@Override
public void setId(int id) {
super.setId(id);
eventEmitter.setViewId(id);
}
private void createViews() {
clearResumePosition();
mediaDataSourceFactory = buildDataSourceFactory(true);
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
}
LayoutParams layoutParams = new LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
exoPlayerView = new ExoPlayerView(getContext());
exoPlayerView.setLayoutParams(layoutParams);
addView(exoPlayerView, 0, layoutParams);
exoPlayerView.setFocusable(this.focusable);
mainHandler = new Handler();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
initializePlayer();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
/* We want to be able to continue playing audio when switching tabs.
* Leave this here in case it causes issues.
*/
// stopPlayback();
}
// LifecycleEventListener implementation
@Override
public void onHostResume() {
if (!playInBackground || !isInBackground) {
setPlayWhenReady(!isPaused);
}
isInBackground = false;
}
@Override
public void onHostPause() {
isInBackground = true;
if (playInBackground) {
return;
}
setPlayWhenReady(false);
}
@Override
public void onHostDestroy() {
stopPlayback();
}
public void cleanUpResources() {
stopPlayback();
}
//BandwidthMeter.EventListener implementation
@Override
public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
if (mReportBandwidth) {
if (player == null) {
eventEmitter.bandwidthReport(bitrate, 0, 0, "-1");
} else {
Format videoFormat = player.getVideoFormat();
int width = videoFormat != null ? videoFormat.width : 0;
int height = videoFormat != null ? videoFormat.height : 0;
String trackId = videoFormat != null ? videoFormat.id : "-1";
eventEmitter.bandwidthReport(bitrate, height, width, trackId);
}
}
}
// Internal methods
/**
* Toggling the visibility of the player control view
*/
private void togglePlayerControlVisibility() {
if(player == null) return;
reLayout(playerControlView);
if (playerControlView.isVisible()) {
playerControlView.hide();
} else {
playerControlView.show();
}
}
/**
* Initializing Player control
*/
private void initializePlayerControl() {
if (playerControlView == null) {
playerControlView = new PlayerControlView(getContext());
}
// Setting the player for the playerControlView
playerControlView.setPlayer(player);
playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container);
// Invoking onClick event for exoplayerView
exoPlayerView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
togglePlayerControlVisibility();
}
});
//Handling the playButton click event
ImageButton playButton = playerControlView.findViewById(R.id.exo_play);
playButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (player != null && player.getPlaybackState() == Player.STATE_ENDED) {
player.seekTo(0);
}
setPausedModifier(false);
}
});
//Handling the pauseButton click event
ImageButton pauseButton = playerControlView.findViewById(R.id.exo_pause);
pauseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setPausedModifier(true);
}
});
//Handling the fullScreenButton click event
ImageButton fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen);
fullScreenButton.setOnClickListener(v -> setFullscreen(!isFullscreen));
// Invoking onPlaybackStateChanged and onPlayWhenReadyChanged events for Player
eventListener = new Player.Listener() {
@Override
public void onPlaybackStateChanged(int playbackState) {
View playButton = playerControlView.findViewById(R.id.exo_play);
View pauseButton = playerControlView.findViewById(R.id.exo_pause);
if (playButton != null && playButton.getVisibility() == GONE) {
playButton.setVisibility(INVISIBLE);
}
if (pauseButton != null && pauseButton.getVisibility() == GONE) {
pauseButton.setVisibility(INVISIBLE);
}
reLayout(playPauseControlContainer);
//Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
player.removeListener(eventListener);
}
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
reLayout(playPauseControlContainer);
//Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
player.removeListener(eventListener);
}
};
player.addListener(eventListener);
}
/**
* Adding Player control to the frame layout
*/
private void addPlayerControl() {
if(player == null) return;
LayoutParams layoutParams = new LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
playerControlView.setLayoutParams(layoutParams);
int indexOfPC = indexOfChild(playerControlView);
if (indexOfPC != -1) {
removeViewAt(indexOfPC);
}
addView(playerControlView, 1, layoutParams);
reLayout(playerControlView);
}
/**
* Update the layout
* @param view view needs to update layout
*
* This is a workaround for the open bug in react-native: https://github.com/facebook/react-native/issues/17968
*/
private void reLayout(View view) {
if (view == null) return;
view.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
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() {
Player 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() {
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;
}
// 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);
}
} 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");
}
}
}, 1);
}
private void initializePlayerCore(ReactExoplayerView self) {
ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
self.trackSelector = new DefaultTrackSelector(getContext(), 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 ExoPlayer.Builder(getContext(), renderersFactory)
.setTrackSelector(self.trackSelector)
.setBandwidthMeter(bandwidthMeter)
.setLoadControl(loadControl)
.build();
player.addListener(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();
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, playerControlView, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
setFullscreen(false);
}
});
}
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;
}
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;
}
}
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);
MediaItem mediaItem = new MediaItem.Builder().setUri(uri).build();
DrmSessionManagerProvider drmProvider = null;
if (drmSessionManager != null) {
drmProvider = new DrmSessionManagerProvider() {
@Override
public DrmSessionManager get(MediaItem mediaItem) {
return drmSessionManager;
}
};
} else {
drmProvider = new DefaultDrmSessionManagerProvider();
}
switch (type) {
case C.TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false)
).setDrmSessionManagerProvider(drmProvider)
.setLoadErrorHandlingPolicy(
config.buildLoadErrorHandlingPolicy(minLoadRetryCount)
).createMediaSource(mediaItem);
case C.TYPE_DASH:
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false)
).setDrmSessionManagerProvider(drmProvider)
.setLoadErrorHandlingPolicy(
config.buildLoadErrorHandlingPolicy(minLoadRetryCount)
).createMediaSource(mediaItem);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(
mediaDataSourceFactory
).setDrmSessionManagerProvider(drmProvider)
.setLoadErrorHandlingPolicy(
config.buildLoadErrorHandlingPolicy(minLoadRetryCount)
).createMediaSource(mediaItem);
case C.TYPE_OTHER:
return new ProgressiveMediaSource.Factory(
mediaDataSourceFactory
).setDrmSessionManagerProvider(drmProvider)
.setLoadErrorHandlingPolicy(
config.buildLoadErrorHandlingPolicy(minLoadRetryCount)
).createMediaSource(mediaItem);
default: {
throw new IllegalStateException("Unsupported type: " + type);
}
}
}
private ArrayList<MediaSource> buildTextSources() {
ArrayList<MediaSource> textSources = new ArrayList<>();
if (textTracks == null) {
return textSources;
}
for (int i = 0; i < textTracks.size(); ++i) {
ReadableMap textTrack = textTracks.getMap(i);
String language = textTrack.getString("language");
String title = textTrack.hasKey("title")
? textTrack.getString("title") : language + " " + i;
Uri uri = Uri.parse(textTrack.getString("uri"));
MediaSource textSource = buildTextSource(title, uri, textTrack.getString("type"),
language);
if (textSource != null) {
textSources.add(textSource);
}
}
return textSources;
}
private MediaSource buildTextSource(String title, Uri uri, String mimeType, String language) {
MediaItem.SubtitleConfiguration subtitleConfiguration = new MediaItem.SubtitleConfiguration.Builder(uri)
.setMimeType(mimeType)
.setLanguage(language)
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setRoleFlags(C.ROLE_FLAG_SUBTITLE)
.setLabel(title)
.build();
return new SingleSampleMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(subtitleConfiguration, C.TIME_UNSET);
}
private void releasePlayer() {
if (player != null) {
updateResumePosition();
player.release();
player.removeListener(this);
trackSelector = null;
player = null;
}
progressHandler.removeMessages(SHOW_PROGRESS);
themedReactContext.removeLifecycleEventListener(this);
audioBecomingNoisyReceiver.removeListener();
bandwidthMeter.removeEventListener(this);
}
private boolean requestAudioFocus() {
if (disableFocus || srcUri == null || this.hasAudioFocus) {
return true;
}
int result = audioManager.requestAudioFocus(this,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
private void setPlayWhenReady(boolean playWhenReady) {
if (player == null) {
return;
}
if (playWhenReady) {
this.hasAudioFocus = requestAudioFocus();
if (this.hasAudioFocus) {
player.setPlayWhenReady(true);
}
} else {
// ensure playback is not ENDED, else it will trigger another ended event
if (player.getPlaybackState() != Player.STATE_ENDED) {
player.setPlayWhenReady(false);
}
}
}
private void startPlayback() {
if (player != null) {
switch (player.getPlaybackState()) {
case Player.STATE_IDLE:
case Player.STATE_ENDED:
initializePlayer();
break;
case Player.STATE_BUFFERING:
case Player.STATE_READY:
if (!player.getPlayWhenReady()) {
setPlayWhenReady(true);
}
break;
default:
break;
}
} else {
initializePlayer();
}
if (!disableFocus) {
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
}
}
private void pausePlayback() {
if (player != null) {
if (player.getPlayWhenReady()) {
setPlayWhenReady(false);
}
}
setKeepScreenOn(false);
}
private void stopPlayback() {
onStopPlayback();
releasePlayer();
}
private void onStopPlayback() {
if (isFullscreen) {
setFullscreen(false);
}
audioManager.abandonAudioFocus(this);
}
private void updateResumePosition() {
resumeWindow = player.getCurrentMediaItemIndex();
resumePosition = player.isCurrentMediaItemSeekable() ? Math.max(0, player.getCurrentPosition())
: C.TIME_UNSET;
}
private void clearResumePosition() {
resumeWindow = C.INDEX_UNSET;
resumePosition = C.TIME_UNSET;
}
/**
* Returns a new DataSource factory.
*
* @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new
* DataSource factory.
* @return A new DataSource factory.
*/
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
return DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext,
useBandwidthMeter ? bandwidthMeter : null, requestHeaders);
}
/**
* Returns a new HttpDataSource factory.
*
* @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new
* DataSource factory.
* @return A new HttpDataSource factory.
*/
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, useBandwidthMeter ? bandwidthMeter : null, requestHeaders);
}
// AudioManager.OnAudioFocusChangeListener implementation
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
this.hasAudioFocus = false;
eventEmitter.audioFocusChanged(false);
pausePlayback();
audioManager.abandonAudioFocus(this);
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
eventEmitter.audioFocusChanged(false);
break;
case AudioManager.AUDIOFOCUS_GAIN:
this.hasAudioFocus = true;
eventEmitter.audioFocusChanged(true);
break;
default:
break;
}
if (player != null) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// Lower the volume
if (!muted) {
player.setVolume(audioVolume * 0.8f);
}
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Raise it back to normal
if (!muted) {
player.setVolume(audioVolume * 1);
}
}
}
}
// AudioBecomingNoisyListener implementation
@Override
public void onAudioBecomingNoisy() {
eventEmitter.audioBecomingNoisy();
}
// Player.Listener implementation
@Override
public void onIsLoadingChanged(boolean isLoading) {
// Do nothing.
}
@Override
public void onEvents(Player player, Player.Events events) {
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
int playbackState = player.getPlaybackState();
boolean playWhenReady = player.getPlayWhenReady();
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
eventEmitter.playbackRateChange(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
switch (playbackState) {
case Player.STATE_IDLE:
text += "idle";
eventEmitter.idle();
clearProgressMessageHandler();
if (!player.getPlayWhenReady()) {
setKeepScreenOn(false);
}
break;
case Player.STATE_BUFFERING:
text += "buffering";
onBuffering(true);
clearProgressMessageHandler();
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
break;
case Player.STATE_READY:
text += "ready";
eventEmitter.ready();
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();
}
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
break;
case Player.STATE_ENDED:
text += "ended";
eventEmitter.end();
onStopPlayback();
setKeepScreenOn(false);
break;
default:
text += "unknown";
break;
}
}
}
private void startProgressHandler() {
progressHandler.sendEmptyMessage(SHOW_PROGRESS);
}
/*
The progress message handler will duplicate recursions of the onProgressMessage handler
on change of player state from any state to STATE_READY with playWhenReady is true (when
the video is not paused). This clears all existing messages.
*/
private void clearProgressMessageHandler() {
progressHandler.removeMessages(SHOW_PROGRESS);
}
private void videoLoaded() {
if (loadVideoStarted) {
loadVideoStarted = false;
if (audioTrackType != null) {
setSelectedAudioTrack(audioTrackType, audioTrackValue);
}
if (videoTrackType != null) {
setSelectedVideoTrack(videoTrackType, videoTrackValue);
}
if (textTrackType != null) {
setSelectedTextTrack(textTrackType, textTrackValue);
}
Format videoFormat = player.getVideoFormat();
int width = videoFormat != null ? videoFormat.width : 0;
int height = videoFormat != null ? videoFormat.height : 0;
String trackId = videoFormat != null ? videoFormat.id : "-1";
// 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) {
return audioTracks;
}
TrackGroupArray groups = info.getTrackGroups(index);
for (int i = 0; i < groups.length; ++i) {
Format format = groups.get(i).getFormat(0);
WritableMap audioTrack = Arguments.createMap();
audioTrack.putInt("index", i);
audioTrack.putString("title", format.id != null ? format.id : "");
audioTrack.putString("type", format.sampleMimeType);
audioTrack.putString("language", format.language != null ? format.language : "");
audioTrack.putString("bitrate", format.bitrate == Format.NO_VALUE ? ""
: String.format(Locale.US, "%.2fMbps", format.bitrate / 1000000f));
audioTracks.pushMap(audioTrack);
}
return audioTracks;
}
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();
if (info == null || trackRendererIndex == C.INDEX_UNSET) {
return videoTracks;
}
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);
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();
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
if (info == null || index == C.INDEX_UNSET) {
return textTracks;
}
TrackGroupArray groups = info.getTrackGroups(index);
for (int i = 0; i < groups.length; ++i) {
Format format = groups.get(i).getFormat(0);
WritableMap textTrack = Arguments.createMap();
textTrack.putInt("index", i);
textTrack.putString("title", format.id != null ? format.id : "");
textTrack.putString("type", format.sampleMimeType);
textTrack.putString("language", format.language != null ? format.language : "");
textTracks.pushMap(textTrack);
}
return textTracks;
}
private void onBuffering(boolean buffering) {
if (isBuffering == buffering) {
return;
}
isBuffering = buffering;
if (buffering) {
eventEmitter.buffering(true);
} else {
eventEmitter.buffering(false);
}
}
@Override
public void onPositionDiscontinuity(Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {
if (playerNeedsSource) {
// This will only occur if the user has performed a seek whilst in the error state. Update the
// resume position so that if the user then retries, playback will resume from the position to
// 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_AUTO_TRANSITION
&& player.getRepeatMode() == Player.REPEAT_MODE_ONE) {
eventEmitter.end();
}
}
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
// Do nothing.
}
@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
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
// Do nothing.
}
@Override
public void onRepeatModeChanged(int repeatMode) {
// Do nothing.
}
@Override
public void onTracksChanged(Tracks tracks) {
// Do nothing.
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters params) {
eventEmitter.playbackRateChange(params.speed);
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
eventEmitter.playbackStateChanged(isPlaying);
}
@Override
public void onPlayerError(PlaybackException e) {
if (e == null) {
return;
}
String errorString = "ExoPlaybackException: " + PlaybackException.getErrorCodeName(e.errorCode);
String errorCode = "2" + String.valueOf(e.errorCode);
boolean needsReInitialization = false;
switch(e.errorCode) {
case PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED:
case PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED:
case PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED:
case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR:
case PlaybackException.ERROR_CODE_DRM_UNSPECIFIED:
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;
}
break;
default:
break;
}
eventEmitter.error(errorString, e, errorCode);
playerNeedsSource = true;
if (isBehindLiveWindow(e)) {
clearResumePosition();
initializePlayer();
} else {
updateResumePosition();
if (needsReInitialization) {
initializePlayer();
}
}
}
private static boolean isBehindLiveWindow(PlaybackException e) {
return e.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
}
public int getTrackRendererIndex(int trackType) {
if (player != null) {
int rendererCount = player.getRendererCount();
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
if (player.getRendererType(rendererIndex) == trackType) {
return rendererIndex;
}
}
}
return C.INDEX_UNSET;
}
@Override
public void onMetadata(Metadata metadata) {
eventEmitter.timedMetadata(metadata);
}
// ReactExoplayerViewManager public api
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;
this.mediaDataSourceFactory =
DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter,
this.requestHeaders);
if (!isSourceEqual) {
reloadSource();
}
}
}
public void clearSrc() {
if (srcUri != null) {
player.stop();
player.clearMediaItems();
this.srcUri = null;
this.extension = null;
this.requestHeaders = null;
this.mediaDataSourceFactory = null;
clearResumePosition();
}
}
public void setProgressUpdateInterval(final float progressUpdateInterval) {
mProgressUpdateInterval = progressUpdateInterval;
}
public void setReportBandwidth(boolean reportBandwidth) {
mReportBandwidth = reportBandwidth;
}
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);
if (!isSourceEqual) {
reloadSource();
}
}
}
public void setTextTracks(ReadableArray textTracks) {
this.textTracks = textTracks;
reloadSource();
}
private void reloadSource() {
playerNeedsSource = true;
initializePlayer();
}
public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) {
exoPlayerView.setResizeMode(resizeMode);
}
private void applyModifiers() {
setRepeatModifier(repeat);
setMutedModifier(muted);
}
public void setRepeatModifier(boolean repeat) {
if (player != null) {
if (repeat) {
player.setRepeatMode(Player.REPEAT_MODE_ONE);
} else {
player.setRepeatMode(Player.REPEAT_MODE_OFF);
}
}
this.repeat = repeat;
}
public void setPreventsDisplaySleepDuringVideoPlayback(boolean preventsDisplaySleepDuringVideoPlayback) {
this.preventsDisplaySleepDuringVideoPlayback = preventsDisplaySleepDuringVideoPlayback;
}
public void setSelectedTrack(int trackType, String type, Dynamic value) {
if (player == null) return;
int rendererIndex = getTrackRendererIndex(trackType);
if (rendererIndex == C.INDEX_UNSET) {
return;
}
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
if (info == null) {
return;
}
TrackGroupArray groups = info.getTrackGroups(rendererIndex);
int groupIndex = C.INDEX_UNSET;
List<Integer> tracks = new ArrayList<>();
tracks.add(0);
if (TextUtils.isEmpty(type)) {
type = "default";
}
DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters()
.buildUpon()
.setRendererDisabled(rendererIndex, true)
.build();
if (type.equals("disabled")) {
trackSelector.setParameters(disableParameters);
return;
} else if (type.equals("language")) {
for (int i = 0; i < groups.length; ++i) {
Format format = groups.get(i).getFormat(0);
if (format.language != null && format.language.equals(value.asString())) {
groupIndex = i;
break;
}
}
} else if (type.equals("title")) {
for (int i = 0; i < groups.length; ++i) {
Format format = groups.get(i).getFormat(0);
if (format.id != null && format.id.equals(value.asString())) {
groupIndex = i;
break;
}
}
} else if (type.equals("index")) {
if (value.asInt() < groups.length) {
groupIndex = value.asInt();
}
} else if (type.equals("resolution")) {
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.set(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.set(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.set(0, closestTrackIndex);
}
}
} else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
// Use system settings if possible
CaptioningManager captioningManager
= (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager != null && captioningManager.isEnabled()) {
groupIndex = getGroupIndexForDefaultLocale(groups);
}
} else if (rendererIndex == C.TRACK_TYPE_AUDIO) { // Audio default
groupIndex = getGroupIndexForDefaultLocale(groups);
}
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 ArrayList<Integer>(group.length);
ArrayList<Integer> allTracks = new ArrayList<Integer>(group.length);
groupIndex = 0;
for (int j = 0; j < group.length; j++) {
allTracks.add(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.size(); g++) {
Format format = group.getFormat(g);
if (isFormatSupported(format)) {
supportedFormatLength++;
}
}
if (allTracks.size() == 1) {
// With only one tracks we can't remove any tracks so attempt to play it anyway
tracks = allTracks;
} else {
tracks = new ArrayList<>(supportedFormatLength + 1);
for (int k = 0; k < allTracks.size(); k++) {
Format format = group.getFormat(k);
if (isFormatSupported(format)) {
tracks.add(allTracks.get(k));
supportedTrackList.add(allTracks.get(k));
}
}
}
}
if (groupIndex == C.INDEX_UNSET) {
trackSelector.setParameters(disableParameters);
return;
}
TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks);
DefaultTrackSelector.Parameters selectionParameters = trackSelector.getParameters()
.buildUpon()
.setRendererDisabled(rendererIndex, false)
.addOverride(selectionOverride)
.build();
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;
}
int groupIndex = 0; // default if no match
String locale2 = Locale.getDefault().getLanguage(); // 2 letter code
String locale3 = Locale.getDefault().getISO3Language(); // 3 letter code
for (int i = 0; i < groups.length; ++i) {
Format format = groups.get(i).getFormat(0);
String language = format.language;
if (language != null && (language.equals(locale2) || language.equals(locale3))) {
groupIndex = i;
break;
}
}
return groupIndex;
}
public void setSelectedVideoTrack(String type, Dynamic value) {
videoTrackType = type;
videoTrackValue = value;
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
}
public void setSelectedAudioTrack(String type, Dynamic value) {
audioTrackType = type;
audioTrackValue = value;
setSelectedTrack(C.TRACK_TYPE_AUDIO, audioTrackType, audioTrackValue);
}
public void setSelectedTextTrack(String type, Dynamic value) {
textTrackType = type;
textTrackValue = value;
setSelectedTrack(C.TRACK_TYPE_TEXT, textTrackType, textTrackValue);
}
public void setPausedModifier(boolean paused) {
isPaused = paused;
if (player != null) {
if (!paused) {
startPlayback();
} else {
pausePlayback();
}
}
}
public void setMutedModifier(boolean muted) {
this.muted = muted;
if (player != null) {
player.setVolume(muted ? 0.f : audioVolume);
}
}
public void setVolumeModifier(float volume) {
audioVolume = volume;
if (player != null) {
player.setVolume(audioVolume);
}
}
public void seekTo(long positionMs) {
if (player != null) {
player.seekTo(positionMs);
eventEmitter.seek(player.getCurrentPosition(), positionMs);
}
}
public void setRateModifier(float newRate) {
rate = newRate;
if (player != null) {
PlaybackParameters params = new PlaybackParameters(rate, 1f);
player.setPlaybackParameters(params);
}
}
public void setMaxBitRateModifier(int newMaxBitRate) {
maxBitRate = newMaxBitRate;
if (player != null) {
trackSelector.setParameters(trackSelector.buildUponParameters()
.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate));
}
}
public void setMinLoadRetryCountModifier(int newMinLoadRetryCount) {
minLoadRetryCount = newMinLoadRetryCount;
releasePlayer();
initializePlayer();
}
public void setPlayInBackground(boolean playInBackground) {
this.playInBackground = playInBackground;
}
public void setDisableFocus(boolean disableFocus) {
this.disableFocus = disableFocus;
}
public void setFocusable(boolean focusable) {
this.focusable = focusable;
exoPlayerView.setFocusable(this.focusable);
}
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
}
isFullscreen = fullscreen;
Activity activity = themedReactContext.getCurrentActivity();
if (activity == null) {
return;
}
if (fullScreenPlayerView == null) {
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, playerControlView, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
setFullscreen(false);
}
});
}
Window window = activity.getWindow();
View decorView = window.getDecorView();
int uiOptions;
if (isFullscreen) {
if (Util.SDK_INT >= 19) { // 4.4+
uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION
| SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| SYSTEM_UI_FLAG_FULLSCREEN;
} else {
uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION
| SYSTEM_UI_FLAG_FULLSCREEN;
}
eventEmitter.fullscreenWillPresent();
if (controls) {
fullScreenPlayerView.show();
}
post(() -> {
decorView.setSystemUiVisibility(uiOptions);
eventEmitter.fullscreenDidPresent();
});
} else {
uiOptions = View.SYSTEM_UI_FLAG_VISIBLE;
eventEmitter.fullscreenWillDismiss();
if (controls) {
fullScreenPlayerView.dismiss();
reLayout(exoPlayerView);
}
post(() -> {
decorView.setSystemUiVisibility(uiOptions);
eventEmitter.fullscreenDidDismiss();
});
}
}
public void setUseTextureView(boolean useTextureView) {
boolean finallyUseTextureView = useTextureView && this.drmUUID == null;
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, double newMaxHeapAllocationPercent, double newMinBackBufferMemoryReservePercent, double newMinBufferMemoryReservePercent) {
minBufferMs = newMinBufferMs;
maxBufferMs = newMaxBufferMs;
bufferForPlaybackMs = newBufferForPlaybackMs;
bufferForPlaybackAfterRebufferMs = newBufferForPlaybackAfterRebufferMs;
maxHeapAllocationPercent = newMaxHeapAllocationPercent;
minBackBufferMemoryReservePercent = newMinBackBufferMemoryReservePercent;
minBufferMemoryReservePercent = newMinBufferMemoryReservePercent;
releasePlayer();
initializePlayer();
}
public void setDrmType(UUID drmType) {
this.drmUUID = drmType;
}
public void setDrmLicenseUrl(String licenseUrl){
this.drmLicenseUrl = licenseUrl;
}
public void setDrmLicenseHeader(String[] header){
this.drmLicenseHeader = header;
}
@Override
public void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
Log.d("DRM Info", "onDrmKeysLoaded");
}
@Override
public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, Exception e) {
Log.d("DRM Info", "onDrmSessionManagerError");
eventEmitter.error("onDrmSessionManagerError", e, "3002");
}
@Override
public void onDrmKeysRestored(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
Log.d("DRM Info", "onDrmKeysRestored");
}
@Override
public void onDrmKeysRemoved(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
Log.d("DRM Info", "onDrmKeysRemoved");
}
/**
* Handling controls prop
*
* @param controls Controls prop, if true enable controls, if false disable them
*/
public void setControls(boolean controls) {
this.controls = controls;
if (player == null || exoPlayerView == null) return;
if (controls) {
addPlayerControl();
} else {
int indexOfPC = indexOfChild(playerControlView);
if (indexOfPC != -1) {
removeViewAt(indexOfPC);
}
}
}
public void setSubtitleStyle(SubtitleStyle style) {
exoPlayerView.setSubtitleStyle(style);
}
}