package com.brentvatne.exoplayer;
import static androidx.media3.common.C.CONTENT_TYPE_DASH;
import static androidx.media3.common.C.CONTENT_TYPE_HLS;
import static androidx.media3.common.C.CONTENT_TYPE_OTHER;
import static androidx.media3.common.C.CONTENT_TYPE_RTSP;
import static androidx.media3.common.C.CONTENT_TYPE_SS;
import static androidx.media3.common.C.TIME_END_OF_SOURCE;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.view.View;
import android.view.accessibility.CaptioningManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Metadata;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.StreamKey;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.Tracks;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.dash.DashMediaSource;
import androidx.media3.exoplayer.dash.DashUtil;
import androidx.media3.exoplayer.dash.DefaultDashChunkSource;
import androidx.media3.exoplayer.dash.manifest.AdaptationSet;
import androidx.media3.exoplayer.dash.manifest.DashManifest;
import androidx.media3.exoplayer.dash.manifest.Period;
import androidx.media3.exoplayer.dash.manifest.Representation;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.drm.FrameworkMediaDrm;
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback;
import androidx.media3.exoplayer.drm.UnsupportedDrmException;
import androidx.media3.exoplayer.hls.HlsMediaSource;
import androidx.media3.exoplayer.ima.ImaAdsLoader;
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
import androidx.media3.exoplayer.rtsp.RtspMediaSource;
import androidx.media3.exoplayer.smoothstreaming.DefaultSsChunkSource;
import androidx.media3.exoplayer.smoothstreaming.SsMediaSource;
import androidx.media3.exoplayer.source.ClippingMediaSource;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MergingMediaSource;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.MappingTrackSelector;
import androidx.media3.exoplayer.trackselection.TrackSelection;
import androidx.media3.exoplayer.trackselection.TrackSelectionArray;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.DefaultAllocator;
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
import androidx.media3.exoplayer.util.EventLogger;
import androidx.media3.extractor.metadata.emsg.EventMessage;
import androidx.media3.extractor.metadata.id3.Id3Frame;
import androidx.media3.extractor.metadata.id3.TextInformationFrame;
import androidx.media3.session.MediaSessionService;
import androidx.media3.ui.LegacyPlayerControlView;
import com.brentvatne.common.api.AdsProps;
import com.brentvatne.common.api.BufferConfig;
import com.brentvatne.common.api.BufferingStrategy;
import com.brentvatne.common.api.ControlsConfig;
import com.brentvatne.common.api.DRMProps;
import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.api.SideLoadedTextTrack;
import com.brentvatne.common.api.Source;
import com.brentvatne.common.api.SubtitleStyle;
import com.brentvatne.common.api.TimedMetadata;
import com.brentvatne.common.api.Track;
import com.brentvatne.common.api.VideoTrack;
import com.brentvatne.common.react.VideoEventEmitter;
import com.brentvatne.common.toolbox.DebugLog;
import com.brentvatne.common.toolbox.ReactBridgeUtils;
import com.brentvatne.react.BuildConfig;
import com.brentvatne.react.R;
import com.brentvatne.react.ReactNativeVideoManager;
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
import com.brentvatne.receiver.BecomingNoisyListener;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.uimanager.ThemedReactContext;
import com.google.ads.interactivemedia.v3.api.AdError;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
import com.google.ads.interactivemedia.v3.api.AdEvent;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.common.collect.ImmutableList;
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.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@SuppressLint("ViewConstructor")
public class ReactExoplayerView extends FrameLayout implements
LifecycleEventListener,
Player.Listener,
BandwidthMeter.EventListener,
BecomingNoisyListener,
DrmSessionEventListener,
AdEvent.AdEventListener,
AdErrorEvent.AdErrorListener {
public static final double DEFAULT_MAX_HEAP_ALLOCATION_PERCENT = 1;
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);
}
protected final VideoEventEmitter eventEmitter;
private final ReactExoplayerConfig config;
private final DefaultBandwidthMeter bandwidthMeter;
private LegacyPlayerControlView playerControlView;
private View playPauseControlContainer;
private Player.Listener eventListener;
private ExoPlayerView exoPlayerView;
private FullScreenPlayerView fullScreenPlayerView;
private ImaAdsLoader adsLoader;
private DataSource.Factory mediaDataSourceFactory;
private ExoPlayer player;
private DefaultTrackSelector trackSelector;
private boolean playerNeedsSource;
private ServiceConnection playbackServiceConnection;
private PlaybackServiceBinder playbackServiceBinder;
// logger to be enable by props
private EventLogger debugEventLogger = null;
private boolean enableDebug = false;
private static final String TAG_EVENT_LOGGER = "RNVExoplayer";
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 AudioOutput audioOutput = AudioOutput.SPEAKER;
private float audioVolume = 1f;
private BufferConfig bufferConfig = new BufferConfig();
private int maxBitRate = 0;
private boolean hasDrmFailed = false;
private boolean isUsingContentResolution = false;
private boolean selectTrackWhenReady = false;
private final Handler mainHandler;
private Runnable mainRunnable;
private boolean useCache = false;
private ControlsConfig controlsConfig = new ControlsConfig();
/*
* When user is seeking first called is on onPositionDiscontinuity -> DISCONTINUITY_REASON_SEEK
* Then we set if to false when playback is back in onIsPlayingChanged -> true
*/
private boolean isSeeking = false;
private long seekPosition = -1;
// Props from React
private Source source = new Source();
private boolean repeat;
private String audioTrackType;
private String audioTrackValue;
private String videoTrackType;
private String videoTrackValue;
private String textTrackType = "disabled";
private String textTrackValue;
private boolean disableFocus;
private boolean focusable = true;
private BufferingStrategy.BufferingStrategyEnum bufferingStrategy;
private boolean disableDisconnectError;
private boolean preventsDisplaySleepDuringVideoPlayback = true;
private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false;
private boolean mReportBandwidth = false;
private boolean controls;
private boolean showNotificationControls = false;
// \ End props
// React
private final ThemedReactContext themedReactContext;
private final AudioManager audioManager;
private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
// store last progress event values to avoid sending unnecessary messages
private long lastPos = -1;
private long lastBufferDuration = -1;
private long lastDuration = -1;
private boolean viewHasDropped = false;
private int selectedSpeedIndex = 1; // Default is 1.0x
private final String instanceId = String.valueOf(UUID.randomUUID());
private CmcdConfiguration.Factory cmcdConfigurationFactory;
public void setCmcdConfigurationFactory(CmcdConfiguration.Factory factory) {
this.cmcdConfigurationFactory = factory;
}
private void updateProgress() {
if (player != null) {
if (playerControlView != null && isPlayingAd() && controls) {
playerControlView.hide();
}
long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
long duration = player.getDuration();
long pos = player.getCurrentPosition();
if (pos > duration) {
pos = duration;
}
if (lastPos != pos
|| lastBufferDuration != bufferedDuration
|| lastDuration != duration) {
lastPos = pos;
lastBufferDuration = bufferedDuration;
lastDuration = duration;
eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos));
}
}
}
private final Handler progressHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == SHOW_PROGRESS) {
updateProgress();
msg = obtainMessage(SHOW_PROGRESS);
sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
}
}
};
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();
this.config = config;
this.bandwidthMeter = config.getBandwidthMeter();
mainHandler = new Handler();
createViews();
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
themedReactContext.addLifecycleEventListener(this);
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
audioFocusChangeListener = new OnAudioFocusChangedListener(this, themedReactContext);
}
private boolean isPlayingAd() {
return player != null && player.isPlayingAd();
}
private void createViews() {
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);
}
// 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() {
cleanUpResources();
}
@Override
protected void onDetachedFromWindow() {
cleanupPlaybackService();
super.onDetachedFromWindow();
}
public void cleanUpResources() {
stopPlayback();
themedReactContext.removeLifecycleEventListener(this);
releasePlayer();
viewHasDropped = true;
}
//BandwidthMeter.EventListener implementation
@Override
public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
if (mReportBandwidth) {
if (player == null) {
eventEmitter.onVideoBandwidthUpdate.invoke(bitrate, 0, 0, null);
} else {
Format videoFormat = player.getVideoFormat();
boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
String trackId = videoFormat != null ? videoFormat.id : null;
eventEmitter.onVideoBandwidthUpdate.invoke(bitrate, height, width, trackId);
}
}
}
// Internal methods
/**
* Toggling the visibility of the player control view
*/
private void togglePlayerControlVisibility() {
if (player == null) return;
reLayoutControls();
if (playerControlView.isVisible()) {
playerControlView.hide();
} else {
playerControlView.show();
}
}
/**
* Initializing Player control
*/
private void initializePlayerControl() {
if (playerControlView == null) {
playerControlView = new LegacyPlayerControlView(getContext());
playerControlView.addVisibilityListener(new LegacyPlayerControlView.VisibilityListener() {
@Override
public void onVisibilityChange(int visibility) {
eventEmitter.onControlsVisibilityChange.invoke(visibility == View.VISIBLE);
}
});
}
// 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((View v) -> {
if (!isPlayingAd()) {
togglePlayerControlVisibility();
}
});
//Handling the playButton click event
ImageButton playButton = playerControlView.findViewById(R.id.exo_play);
playButton.setOnClickListener((View v) -> {
if (player != null && player.getPlaybackState() == Player.STATE_ENDED) {
player.seekTo(0);
}
setPausedModifier(false);
});
//Handling the rewind and forward button click events
ImageButton exoRewind = playerControlView.findViewById(R.id.exo_rew);
ImageButton exoForward = playerControlView.findViewById(R.id.exo_ffwd);
exoRewind.setOnClickListener((View v) -> {
seekTo(player.getCurrentPosition() - controlsConfig.getSeekIncrementMS());
});
exoForward.setOnClickListener((View v) -> {
seekTo(player.getCurrentPosition() + controlsConfig.getSeekIncrementMS());
});
//Handling the pauseButton click event
ImageButton pauseButton = playerControlView.findViewById(R.id.exo_pause);
pauseButton.setOnClickListener((View v) ->
setPausedModifier(true)
);
//Handling the settingButton click event
final ImageButton settingButton = playerControlView.findViewById(R.id.exo_settings);
settingButton.setOnClickListener(v -> openSettings());
//Handling the fullScreenButton click event
final ImageButton fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen);
fullScreenButton.setOnClickListener(v -> setFullscreen(!isFullscreen));
updateFullScreenButtonVisibility();
refreshControlsStyles();
// 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);
}
private void openSettings() {
AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext);
builder.setTitle(R.string.settings);
String[] settingsOptions = {themedReactContext.getString(R.string.playback_speed)};
builder.setItems(settingsOptions, (dialog, which) -> {
if (which == 0) {
showPlaybackSpeedOptions();
}
});
builder.show();
}
private void showPlaybackSpeedOptions() {
String[] speedOptions = {"0.5x", "1.0x", "1.5x", "2.0x"};
AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext);
builder.setTitle(R.string.select_playback_speed);
builder.setSingleChoiceItems(speedOptions, selectedSpeedIndex, (dialog, which) -> {
selectedSpeedIndex = which;
float speed = switch (which) {
case 0 -> 0.5f;
case 2 -> 1.5f;
case 3 -> 2.0f;
default -> 1.0f;
};
setRateModifier(speed);
});
builder.show();
}
/**
* Adding Player control to the frame layout
*/
private void addPlayerControl() {
if (playerControlView == 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: ...
*/
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 void refreshControlsStyles() {
if (playerControlView == null || player == null || !controls) return;
updateLiveContent();
updatePlayPauseButtons();
updateButtonVisibility(controlsConfig.getHideForward(), R.id.exo_ffwd);
updateButtonVisibility(controlsConfig.getHideRewind(), R.id.exo_rew);
updateButtonVisibility(controlsConfig.getHideNext(), R.id.exo_next);
updateButtonVisibility(controlsConfig.getHidePrevious(), R.id.exo_prev);
updateViewVisibility(playerControlView.findViewById(R.id.exo_fullscreen), controlsConfig.getHideFullscreen(), GONE);
updateViewVisibility(playerControlView.findViewById(R.id.exo_position), controlsConfig.getHidePosition(), GONE);
updateViewVisibility(playerControlView.findViewById(R.id.exo_progress), controlsConfig.getHideSeekBar(), INVISIBLE);
updateViewVisibility(playerControlView.findViewById(R.id.exo_duration), controlsConfig.getHideDuration(), GONE);
updateViewVisibility(playerControlView.findViewById(R.id.exo_settings), controlsConfig.getHideSettingButton(), GONE );
}
private void updateLiveContent() {
LinearLayout exoLiveContainer = playerControlView.findViewById(R.id.exo_live_container);
TextView exoLiveLabel = playerControlView.findViewById(R.id.exo_live_label);
boolean isLive = false;
Timeline timeline = player.getCurrentTimeline();
// Determine if the content is live
if (!timeline.isEmpty()) {
Timeline.Window window = new Timeline.Window();
timeline.getWindow(player.getCurrentMediaItemIndex(), window);
isLive = window.isLive();
}
if (isLive && controlsConfig.getLiveLabel() != null) {
exoLiveLabel.setText(controlsConfig.getLiveLabel());
exoLiveContainer.setVisibility(VISIBLE);
} else {
exoLiveContainer.setVisibility(GONE);
}
}
private void updatePlayPauseButtons() {
final ImageButton playButton = playerControlView.findViewById(R.id.exo_play);
final ImageButton pauseButton = playerControlView.findViewById(R.id.exo_pause);
if (controlsConfig.getHidePlayPause()) {
playPauseControlContainer.setAlpha(0);
playButton.setClickable(false);
pauseButton.setClickable(false);
} else {
playPauseControlContainer.setAlpha(1.0f);
playButton.setClickable(true);
pauseButton.setClickable(true);
}
}
private void updateButtonVisibility(boolean hide, int buttonID) {
ImageButton button = playerControlView.findViewById(buttonID);
if (hide) {
button.setImageAlpha(0);
button.setClickable(false);
} else {
button.setImageAlpha(255);
button.setClickable(true);
}
}
private void updateViewVisibility(View view, boolean hide, int hideVisibility) {
if (hide) {
view.setVisibility(hideVisibility);
} else if (view.getVisibility() == hideVisibility) {
view.setVisibility(VISIBLE);
}
}
private void reLayoutControls() {
reLayout(exoPlayerView);
reLayout(playerControlView);
}
/// returns true is adaptive bitrate shall be used
public boolean isUsingVideoABR() {
return videoTrackType == null || "auto".equals(videoTrackType);
}
public void setDebug(boolean enableDebug) {
this.enableDebug = enableDebug;
refreshDebugState();
}
private void refreshDebugState() {
if (player == null) {
return;
}
if (enableDebug) {
debugEventLogger = new EventLogger(TAG_EVENT_LOGGER);
player.addAnalyticsListener(debugEventLogger);
} else if (debugEventLogger != null) {
player.removeAnalyticsListener(debugEventLogger);
debugEventLogger = null;
}
}
public void setViewType(int viewType) {
exoPlayerView.updateSurfaceView(viewType);
}
private class RNVLoadControl extends DefaultLoadControl {
private final int availableHeapInBytes;
private final Runtime runtime;
public RNVLoadControl(DefaultAllocator allocator, BufferConfig config) {
super(allocator,
config.getMinBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
? config.getMinBufferMs()
: DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
config.getMaxBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
? config.getMaxBufferMs()
: DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
config.getBufferForPlaybackMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
? config.getBufferForPlaybackMs()
: DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS ,
config.getBufferForPlaybackAfterRebufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
? config.getBufferForPlaybackAfterRebufferMs()
: DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
-1,
true,
config.getBackBufferDurationMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
? config.getBackBufferDurationMs()
: DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS,
DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);
runtime = Runtime.getRuntime();
ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(ThemedReactContext.ACTIVITY_SERVICE);
double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
? bufferConfig.getMaxHeapAllocationPercent()
: DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024);
}
@Override
public boolean shouldContinueLoading(long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
if (bufferingStrategy == BufferingStrategy.BufferingStrategyEnum.DisableBuffering) {
return false;
} else if (bufferingStrategy == BufferingStrategy.BufferingStrategyEnum.DependingOnMemory) {
// The goal of this algorithm is to pause video loading (increasing the buffer)
// when available memory on device become low.
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;
double minBufferMemoryReservePercent = bufferConfig.getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
? bufferConfig.getMinBufferMemoryReservePercent()
: ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
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) {
DebugLog.w(TAG, "Free memory reached 0, forcing garbage collection");
runtime.gc();
return false;
}
}
// "default" case or normal case for "DependingOnMemory"
return super.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed);
}
}
private void initializePlayer() {
ReactExoplayerView self = this;
Activity activity = themedReactContext.getCurrentActivity();
// This ensures all props have been settled, to avoid async racing conditions.
Source runningSource = source;
mainRunnable = () -> {
if (viewHasDropped && runningSource == source) {
return;
}
try {
if (runningSource.getUri() == null) {
return;
}
if (player == null) {
// Initialize core configuration and listeners
initializePlayerCore(self);
}
if (playerNeedsSource) {
// Will force display of shutter view if needed
exoPlayerView.updateShutterViewVisibility();
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(() -> {
// DRM initialization must run on a different thread
if (viewHasDropped && runningSource == source) {
return;
}
if (activity == null) {
DebugLog.e(TAG, "Failed to initialize Player!, null activity");
eventEmitter.onVideoError.invoke("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001");
return;
}
// Initialize handler to run on the main thread
activity.runOnUiThread(() -> {
if (viewHasDropped && runningSource == source) {
return;
}
try {
// Source initialization must run on the main thread
initializePlayerSource(runningSource);
} catch (Exception ex) {
self.playerNeedsSource = true;
DebugLog.e(TAG, "Failed to initialize Player! 1");
DebugLog.e(TAG, ex.toString());
ex.printStackTrace();
eventEmitter.onVideoError.invoke(ex.toString(), ex, "1001");
}
});
});
} else if (runningSource == source) {
initializePlayerSource(runningSource);
}
} catch (Exception ex) {
self.playerNeedsSource = true;
DebugLog.e(TAG, "Failed to initialize Player! 2");
DebugLog.e(TAG, ex.toString());
ex.printStackTrace();
eventEmitter.onVideoError.invoke(ex.toString(), ex, "1001");
}
};
mainHandler.postDelayed(mainRunnable, 1);
}
public void getCurrentPosition(Promise promise) {
if (player != null) {
float currentPosition = player.getCurrentPosition() / 1000.0f;
promise.resolve(currentPosition);
} else {
promise.reject("PLAYER_NOT_AVAILABLE", "Player is not initialized.");
}
}
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,
bufferConfig
);
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(getContext())
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
.setEnableDecoderFallback(true)
.forceEnableMediaCodecAsynchronousQueueing();
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory);
if (useCache) {
mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
}
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
player = new ExoPlayer.Builder(getContext(), renderersFactory)
.setTrackSelector(self.trackSelector)
.setBandwidthMeter(bandwidthMeter)
.setLoadControl(loadControl)
.setMediaSourceFactory(mediaSourceFactory)
.build();
ReactNativeVideoManager.Companion.getInstance().onInstanceCreated(instanceId, player);
refreshDebugState();
player.addListener(self);
player.setVolume(muted ? 0.f : audioVolume * 1);
exoPlayerView.setPlayer(player);
audioBecomingNoisyReceiver.setListener(self);
bandwidthMeter.addEventListener(new Handler(), self);
setPlayWhenReady(!isPaused);
playerNeedsSource = true;
PlaybackParameters params = new PlaybackParameters(rate, 1f);
player.setPlaybackParameters(params);
changeAudioOutput(this.audioOutput);
if(showNotificationControls) {
setupPlaybackService();
}
}
private AdsMediaSource initializeAds(MediaSource videoSource, Source runningSource) {
AdsProps adProps = runningSource.getAdsProps();
Uri uri = runningSource.getUri();
if (adProps != null && uri != null) {
Uri adTagUrl = adProps.getAdTagUrl();
if (adTagUrl != null) {
exoPlayerView.showAds();
// Create an AdsLoader.
ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader
.Builder(themedReactContext)
.setAdEventListener(this)
.setAdErrorListener(this);
if (adProps.getAdLanguage() != null) {
ImaSdkSettings imaSdkSettings = ImaSdkFactory.getInstance().createImaSdkSettings();
imaSdkSettings.setLanguage(adProps.getAdLanguage());
imaLoaderBuilder.setImaSdkSettings(imaSdkSettings);
}
adsLoader = imaLoaderBuilder.build();
adsLoader.setPlayer(player);
if (adsLoader != null) {
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
DataSpec adTagDataSpec = new DataSpec(adTagUrl);
return new AdsMediaSource(videoSource,
adTagDataSpec,
ImmutableList.of(uri, adTagUrl),
mediaSourceFactory, adsLoader, exoPlayerView);
}
}
}
exoPlayerView.hideAds();
return null;
}
private DrmSessionManager initializePlayerDrm() {
DrmSessionManager drmSessionManager = null;
DRMProps drmProps = source.getDrmProps();
// need to realign UUID in DRM Props from source
if (drmProps != null && drmProps.getDrmType() != null) {
UUID uuid = Util.getDrmUuid(drmProps.getDrmType());
if (uuid != null) {
try {
DebugLog.w(TAG, "drm buildDrmSessionManager");
drmSessionManager = buildDrmSessionManager(uuid, drmProps);
} 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.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
}
}
}
return drmSessionManager;
}
private void initializePlayerSource(Source runningSource) {
if (runningSource.getUri() == null) {
return;
}
/// init DRM
DrmSessionManager drmSessionManager = initializePlayerDrm();
if (drmSessionManager == null && runningSource.getDrmProps() != null && runningSource.getDrmProps().getDrmType() != null) {
// Failed to initialize DRM session manager - cannot continue
DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
return;
}
// init source to manage ads and external text tracks
MediaSource videoSource = buildMediaSource(runningSource.getUri(),
runningSource.getExtension(),
drmSessionManager,
runningSource.getCropStartMs(),
runningSource.getCropEndMs());
MediaSource mediaSourceWithAds = initializeAds(videoSource, runningSource);
MediaSource mediaSource = Objects.requireNonNullElse(mediaSourceWithAds, videoSource);
MediaSource subtitlesSource = buildTextSource();
if (subtitlesSource != null) {
MediaSource[] mediaSourceArray = {mediaSource, subtitlesSource};
mediaSource = new MergingMediaSource(mediaSourceArray);
}
// wait for player to be set
while (player == null) {
try {
wait();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
DebugLog.e(TAG, ex.toString());
}
}
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition);
player.setMediaSource(mediaSource, false);
} else if (runningSource.getStartPositionMs() > 0) {
player.setMediaSource(mediaSource, runningSource.getStartPositionMs());
} else {
player.setMediaSource(mediaSource, true);
}
player.prepare();
playerNeedsSource = false;
reLayoutControls();
eventEmitter.onVideoLoadStart.invoke();
loadVideoStarted = true;
finishPlayerInitialization();
}
private void finishPlayerInitialization() {
// Initializing the playerControlView
initializePlayerControl();
setControls(controls);
applyModifiers();
}
private void setupPlaybackService() {
if (!showNotificationControls || player == null) {
return;
}
playbackServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
playbackServiceBinder = (PlaybackServiceBinder) service;
try {
Activity currentActivity = themedReactContext.getCurrentActivity();
if (currentActivity != null) {
playbackServiceBinder.getService().registerPlayer(player,
(Class) currentActivity.getClass());
} else {
// Handle the case where currentActivity is null
DebugLog.w(TAG, "Could not register ExoPlayer: currentActivity is null");
}
} catch (Exception e) {
DebugLog.e(TAG, "Could not register ExoPlayer: " + e.getMessage());
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
try {
if (playbackServiceBinder != null) {
playbackServiceBinder.getService().unregisterPlayer(player);
}
} catch (Exception ignored) {}
playbackServiceBinder = null;
}
@Override
public void onNullBinding(ComponentName name) {
DebugLog.e(TAG, "Could not register ExoPlayer");
}
};
Intent intent = new Intent(themedReactContext, VideoPlaybackService.class);
intent.setAction(MediaSessionService.SERVICE_INTERFACE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
themedReactContext.startForegroundService(intent);
} else {
themedReactContext.startService(intent);
}
int flags;
if (Build.VERSION.SDK_INT >= 29) {
flags = Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES;
} else {
flags = Context.BIND_AUTO_CREATE;
}
themedReactContext.bindService(intent, playbackServiceConnection, flags);
}
private void cleanupPlaybackService() {
try {
if(player != null && playbackServiceBinder != null) {
playbackServiceBinder.getService().unregisterPlayer(player);
}
playbackServiceBinder = null;
if(playbackServiceConnection != null) {
themedReactContext.unbindService(playbackServiceConnection);
}
} catch(Exception e) {
DebugLog.w(TAG, "Cloud not cleanup playback service");
}
}
private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps) throws UnsupportedDrmException {
return buildDrmSessionManager(uuid, drmProps, 0);
}
private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps, int retryCount) throws UnsupportedDrmException {
if (Util.SDK_INT < 18) {
return null;
}
try {
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmProps.getDrmLicenseServer(),
buildHttpDataSourceFactory(false));
String[] keyRequestPropertiesArray = drmProps.getDrmLicenseHeader();
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.Builder()
.setUuidAndExoMediaDrmProvider(uuid, (_uuid) -> mediaDrm)
.setKeyRequestParameters(null)
.setMultiSession(drmProps.getMultiDrm())
.build(drmCallback);
} 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, drmProps, ++retryCount);
}
// Handle the unknow exception and emit to JS
eventEmitter.onVideoError.invoke(ex.toString(), ex, "3006");
return null;
}
}
private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) {
if (uri == null) {
throw new IllegalStateException("Invalid video uri");
}
int type;
if ("rtsp".equals(overrideExtension)) {
type = CONTENT_TYPE_RTSP;
} else {
type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension
: uri.getLastPathSegment());
}
config.setDisableDisconnectError(this.disableDisconnectError);
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder()
.setUri(uri);
// refresh custom Metadata
MediaMetadata customMetadata = ConfigurationUtils.buildCustomMetadata(source.getMetadata());
if (customMetadata != null) {
mediaItemBuilder.setMediaMetadata(customMetadata);
}
if (source.getAdsProps() != null) {
Uri adTagUrl = source.getAdsProps().getAdTagUrl();
if (adTagUrl != null) {
mediaItemBuilder.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(adTagUrl).build()
);
}
}
MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils.getLiveConfiguration(bufferConfig);
mediaItemBuilder.setLiveConfiguration(liveConfiguration.build());
MediaSource.Factory mediaSourceFactory;
DrmSessionManagerProvider drmProvider;
List streamKeys = new ArrayList<>();
if (drmSessionManager != null) {
drmProvider = ((_mediaItem) -> drmSessionManager);
} else {
drmProvider = new DefaultDrmSessionManagerProvider();
}
switch (type) {
case CONTENT_TYPE_SS:
if(!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) {
DebugLog.e("Exo Player Exception", "Smooth Streaming is not enabled!");
throw new IllegalStateException("Smooth Streaming is not enabled!");
}
mediaSourceFactory = new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false)
);
break;
case CONTENT_TYPE_DASH:
if(!BuildConfig.USE_EXOPLAYER_DASH) {
DebugLog.e("Exo Player Exception", "DASH is not enabled!");
throw new IllegalStateException("DASH is not enabled!");
}
mediaSourceFactory = new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false)
);
break;
case CONTENT_TYPE_HLS:
if (!BuildConfig.USE_EXOPLAYER_HLS) {
DebugLog.e("Exo Player Exception", "HLS is not enabled!");
throw new IllegalStateException("HLS is not enabled!");
}
mediaSourceFactory = new HlsMediaSource.Factory(
mediaDataSourceFactory
).setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation());
break;
case CONTENT_TYPE_OTHER:
if ("asset".equals(uri.getScheme())) {
try {
DataSource.Factory assetDataSourceFactory = DataSourceUtil.buildAssetDataSourceFactory(themedReactContext, uri);
mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory);
} catch (Exception e) {
throw new IllegalStateException("cannot open input file" + uri);
}
} else if ("file".equals(uri.getScheme()) ||
!useCache) {
mediaSourceFactory = new ProgressiveMediaSource.Factory(
mediaDataSourceFactory
);
} else {
mediaSourceFactory = new ProgressiveMediaSource.Factory(
RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))
);
}
break;
case CONTENT_TYPE_RTSP:
if (!BuildConfig.USE_EXOPLAYER_RTSP) {
DebugLog.e("Exo Player Exception", "RTSP is not enabled!");
throw new IllegalStateException("RTSP is not enabled!");
}
mediaSourceFactory = new RtspMediaSource.Factory();
break;
default: {
throw new IllegalStateException("Unsupported type: " + type);
}
}
if (cmcdConfigurationFactory != null) {
mediaSourceFactory = mediaSourceFactory.setCmcdConfigurationFactory(
cmcdConfigurationFactory::createCmcdConfiguration
);
}
MediaItem mediaItem = mediaItemBuilder.setStreamKeys(streamKeys).build();
MediaSource mediaSource = mediaSourceFactory
.setDrmSessionManagerProvider(drmProvider)
.setLoadErrorHandlingPolicy(
config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount())
)
.createMediaSource(mediaItem);
if (cropStartMs >= 0 && cropEndMs >= 0) {
return new ClippingMediaSource(mediaSource, cropStartMs * 1000, cropEndMs * 1000);
} else if (cropStartMs >= 0) {
return new ClippingMediaSource(mediaSource, cropStartMs * 1000, TIME_END_OF_SOURCE);
} else if (cropEndMs >= 0) {
return new ClippingMediaSource(mediaSource, 0, cropEndMs * 1000);
}
return mediaSource;
}
@Nullable
private MediaSource buildTextSource() {
if (source.getSideLoadedTextTracks() == null) {
return null;
}
List subtitleConfigurations = new ArrayList<>();
for (SideLoadedTextTrack track : source.getSideLoadedTextTracks().getTracks()) {
MediaItem.SubtitleConfiguration subtitleConfiguration = new MediaItem.SubtitleConfiguration.Builder(track.getUri())
.setMimeType(track.getType())
.setLanguage(track.getLanguage())
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setRoleFlags(C.ROLE_FLAG_SUBTITLE)
.setLabel(track.getTitle())
.build();
subtitleConfigurations.add(subtitleConfiguration);
}
MediaItem subtitlesMediaItem = new MediaItem.Builder()
.setUri(source.getUri())
.setSubtitleConfigurations(subtitleConfigurations).build();
return new DefaultMediaSourceFactory(mediaDataSourceFactory).createMediaSource(subtitlesMediaItem);
}
private void releasePlayer() {
if (player != null) {
if(playbackServiceBinder != null) {
playbackServiceBinder.getService().unregisterPlayer(player);
themedReactContext.unbindService(playbackServiceConnection);
}
updateResumePosition();
player.release();
player.removeListener(this);
trackSelector = null;
ReactNativeVideoManager.Companion.getInstance().onInstanceRemoved(instanceId, player);
player = null;
}
if (adsLoader != null) {
adsLoader.release();
adsLoader = null;
}
progressHandler.removeMessages(SHOW_PROGRESS);
audioBecomingNoisyReceiver.removeListener();
bandwidthMeter.removeEventListener(this);
if (mainHandler != null && mainRunnable != null) {
mainHandler.removeCallbacks(mainRunnable);
mainRunnable = null;
}
}
private static class OnAudioFocusChangedListener implements AudioManager.OnAudioFocusChangeListener {
private final ReactExoplayerView view;
private final ThemedReactContext themedReactContext;
private OnAudioFocusChangedListener(ReactExoplayerView view, ThemedReactContext themedReactContext) {
this.view = view;
this.themedReactContext = themedReactContext;
}
@Override
public void onAudioFocusChange(int focusChange) {
Activity activity = themedReactContext.getCurrentActivity();
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
view.hasAudioFocus = false;
view.eventEmitter.onAudioFocusChanged.invoke(false);
// FIXME this pause can cause issue if content doesn't have pause capability (can happen on live channel)
if (activity != null) {
activity.runOnUiThread(view::pausePlayback);
}
view.audioManager.abandonAudioFocus(this);
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
view.eventEmitter.onAudioFocusChanged.invoke(false);
break;
case AudioManager.AUDIOFOCUS_GAIN:
view.hasAudioFocus = true;
view.eventEmitter.onAudioFocusChanged.invoke(true);
break;
default:
break;
}
if (view.player != null && activity != null) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// Lower the volume
if (!view.muted) {
activity.runOnUiThread(() ->
view.player.setVolume(view.audioVolume * 0.8f)
);
}
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Raise it back to normal
if (!view.muted) {
activity.runOnUiThread(() ->
view.player.setVolume(view.audioVolume * 1)
);
}
}
}
}
}
private boolean requestAudioFocus() {
if (disableFocus || source.getUri() == null || this.hasAudioFocus) {
return true;
}
int result = audioManager.requestAudioFocus(audioFocusChangeListener,
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 {
player.setPlayWhenReady(false);
}
}
private void resumePlayback() {
if (player != null) {
if (!player.getPlayWhenReady()) {
setPlayWhenReady(true);
}
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
}
}
private void pausePlayback() {
if (player != null) {
if (player.getPlayWhenReady()) {
setPlayWhenReady(false);
}
}
setKeepScreenOn(false);
}
private void stopPlayback() {
onStopPlayback();
releasePlayer();
}
private void onStopPlayback() {
audioManager.abandonAudioFocus(audioFocusChangeListener);
}
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, source.getHeaders());
}
/**
* 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, source.getHeaders());
}
// AudioBecomingNoisyListener implementation
@Override
public void onAudioBecomingNoisy() {
eventEmitter.onVideoAudioBecomingNoisy.invoke();
}
// Player.Listener implementation
@Override
public void onIsLoadingChanged(boolean isLoading) {
// Do nothing.
}
@Override
public void onEvents(@NonNull 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.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
switch (playbackState) {
case Player.STATE_IDLE:
text += "idle";
eventEmitter.onVideoIdle.invoke();
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.onReadyForDisplay.invoke();
onBuffering(false);
clearProgressMessageHandler(); // ensure there is no other message
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";
updateProgress();
eventEmitter.onVideoEnd.invoke();
onStopPlayback();
setKeepScreenOn(false);
break;
default:
text += "unknown";
break;
}
DebugLog.d(TAG, text);
}
}
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 (!player.isPlayingAd() && 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();
boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
String trackId = videoFormat != null ? videoFormat.id : null;
// Properties that must be accessed on the main thread
long duration = player.getDuration();
long currentPosition = player.getCurrentPosition();
ArrayList