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 audioTracks = getAudioTrackInfo(); ArrayList textTracks = getTextTrackInfo(); if (source.getContentStartTime() != -1) { ExecutorService es = Executors.newSingleThreadExecutor(); es.execute(() -> { // To prevent ANRs caused by getVideoTrackInfo we run this on a different thread and notify the player only when we're done ArrayList videoTracks = getVideoTrackInfoFromManifest(); if (videoTracks != null) { isUsingContentResolution = true; } eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height, audioTracks, textTracks, videoTracks, trackId ); }); return; } ArrayList videoTracks = getVideoTrackInfo(); eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height, audioTracks, textTracks, videoTracks, trackId); refreshControlsStyles(); } } private static boolean isTrackSelected(TrackSelection selection, TrackGroup group, int trackIndex){ return selection != null && selection.getTrackGroup() == group && selection.indexOf( trackIndex ) != C.INDEX_UNSET; } private ArrayList getAudioTrackInfo() { ArrayList audioTracks = new ArrayList<>(); 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); TrackSelectionArray selectionArray = player.getCurrentTrackSelections(); TrackSelection selection = selectionArray.get( C.TRACK_TYPE_AUDIO ); for (int i = 0; i < groups.length; ++i) { TrackGroup group = groups.get(i); Format format = group.getFormat(0); Track audioTrack = exoplayerTrackToGenericTrack(format, i, selection, group); audioTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); audioTracks.add(audioTrack); } return audioTracks; } private VideoTrack exoplayerVideoTrackToGenericVideoTrack(Format format, int trackIndex) { VideoTrack videoTrack = new VideoTrack(); videoTrack.setWidth(format.width == Format.NO_VALUE ? 0 : format.width); videoTrack.setHeight(format.height == Format.NO_VALUE ? 0 : format.height); videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); videoTrack.setRotation(format.rotationDegrees); if (format.codecs != null) videoTrack.setCodecs(format.codecs); videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id); videoTrack.setIndex(trackIndex); return videoTrack; } private ArrayList getVideoTrackInfo() { ArrayList videoTracks = new ArrayList<>(); if (trackSelector == null) { // Likely player is unmounting so no video tracks are available anymore return videoTracks; } MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); int index = getTrackRendererIndex(C.TRACK_TYPE_VIDEO); if (info == null || index == C.INDEX_UNSET) { return videoTracks; } TrackGroupArray groups = info.getTrackGroups(index); 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)) { VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, trackIndex); videoTracks.add(videoTrack); } } } return videoTracks; } private ArrayList getVideoTrackInfoFromManifest() { return this.getVideoTrackInfoFromManifest(0); } // We need retry count to in case where minefest request fails from poor network conditions @WorkerThread private ArrayList getVideoTrackInfoFromManifest(int retryCount) { ExecutorService es = Executors.newSingleThreadExecutor(); final DataSource dataSource = this.mediaDataSourceFactory.createDataSource(); final Uri sourceUri = source.getUri(); final long startTime = source.getContentStartTime() * 1000 - 100; // s -> ms with 100ms offset Future> result = es.submit(new Callable() { final DataSource ds = dataSource; final Uri uri = sourceUri; final long startTimeUs = startTime * 1000; // ms -> us public ArrayList call() { ArrayList videoTracks = new ArrayList<>(); 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 (isFormatSupported(format)) { if (representation.presentationTimeOffsetUs <= startTimeUs) { break; } hasFoundContentPeriod = true; VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, representationIndex); videoTracks.add(videoTrack); } } if (hasFoundContentPeriod) { return videoTracks; } } } } catch (Exception e) { DebugLog.w(TAG, "error in getVideoTrackInfoFromManifest:" + e.getMessage()); } return null; } }); try { ArrayList results = result.get(3000, TimeUnit.MILLISECONDS); if (results == null && retryCount < 1) { return this.getVideoTrackInfoFromManifest(++retryCount); } es.shutdown(); return results; } catch (Exception e) { DebugLog.w(TAG, "error in getVideoTrackInfoFromManifest handling request:" + e.getMessage()); } return null; } private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection, TrackGroup group) { Track track = new Track(); track.setIndex(trackIndex); if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType); if (format.language != null) track.setLanguage(format.language); if (format.label != null) track.setTitle(format.label); track.setSelected(isTrackSelected(selection, group, trackIndex)); return track; } private ArrayList getTextTrackInfo() { ArrayList textTracks = new ArrayList<>(); if (trackSelector == null) { return textTracks; } MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT); if (info == null || index == C.INDEX_UNSET) { return textTracks; } TrackSelectionArray selectionArray = player.getCurrentTrackSelections(); TrackSelection selection = selectionArray.get( C.TRACK_TYPE_VIDEO ); TrackGroupArray groups = info.getTrackGroups(index); for (int i = 0; i < groups.length; ++i) { TrackGroup group = groups.get(i); Format format = group.getFormat(0); Track textTrack = exoplayerTrackToGenericTrack(format, i, selection, group); textTracks.add(textTrack); } return textTracks; } private void onBuffering(boolean buffering) { if (isBuffering == buffering) { return; } if (isPaused && isSeeking && !buffering) { eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition); isSeeking = false; } isBuffering = buffering; eventEmitter.onVideoBuffer.invoke(buffering); } @Override public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) { if (reason == Player.DISCONTINUITY_REASON_SEEK) { isSeeking = true; seekPosition = newPosition.positionMs; 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); } } 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) { updateProgress(); eventEmitter.onVideoEnd.invoke(); } } @Override public void onTimelineChanged(@NonNull Timeline timeline, int reason) { // Do nothing. } @Override public void onTracksChanged(@NonNull Tracks tracks) { eventEmitter.onTextTracks.invoke(getTextTrackInfo()); eventEmitter.onAudioTracks.invoke(getAudioTrackInfo()); eventEmitter.onVideoTracks.invoke(getVideoTrackInfo()); } @Override public void onPlaybackParametersChanged(PlaybackParameters params) { eventEmitter.onPlaybackRateChange.invoke(params.speed); } @Override public void onVolumeChanged(float volume) { eventEmitter.onVolumeChange.invoke(volume); } @Override public void onIsPlayingChanged(boolean isPlaying) { if (isPlaying && isSeeking) { eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition); } eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking); if (isPlaying) { isSeeking = false; } } @Override public void onPlayerError(@NonNull PlaybackException e) { String errorString = "ExoPlaybackException: " + PlaybackException.getErrorCodeName(e.errorCode); String errorCode = "2" + e.errorCode; 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.onVideoError.invoke(errorString, e, errorCode); playerNeedsSource = true; if (isBehindLiveWindow(e)) { clearResumePosition(); if (player != null) { player.seekToDefaultPosition(); player.prepare(); } } else { updateResumePosition(); } } 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(@NonNull Metadata metadata) { ArrayList metadataArray = new ArrayList<>(); for (int i = 0; i < metadata.length(); i++) { Metadata.Entry entry = metadata.get(i); if (entry instanceof Id3Frame) { Id3Frame frame = (Id3Frame) metadata.get(i); String value = ""; if (frame instanceof TextInformationFrame) { TextInformationFrame txxxFrame = (TextInformationFrame) frame; value = txxxFrame.value; } TimedMetadata timedMetadata = new TimedMetadata(frame.id, value); metadataArray.add(timedMetadata); } else if (entry instanceof EventMessage) { EventMessage eventMessage = (EventMessage) entry; TimedMetadata timedMetadata = new TimedMetadata(eventMessage.schemeIdUri, eventMessage.value); metadataArray.add(timedMetadata); } else { DebugLog.d(TAG, "unhandled metadata " + entry); } } eventEmitter.onTimedMetadata.invoke(metadataArray); } public void onCues(CueGroup cueGroup) { if (!cueGroup.cues.isEmpty() && cueGroup.cues.get(0).text != null) { String subtitleText = cueGroup.cues.get(0).text.toString(); eventEmitter.onTextTrackDataChanged.invoke(subtitleText); } } // ReactExoplayerViewManager public api public void setSrc(Source source) { if (source.getUri() != null) { clearResumePosition(); boolean isSourceEqual = source.isEquals(this.source); hasDrmFailed = false; this.source = source; this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter, source.getHeaders()); if (source.getCmcdProps() != null) { CMCDConfig cmcdConfig = new CMCDConfig(source.getCmcdProps()); CmcdConfiguration.Factory factory = cmcdConfig.toCmcdConfigurationFactory(); this.setCmcdConfigurationFactory(factory); } else { this.setCmcdConfigurationFactory(null); } if (!isSourceEqual) { playerNeedsSource = true; initializePlayer(); } } else { clearSrc(); } } public void clearSrc() { if (source.getUri() != null) { if (player != null) { player.stop(); player.clearMediaItems(); } } exoPlayerView.hideAds(); this.source = new Source(); this.mediaDataSourceFactory = null; clearResumePosition(); } public void setProgressUpdateInterval(final float progressUpdateInterval) { mProgressUpdateInterval = progressUpdateInterval; } public void setReportBandwidth(boolean reportBandwidth) { mReportBandwidth = reportBandwidth; } public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) { if (exoPlayerView != null) { 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 disableTrack(int rendererIndex) { DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters() .buildUpon() .setRendererDisabled(rendererIndex, true) .build(); trackSelector.setParameters(disableParameters); } public void setSelectedTrack(int trackType, String type, String 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 tracks = new ArrayList<>(); tracks.add(0); if (TextUtils.isEmpty(type)) { type = "default"; } if ("disabled".equals(type)) { disableTrack(rendererIndex); return; } else if ("language".equals(type)) { for (int i = 0; i < groups.length; ++i) { Format format = groups.get(i).getFormat(0); if (format.language != null && format.language.equals(value)) { groupIndex = i; break; } } } else if ("title".equals(type)) { for (int i = 0; i < groups.length; ++i) { Format format = groups.get(i).getFormat(0); if (format.label != null && format.label.equals(value)) { groupIndex = i; break; } } } else if ("index".equals(type)) { int iValue = ReactBridgeUtils.safeParseInt(value, -1); if (iValue != -1) { if (trackType == C.TRACK_TYPE_VIDEO && groups.length == 1) { groupIndex = 0; if (iValue < groups.get(groupIndex).length) { tracks.set(0, iValue); } } else if (iValue < groups.length) { groupIndex = iValue; } } } else if ("resolution".equals(type)) { int height = ReactBridgeUtils.safeParseInt(value, -1); if (height != -1) { 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); ArrayList allTracks = new ArrayList<>(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; 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)); } } } } if (groupIndex == C.INDEX_UNSET) { disableTrack(rendererIndex); return; } TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks); DefaultTrackSelector.Parameters.Builder selectionParameters = trackSelector.getParameters() .buildUpon() .setExceedAudioConstraintsIfNecessary(true) .setExceedRendererCapabilitiesIfNecessary(true) .setExceedVideoConstraintsIfNecessary(true) .setRendererDisabled(rendererIndex, false) .clearOverridesOfType(selectionOverride.getType()); if (trackType == C.TRACK_TYPE_VIDEO && isUsingVideoABR()) { selectionParameters.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate); } else { selectionParameters.addOverride(selectionOverride); } trackSelector.setParameters(selectionParameters.build()); } 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; 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, String value) { videoTrackType = type; videoTrackValue = value; if (!loadVideoStarted) setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); } public void setSelectedAudioTrack(String type, String value) { audioTrackType = type; audioTrackValue = value; setSelectedTrack(C.TRACK_TYPE_AUDIO, audioTrackType, audioTrackValue); } public void setSelectedTextTrack(String type, String value) { textTrackType = type; textTrackValue = value; setSelectedTrack(C.TRACK_TYPE_TEXT, textTrackType, textTrackValue); } public void setPausedModifier(boolean paused) { isPaused = paused; if (player != null) { if (!paused) { resumePlayback(); } else { pausePlayback(); } } } public void setMutedModifier(boolean muted) { this.muted = muted; if (player != null) { player.setVolume(muted ? 0.f : audioVolume); } } private void changeAudioOutput(AudioOutput output) { if (player != null) { int streamType = output.getStreamType(); int usage = Util.getAudioUsageForStreamType(streamType); int contentType = Util.getAudioContentTypeForStreamType(streamType); AudioAttributes audioAttributes = new AudioAttributes.Builder().setUsage(usage) .setContentType(contentType) .build(); player.setAudioAttributes(audioAttributes, false); AudioManager audioManager = (AudioManager) themedReactContext.getSystemService(Context.AUDIO_SERVICE); boolean isSpeakerOutput = output == AudioOutput.SPEAKER; audioManager.setMode( isSpeakerOutput ? AudioManager.MODE_NORMAL : AudioManager.MODE_IN_COMMUNICATION); audioManager.setSpeakerphoneOn(isSpeakerOutput); } } public void setAudioOutput(AudioOutput output) { if (audioOutput != output) { this.audioOutput = output; changeAudioOutput(output); } } public void setVolumeModifier(float volume) { audioVolume = volume; if (player != null) { player.setVolume(audioVolume); } } public void seekTo(long positionMs) { if (player != null) { player.seekTo(positionMs); } } public void setRateModifier(float newRate) { if (newRate <= 0) { DebugLog.w(TAG, "cannot set rate <= 0"); return; } rate = newRate; if (player != null) { PlaybackParameters params = new PlaybackParameters(rate, 1f); player.setPlaybackParameters(params); } } public void setMaxBitRateModifier(int newMaxBitRate) { maxBitRate = newMaxBitRate; if (player != null && isUsingVideoABR()) { // do not apply yet if not auto trackSelector.setParameters(trackSelector.buildUponParameters() .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); } } 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 setShowNotificationControls(boolean showNotificationControls) { this.showNotificationControls = showNotificationControls; if (playbackServiceConnection == null && showNotificationControls) { setupPlaybackService(); } else if(!showNotificationControls && playbackServiceConnection != null) { cleanupPlaybackService(); } } public void setBufferingStrategy(BufferingStrategy.BufferingStrategyEnum _bufferingStrategy) { bufferingStrategy = _bufferingStrategy; } public boolean getPreventsDisplaySleepDuringVideoPlayback() { return preventsDisplaySleepDuringVideoPlayback; } private void updateFullScreenButtonVisibility() { if (playerControlView != null) { final ImageButton fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen); //Handling the fullScreenButton click event if (isFullscreen && fullScreenPlayerView != null && !fullScreenPlayerView.isShowing()) { fullScreenButton.setVisibility(GONE); } else { fullScreenButton.setVisibility(VISIBLE); } } } 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 (isFullscreen) { fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, playerControlView, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { setFullscreen(false); } }, controlsConfig); eventEmitter.onVideoFullscreenPlayerWillPresent.invoke(); if (fullScreenPlayerView != null) { fullScreenPlayerView.show(); } UiThreadUtil.runOnUiThread(() -> { eventEmitter.onVideoFullscreenPlayerDidPresent.invoke(); }); } else { eventEmitter.onVideoFullscreenPlayerWillDismiss.invoke(); if (fullScreenPlayerView != null) { fullScreenPlayerView.dismiss(); reLayoutControls(); setControls(controls); } UiThreadUtil.runOnUiThread(() -> { eventEmitter.onVideoFullscreenPlayerDidDismiss.invoke(); }); } // need to be done at the end to avoid hiding fullscreen control button when fullScreenPlayerView is shown updateFullScreenButtonVisibility(); } public void setHideShutterView(boolean hideShutterView) { exoPlayerView.setHideShutterView(hideShutterView); } public void setBufferConfig(BufferConfig config) { bufferConfig = config; if (bufferConfig.getCacheSize() > 0) { RNVSimpleCache.INSTANCE.setSimpleCache( this.getContext(), bufferConfig.getCacheSize() ); useCache = true; } else { useCache = false; } releasePlayer(); initializePlayer(); } @Override public void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { DebugLog.d("DRM Info", "onDrmKeysLoaded"); } @Override public void onDrmSessionAcquired(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, int state) { DebugLog.d("DRM Info", "onDrmSessionAcquired"); } @Override public void onDrmSessionReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { DebugLog.d("DRM Info", "onDrmSessionReleased"); } @Override public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, @NonNull Exception e) { DebugLog.d("DRM Info", "onDrmSessionManagerError"); eventEmitter.onVideoError.invoke("onDrmSessionManagerError", e, "3002"); } @Override public void onDrmKeysRestored(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { DebugLog.d("DRM Info", "onDrmKeysRestored"); } @Override public void onDrmKeysRemoved(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { DebugLog.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 (controls) { addPlayerControl(); updateFullScreenButtonVisibility(); } else { int indexOfPC = indexOfChild(playerControlView); if (indexOfPC != -1) { removeViewAt(indexOfPC); } } refreshControlsStyles(); } public void setSubtitleStyle(SubtitleStyle style) { exoPlayerView.setSubtitleStyle(style); } public void setShutterColor(Integer color) { exoPlayerView.setShutterColor(color); } @Override public void onAdEvent(AdEvent adEvent) { if (adEvent.getAdData() != null) { eventEmitter.onReceiveAdEvent.invoke(adEvent.getType().name(), adEvent.getAdData()); } else { eventEmitter.onReceiveAdEvent.invoke(adEvent.getType().name(), null); } } @Override public void onAdError(AdErrorEvent adErrorEvent) { AdError error = adErrorEvent.getError(); Map errMap = Map.of( "message", error.getMessage(), "code", String.valueOf(error.getErrorCode()), "type", String.valueOf(error.getErrorType()) ); eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap); } public void setControlsStyles(ControlsConfig controlsStyles) { controlsConfig = controlsStyles; refreshControlsStyles(); } }