diff --git a/README.md b/README.md index ab9715d4..9c396a2f 100644 --- a/README.md +++ b/README.md @@ -489,7 +489,7 @@ Platforms: iOS * **landscape** * **portrait** -Platforms: iOS +Platforms: Android ExoPlayer, iOS #### headers Pass headers to the HTTP client. Can be used for authorization. Headers must be a part of the source object. diff --git a/android-exoplayer/src/main/AndroidManifest.xml b/android-exoplayer/src/main/AndroidManifest.xml index 3535ad44..53e507e0 100644 --- a/android-exoplayer/src/main/AndroidManifest.xml +++ b/android-exoplayer/src/main/AndroidManifest.xml @@ -1,3 +1,9 @@ + + + diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java new file mode 100644 index 00000000..d2e6f2d3 --- /dev/null +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java @@ -0,0 +1,155 @@ +package com.brentvatne.exoplayer; + +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.ImageView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.brentvatne.react.R; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ui.PlayerControlView; + +public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implements ReactExoplayerView.FullScreenDelegate { + public static final String EXTRA_EXO_PLAYER_VIEW_ID = "extra_id"; + public static final String EXTRA_ORIENTATION = "extra_orientation"; + + private ReactExoplayerView exoplayerView; + private PlayerControlView playerControlView; + private SimpleExoPlayer player; + + @Override + public void onCreate(Bundle savedInstanceState) { + int exoplayerViewId = getIntent().getIntExtra(EXTRA_EXO_PLAYER_VIEW_ID, -1); + exoplayerView = ReactExoplayerView.getViewInstance(exoplayerViewId); + if (exoplayerView == null) { + finish(); + return; + } + super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + String orientation = getIntent().getStringExtra(EXTRA_ORIENTATION); + if ("landscape".equals(orientation)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + } else if ("portrait".equals(orientation)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } + setContentView(R.layout.exo_player_fullscreen_video); + player = exoplayerView.getPlayer(); + + ExoPlayerView playerView = findViewById(R.id.player_view); + playerView.setPlayer(player); + playerView.setOnClickListener(v -> togglePlayerControlVisibility()); + + playerControlView = findViewById(R.id.player_controls); + playerControlView.setPlayer(player); + // Set the fullscreen button to "close fullscreen" icon + ImageView fullscreenIcon = playerControlView.findViewById(R.id.exo_fullscreen_icon); + fullscreenIcon.setImageResource(R.drawable.exo_controls_fullscreen_exit); + playerControlView.findViewById(R.id.exo_fullscreen_button) + .setOnClickListener(v -> { + if (exoplayerView != null) { + exoplayerView.setFullscreen(false); + } + }); + //Handling the playButton click event + playerControlView.findViewById(R.id.exo_play).setOnClickListener(v -> { + if (player != null && player.getPlaybackState() == Player.STATE_ENDED) { + player.seekTo(0); + } + if (exoplayerView != null) { + exoplayerView.setPausedModifier(false); + } + }); + + //Handling the pauseButton click event + playerControlView.findViewById(R.id.exo_pause).setOnClickListener(v -> { + if (exoplayerView != null) { + exoplayerView.setPausedModifier(true); + } + }); + } + + @Override + public void onResume() { + super.onResume(); + if (exoplayerView != null) { + exoplayerView.syncPlayerState(); + exoplayerView.registerFullScreenDelegate(this); + } + } + + @Override + public void onPause() { + super.onPause(); + player.setPlayWhenReady(false); + if (exoplayerView != null) { + exoplayerView.registerFullScreenDelegate(null); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + playerControlView.postDelayed(this::hideSystemUI, 200); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if ((keyCode == KeyEvent.KEYCODE_BACK)) { + if (exoplayerView != null) { + exoplayerView.setFullscreen(false); + return false; + } + return true; + } + return super.onKeyDown(keyCode, event); + } + + private void togglePlayerControlVisibility() { + if (playerControlView.isVisible()) { + playerControlView.hide(); + } else { + playerControlView.show(); + } + } + + /** + * Enables regular immersive mode. + */ + private void hideSystemUI() { + View decorView = getWindow().getDecorView(); + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_IMMERSIVE + // Set the content to appear under the system bars so that the + // content doesn't resize when the system bars hide and show. + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + // Hide the nav bar and status bar + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN); + } + + /** + * Shows the system bars by removing all the flags + * except for the ones that make the content appear under the system bars. + */ + private void showSystemUI() { + View decorView = getWindow().getDecorView(); + decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + + @Override + public void closeFullScreen() { + finish(); + } +} diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index 61cb0dd6..2e2f3169 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -129,9 +129,6 @@ public final class ExoPlayerView extends FrameLayout { * @param player The {@link SimpleExoPlayer} to use. */ public void setPlayer(SimpleExoPlayer player) { - if (this.player == player) { - return; - } if (this.player != null) { this.player.removeTextOutput(componentListener); this.player.removeVideoListener(componentListener); diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index fe95fdf4..482ff939 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1,8 +1,8 @@ package com.brentvatne.exoplayer; import android.annotation.SuppressLint; -import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.media.AudioManager; import android.net.Uri; import android.os.Handler; @@ -10,7 +10,6 @@ import android.os.Message; import android.text.TextUtils; import android.util.Log; import android.view.View; -import android.view.Window; import android.view.accessibility.CaptioningManager; import android.widget.FrameLayout; import android.widget.ImageButton; @@ -30,7 +29,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -39,7 +37,6 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; @@ -61,8 +58,8 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.ExoTrackSelection; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -76,9 +73,10 @@ import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.util.ArrayList; +import java.util.HashMap; import java.util.Locale; -import java.util.UUID; import java.util.Map; +import java.util.UUID; @SuppressLint("ViewConstructor") class ReactExoplayerView extends FrameLayout implements @@ -100,6 +98,9 @@ class ReactExoplayerView extends FrameLayout implements DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } + private static Map instances = new HashMap<>(); + private FullScreenDelegate fullScreenDelegate; + private final VideoEventEmitter eventEmitter; private final ReactExoplayerConfig config; private final DefaultBandwidthMeter bandwidthMeter; @@ -118,7 +119,9 @@ class ReactExoplayerView extends FrameLayout implements private long resumePosition; private boolean loadVideoStarted; private boolean isFullscreen; + private String fullScreenOrientation; private boolean isInBackground; + private boolean isInFullscreen; private boolean isPaused; private boolean isBuffering; private boolean muted = false; @@ -134,8 +137,6 @@ class ReactExoplayerView extends FrameLayout implements private int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; private int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - private Handler mainHandler; - // Props from React private Uri srcUri; private String extension; @@ -202,7 +203,6 @@ class ReactExoplayerView extends FrameLayout implements createViews(); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); } @@ -227,8 +227,6 @@ class ReactExoplayerView extends FrameLayout implements exoPlayerView.setLayoutParams(layoutParams); addView(exoPlayerView, 0, layoutParams); - - mainHandler = new Handler(); } @Override @@ -251,7 +249,15 @@ class ReactExoplayerView extends FrameLayout implements @Override public void onHostResume() { if (!playInBackground || !isInBackground) { - setPlayWhenReady(!isPaused); + if (isInFullscreen) { + if (player != null) { + exoPlayerView.setPlayer(player); + syncPlayerState(); + } + isInFullscreen = false; + } else { + setPlayWhenReady(!isPaused); + } } isInBackground = false; } @@ -272,6 +278,7 @@ class ReactExoplayerView extends FrameLayout implements public void cleanUpResources() { stopPlayback(); + instances.remove(this.getId()); } //BandwidthMeter.EventListener implementation @@ -290,6 +297,29 @@ class ReactExoplayerView extends FrameLayout implements } } + public static ReactExoplayerView getViewInstance(Integer uid) { + return instances.get(uid); + } + + public SimpleExoPlayer getPlayer() { + return player; + } + + public void syncPlayerState() { + if (player == null) return; + if (player.getPlaybackState() == Player.STATE_ENDED) { + // Try to get last frame displayed + player.seekTo(player.getDuration() - 200); + player.setPlayWhenReady(true); + } else { + player.setPlayWhenReady(!isPaused); + } + } + + public void registerFullScreenDelegate(FullScreenDelegate delegate) { + this.fullScreenDelegate = delegate; + } + // Internal methods /** @@ -305,6 +335,15 @@ class ReactExoplayerView extends FrameLayout implements } } + private void showFullscreen() { + instances.put(this.getId(), this); + Intent intent = new Intent(getContext(), ExoPlayerFullscreenVideoActivity.class); + intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_EXO_PLAYER_VIEW_ID, this.getId()); + intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_ORIENTATION, this.fullScreenOrientation); + getContext().startActivity(intent); + isInFullscreen = true; + } + /** * Initializing Player control */ @@ -317,6 +356,7 @@ class ReactExoplayerView extends FrameLayout implements playerControlView.setPlayer(player); playerControlView.show(); playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container); + playerControlView.findViewById(R.id.exo_fullscreen_button).setOnClickListener(v -> setFullscreen(true)); // Invoking onClick event for exoplayerView exoPlayerView.setOnClickListener(new OnClickListener() { @@ -389,6 +429,7 @@ class ReactExoplayerView extends FrameLayout implements } private void initializePlayer() { + themedReactContext.addLifecycleEventListener(this); ReactExoplayerView self = this; // This ensures all props have been settled, to avoid async racing conditions. new Handler().postDelayed(new Runnable() { @@ -411,7 +452,7 @@ class ReactExoplayerView extends FrameLayout implements new DefaultRenderersFactory(getContext()) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) - .setTrackSelector​(trackSelector) + .setTrackSelector(trackSelector) .setBandwidthMeter(bandwidthMeter) .setLoadControl(defaultLoadControl) .build(); @@ -641,9 +682,6 @@ class ReactExoplayerView extends FrameLayout implements } private void onStopPlayback() { - if (isFullscreen) { - setFullscreen(false); - } audioManager.abandonAudioFocus(this); } @@ -1297,34 +1335,23 @@ class ReactExoplayerView extends FrameLayout implements return; // Avoid generating events when nothing is changing } isFullscreen = fullscreen; - - Activity activity = themedReactContext.getCurrentActivity(); - if (activity == null) { - return; - } - Window window = activity.getWindow(); - View decorView = window.getDecorView(); - int uiOptions; if (isFullscreen) { - if (Util.SDK_INT >= 19) { // 4.4+ - uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION - | SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | SYSTEM_UI_FLAG_FULLSCREEN; - } else { - uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION - | SYSTEM_UI_FLAG_FULLSCREEN; - } eventEmitter.fullscreenWillPresent(); - decorView.setSystemUiVisibility(uiOptions); + showFullscreen(); eventEmitter.fullscreenDidPresent(); } else { - uiOptions = View.SYSTEM_UI_FLAG_VISIBLE; eventEmitter.fullscreenWillDismiss(); - decorView.setSystemUiVisibility(uiOptions); + if (fullScreenDelegate != null) { + fullScreenDelegate.closeFullScreen(); + } eventEmitter.fullscreenDidDismiss(); } } + public void setFullscreenOrientation(String orientation) { + this.fullScreenOrientation = orientation; + } + public void setUseTextureView(boolean useTextureView) { boolean finallyUseTextureView = useTextureView && this.drmUUID == null; exoPlayerView.setUseTextureView(finallyUseTextureView); @@ -1394,4 +1421,8 @@ class ReactExoplayerView extends FrameLayout implements } } } + + public interface FullScreenDelegate { + void closeFullScreen(); + } } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index eccbee75..02ec4d48 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -64,6 +64,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager - + + + + + + diff --git a/android-exoplayer/src/main/res/layout/exo_player_fullscreen_video.xml b/android-exoplayer/src/main/res/layout/exo_player_fullscreen_video.xml new file mode 100644 index 00000000..671a00ee --- /dev/null +++ b/android-exoplayer/src/main/res/layout/exo_player_fullscreen_video.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 3535ad44..42a828cc 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ + package="com.brentvatne.react">