diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 5ecc35034..cb8f0961f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -240,10 +240,6 @@ public final class VideoDetailFragment playerUi.ifPresent(MainPlayerUi::toggleFullscreen); } - if (playerIsNotStopped() && player.videoPlayerSelected()) { - addVideoPlayerView(); - } - //noinspection SimplifyOptionalCallChains if (playAfterConnect || (currentInfo != null @@ -335,6 +331,9 @@ public final class VideoDetailFragment @Override public void onResume() { super.onResume(); + if (DEBUG) { + Log.d(TAG, "onResume() called"); + } activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED)); @@ -1310,22 +1309,14 @@ public final class VideoDetailFragment if (!isPlayerAvailable()) { return; } - - final Optional root = player.UIs().get(VideoPlayerUi.class) - .map(VideoPlayerUi::getBinding) - .map(ViewBinding::getRoot); - - // Check if viewHolder already contains a child TODO TODO whaat - /*if (playerService != null - && root.map(View::getParent).orElse(null) != binding.playerPlaceholder) { - playerService.removeViewFromParent(); - }*/ setHeightThumbnail(); // Prevent from re-adding a view multiple times - if (root.isPresent() && root.get().getParent() == null) { - binding.playerPlaceholder.addView(root.get()); - } + new Handler().post(() -> player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> { + playerUi.removeViewFromParent(); + binding.playerPlaceholder.addView(playerUi.getBinding().getRoot()); + playerUi.setupVideoSurfaceIfNeeded(); + })); } private void removeVideoPlayerView() { @@ -1793,9 +1784,6 @@ public final class VideoDetailFragment @Override public void onViewCreated() { - // Video view can have elements visible from popup, - // We hide it here but once it ready the view will be shown in handleIntent() - getRoot().ifPresent(view -> view.setVisibility(View.GONE)); addVideoPlayerView(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 284ab74d8..78e93970c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -485,6 +485,10 @@ public final class Player implements PlaybackListener, Listener { // make sure UIs know whether a service is connected or not UIs.call(PlayerUi::onFragmentListenerSet); } + if (!exoPlayerIsNull()) { + UIs.call(PlayerUi::initPlayer); + UIs.call(PlayerUi::initPlayback); + } } private void initPlayback(@NonNull final PlayQueue queue, @@ -599,7 +603,7 @@ public final class Player implements PlaybackListener, Listener { progressUpdateDisposable.set(null); PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading - UIs.call(PlayerUi::destroy); + UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object } public void setRecovery() { @@ -737,7 +741,7 @@ public final class Player implements PlaybackListener, Listener { case Intent.ACTION_CONFIGURATION_CHANGED: assureCorrectAppLanguage(service); if (DEBUG) { - Log.d(TAG, "onConfigurationChanged() called"); + Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received"); } break; case Intent.ACTION_HEADSET_PLUG: //FIXME diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt index 17205fb9a..81e216006 100644 --- a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt +++ b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt @@ -1,12 +1,12 @@ package org.schabi.newpipe.player.gesture -import android.app.Activity import android.content.Context import android.util.Log import android.view.MotionEvent import android.view.View import android.view.View.OnTouchListener import android.widget.ProgressBar +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources import org.schabi.newpipe.MainActivity import org.schabi.newpipe.R @@ -29,8 +29,6 @@ import kotlin.math.min class MainPlayerGestureListener( private val playerUi: MainPlayerUi ) : BasePlayerGestureListener(playerUi), OnTouchListener { - private val maxVolume: Int = player.audioReactor.maxVolume - private var isMoving = false override fun onTouch(v: View, event: MotionEvent): Boolean { @@ -41,11 +39,11 @@ class MainPlayerGestureListener( } return when (event.action) { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { - v.parent.requestDisallowInterceptTouchEvent(playerUi.isFullscreen) + v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen) true } MotionEvent.ACTION_UP -> { - v.parent.requestDisallowInterceptTouchEvent(false) + v.parent?.requestDisallowInterceptTouchEvent(false) false } else -> true @@ -68,14 +66,15 @@ class MainPlayerGestureListener( private fun onScrollVolume(distanceY: Float) { // If we just started sliding, change the progress bar to match the system volume if (binding.volumeRelativeLayout.visibility != View.VISIBLE) { - val volumePercent: Float = player.audioReactor.volume / maxVolume.toFloat() + val volumePercent: Float = + player.audioReactor.volume / player.audioReactor.maxVolume.toFloat() binding.volumeProgressBar.progress = (volumePercent * MAX_GESTURE_LENGTH).toInt() } binding.volumeProgressBar.incrementProgressBy(distanceY.toInt()) val currentProgressPercent: Float = binding.volumeProgressBar.progress.toFloat() / MAX_GESTURE_LENGTH - val currentVolume = (maxVolume * currentProgressPercent).toInt() + val currentVolume = (player.audioReactor.maxVolume * currentProgressPercent).toInt() player.audioReactor.volume = currentVolume if (DEBUG) { Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume") @@ -102,7 +101,7 @@ class MainPlayerGestureListener( } private fun onScrollBrightness(distanceY: Float) { - val parent: Activity = playerUi.parentActivity + val parent: AppCompatActivity = playerUi.parentActivity.orElse(null) ?: return val window = parent.window val layoutParams = window.attributes val bar: ProgressBar = binding.brightnessProgressBar diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java index 10ed424ba..7c60671dd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java @@ -13,12 +13,13 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAct import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; +import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.Color; -import android.os.Build; import android.os.Handler; import android.provider.Settings; import android.util.DisplayMetrics; @@ -28,7 +29,6 @@ import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; -import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -37,6 +37,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; +import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; @@ -68,8 +69,9 @@ import org.schabi.newpipe.util.external_communication.KoreUtils; import java.util.List; import java.util.Objects; +import java.util.Optional; -public final class MainPlayerUi extends VideoPlayerUi { +public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener { private static final String TAG = MainPlayerUi.class.getSimpleName(); private boolean isFullscreen = false; @@ -113,7 +115,6 @@ public final class MainPlayerUi extends VideoPlayerUi { super.setupAfterIntent(); - binding.getRoot().setVisibility(View.VISIBLE); initVideoPlayer(); // Android TV: without it focus will frame the whole player binding.playPauseButton.requestFocus(); @@ -139,7 +140,8 @@ public final class MainPlayerUi extends VideoPlayerUi { binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked()); binding.addToPlaylistButton.setOnClickListener(v -> - player.onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager())); + getParentActivity().map(FragmentActivity::getSupportFragmentManager) + .ifPresent(player::onAddToPlaylistClicked)); settingsContentObserver = new ContentObserver(new Handler()) { @Override @@ -151,7 +153,20 @@ public final class MainPlayerUi extends VideoPlayerUi { Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, settingsContentObserver); - binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange); + binding.getRoot().addOnLayoutChangeListener(this); + } + + @Override + protected void deinitListeners() { + super.deinitListeners(); + + binding.queueButton.setOnClickListener(null); + binding.segmentsButton.setOnClickListener(null); + binding.addToPlaylistButton.setOnClickListener(null); + + context.getContentResolver().unregisterContentObserver(settingsContentObserver); + + binding.getRoot().removeOnLayoutChangeListener(this); } @Override @@ -178,7 +193,6 @@ public final class MainPlayerUi extends VideoPlayerUi { @Override public void destroy() { super.destroy(); - context.getContentResolver().unregisterContentObserver(settingsContentObserver); // Exit from fullscreen when user closes the player via notification if (isFullscreen) { @@ -324,9 +338,10 @@ public final class MainPlayerUi extends VideoPlayerUi { player.useVideoSource(false); break; case MINIMIZE_ON_EXIT_MODE_POPUP: - player.setRecovery(); - NavigationHelper.playOnPopupPlayer(getParentActivity(), - player.getPlayQueue(), true); + getParentActivity().ifPresent(activity -> { + player.setRecovery(); + NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(), true); + }); break; case MINIMIZE_ON_EXIT_MODE_NONE: default: player.pause(); @@ -385,14 +400,15 @@ public final class MainPlayerUi extends VideoPlayerUi { @Override public void showSystemUIPartially() { if (isFullscreen) { - final Window window = getParentActivity().getWindow(); - window.setStatusBarColor(Color.TRANSPARENT); - window.setNavigationBarColor(Color.TRANSPARENT); - final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - window.getDecorView().setSystemUiVisibility(visibility); - window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + getParentActivity().map(Activity::getWindow).ifPresent(window -> { + window.setStatusBarColor(Color.TRANSPARENT); + window.setNavigationBarColor(Color.TRANSPARENT); + final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + window.getDecorView().setSystemUiVisibility(visibility); + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + }); } } @@ -476,8 +492,9 @@ public final class MainPlayerUi extends VideoPlayerUi { //region Gestures @SuppressWarnings("checkstyle:ParameterNumber") - private void onLayoutChange(final View view, final int l, final int t, final int r, final int b, - final int ol, final int ot, final int or, final int ob) { + @Override + public void onLayoutChange(final View view, final int l, final int t, final int r, final int b, + final int ol, final int ot, final int or, final int ob) { if (l != ol || t != ot || r != or || b != ob) { // Use smaller value to be consistent between screen orientations // (and to make usage easier) @@ -501,9 +518,8 @@ public final class MainPlayerUi extends VideoPlayerUi { private void setInitialGestureValues() { if (player.getAudioReactor() != null) { - final float currentVolumeNormalized = - (float) player.getAudioReactor().getVolume() - / player.getAudioReactor().getMaxVolume(); + final float currentVolumeNormalized = (float) player.getAudioReactor().getVolume() + / player.getAudioReactor().getMaxVolume(); binding.volumeProgressBar.setProgress( (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized)); } @@ -714,7 +730,7 @@ public final class MainPlayerUi extends VideoPlayerUi { @Override public void held(final PlayQueueItem item, final View view) { @Nullable final PlayQueue playQueue = player.getPlayQueue(); - @Nullable final AppCompatActivity parentActivity = getParentActivity(); + @Nullable final AppCompatActivity parentActivity = getParentActivity().orElse(null); if (playQueue != null && parentActivity != null && playQueue.indexOf(item) != -1) { openPopupMenu(player.getPlayQueue(), item, view, true, parentActivity.getSupportFragmentManager(), context); @@ -801,10 +817,15 @@ public final class MainPlayerUi extends VideoPlayerUi { @Override protected void onPlaybackSpeedClicked() { + final AppCompatActivity activity = getParentActivity().orElse(null); + if (activity == null) { + return; + } + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), player.getPlaybackSkipSilence(), (speed, pitch, skipSilence) -> player.setPlaybackParameters(speed, pitch, skipSilence)) - .show(getParentActivity().getSupportFragmentManager(), null); + .show(activity.getSupportFragmentManager(), null); } @Override @@ -876,15 +897,15 @@ public final class MainPlayerUi extends VideoPlayerUi { } isFullscreen = !isFullscreen; - if (!isFullscreen) { - // Apply window insets because Android will not do it when orientation changes - // from landscape to portrait (open vertical video to reproduce) - binding.playbackControlRoot.setPadding(0, 0, 0, 0); - } else { + if (isFullscreen) { // Android needs tens milliseconds to send new insets but a user is able to see // how controls changes it's position from `0` to `nav bar height` padding. // So just hide the controls to hide this visual inconsistency hideControls(0, 0); + } else { + // Apply window insets because Android will not do it when orientation changes + // from landscape to portrait (open vertical video to reproduce) + binding.playbackControlRoot.setPadding(0, 0, 0, 0); } fragmentListener.onFullscreenStateChanged(isFullscreen); @@ -924,14 +945,22 @@ public final class MainPlayerUi extends VideoPlayerUi { return binding; } - public AppCompatActivity getParentActivity() { - return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext(); + public Optional getParentActivity() { + final ViewParent rootParent = binding.getRoot().getParent(); + if (rootParent instanceof ViewGroup) { + final Context activity = ((ViewGroup) rootParent).getContext(); + if (activity instanceof AppCompatActivity) { + return Optional.of((AppCompatActivity) activity); + } + } + return Optional.empty(); } public boolean isLandscape() { // DisplayMetrics from activity context knows about MultiWindow feature // while DisplayMetrics from app context doesn't - return DeviceUtils.isLandscape(getParentActivity()); + return DeviceUtils.isLandscape( + getParentActivity().map(Context.class::cast).orElse(player.getService())); } //endregion } diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java index fd63790d6..15b468fb7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java @@ -19,7 +19,6 @@ import org.schabi.newpipe.player.Player; import java.util.List; public abstract class PlayerUi { - private static final String TAG = PlayerUi.class.getSimpleName(); @NonNull protected Context context; @NonNull protected Player player; diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java index b8a26a233..7df9102b7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java @@ -69,11 +69,9 @@ public final class PopupPlayerUi extends VideoPlayerUi { @Override public void setupAfterIntent() { - setupElementsVisibility(); - binding.getRoot().setVisibility(View.VISIBLE); + super.setupAfterIntent(); initPopup(); initPopupCloseOverlay(); - binding.playPauseButton.requestFocus(); } @Override @@ -103,6 +101,7 @@ public final class PopupPlayerUi extends VideoPlayerUi { binding.loadingPanel.setMinimumHeight(popupLayoutParams.height); windowManager.addView(binding.getRoot(), popupLayoutParams); + setupVideoSurfaceIfNeeded(); // now there is a parent, we can setup video surface // Popup doesn't have aspectRatio selector, using FIT automatically setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); @@ -304,25 +303,23 @@ public final class PopupPlayerUi extends VideoPlayerUi { } public void removePopupFromView() { - if (windowManager != null) { - // wrap in try-catch since it could sometimes generate errors randomly - try { - if (popupHasParent()) { - windowManager.removeView(binding.getRoot()); - } - } catch (final IllegalArgumentException e) { - Log.w(TAG, "Failed to remove popup from window manager", e); + // wrap in try-catch since it could sometimes generate errors randomly + try { + if (popupHasParent()) { + windowManager.removeView(binding.getRoot()); } + } catch (final IllegalArgumentException e) { + Log.w(TAG, "Failed to remove popup from window manager", e); + } - try { - final boolean closeOverlayHasParent = closeOverlayBinding != null - && closeOverlayBinding.getRoot().getParent() != null; - if (closeOverlayHasParent) { - windowManager.removeView(closeOverlayBinding.getRoot()); - } - } catch (final IllegalArgumentException e) { - Log.w(TAG, "Failed to remove popup overlay from window manager", e); + try { + final boolean closeOverlayHasParent = closeOverlayBinding != null + && closeOverlayBinding.getRoot().getParent() != null; + if (closeOverlayHasParent) { + windowManager.removeView(closeOverlayBinding.getRoot()); } + } catch (final IllegalArgumentException e) { + Log.w(TAG, "Failed to remove popup overlay from window manager", e); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java index 99ecb5540..24cdb8908 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java @@ -32,7 +32,6 @@ import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; -import android.view.Surface; import android.view.View; import android.widget.LinearLayout; import android.widget.RelativeLayout; @@ -107,6 +106,7 @@ public abstract class VideoPlayerUi extends PlayerUi protected PlayerBinding binding; private final Handler controlsVisibilityHandler = new Handler(); @Nullable private SurfaceHolderCallback surfaceHolderCallback; + boolean surfaceIsSetup = false; @Nullable private Bitmap thumbnail = null; @@ -130,6 +130,7 @@ public abstract class VideoPlayerUi extends PlayerUi private GestureDetector gestureDetector; private BasePlayerGestureListener playerGestureListener; + @Nullable private View.OnLayoutChangeListener onLayoutChangeListener = null; @NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder = new SeekbarPreviewThumbnailHolder(); @@ -138,6 +139,7 @@ public abstract class VideoPlayerUi extends PlayerUi @NonNull final PlayerBinding playerBinding) { super(player); binding = playerBinding; + setupFromView(); } @@ -222,8 +224,8 @@ public abstract class VideoPlayerUi extends PlayerUi // PlaybackControlRoot already consumed window insets but we should pass them to // player_overlays and fast_seek_overlay too. Without it they will be off-centered. - binding.playbackControlRoot.addOnLayoutChangeListener( - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + onLayoutChangeListener + = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { binding.playerOverlays.setPadding( v.getPaddingLeft(), v.getPaddingTop(), @@ -240,7 +242,43 @@ public abstract class VideoPlayerUi extends PlayerUi fastSeekParams.topMargin = -v.getPaddingBottom(); fastSeekParams.rightMargin = -v.getPaddingLeft(); fastSeekParams.bottomMargin = -v.getPaddingTop(); - }); + }; + binding.playbackControlRoot.addOnLayoutChangeListener(onLayoutChangeListener); + } + + protected void deinitListeners() { + binding.qualityTextView.setOnClickListener(null); + binding.playbackSpeed.setOnClickListener(null); + binding.playbackSeekBar.setOnSeekBarChangeListener(null); + binding.captionTextView.setOnClickListener(null); + binding.resizeTextView.setOnClickListener(null); + binding.playbackLiveSync.setOnClickListener(null); + + binding.getRoot().setOnTouchListener(null); + playerGestureListener = null; + gestureDetector = null; + + binding.repeatButton.setOnClickListener(null); + binding.shuffleButton.setOnClickListener(null); + + binding.playPauseButton.setOnClickListener(null); + binding.playPreviousButton.setOnClickListener(null); + binding.playNextButton.setOnClickListener(null); + + binding.moreOptionsButton.setOnClickListener(null); + binding.moreOptionsButton.setOnLongClickListener(null); + binding.share.setOnClickListener(null); + binding.share.setOnLongClickListener(null); + binding.fullScreenButton.setOnClickListener(null); + binding.screenRotationButton.setOnClickListener(null); + binding.playWithKodi.setOnClickListener(null); + binding.openInBrowser.setOnClickListener(null); + binding.playerCloseButton.setOnClickListener(null); + binding.switchMute.setOnClickListener(null); + + ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, null); + + binding.playbackControlRoot.removeOnLayoutChangeListener(onLayoutChangeListener); } /** @@ -304,18 +342,25 @@ public abstract class VideoPlayerUi extends PlayerUi playerGestureListener.doubleTapControls(binding.fastSeekOverlay); } + public void deinitPlayerSeekOverlay() { + binding.fastSeekOverlay + .seekSecondsSupplier(null) + .performListener(null); + } + @Override public void setupAfterIntent() { super.setupAfterIntent(); setupElementsVisibility(); setupElementsSize(context.getResources()); + binding.getRoot().setVisibility(View.VISIBLE); + binding.playPauseButton.requestFocus(); } @Override public void initPlayer() { super.initPlayer(); - setupVideoSurface(); - setupFromView(); + setupVideoSurfaceIfNeeded(); } @Override @@ -331,7 +376,7 @@ public abstract class VideoPlayerUi extends PlayerUi @Override public void destroyPlayer() { super.destroyPlayer(); - cleanupVideoSurface(); + clearVideoSurface(); } @Override @@ -340,6 +385,8 @@ public abstract class VideoPlayerUi extends PlayerUi if (binding != null) { binding.endScreen.setImageBitmap(null); } + deinitPlayerSeekOverlay(); + deinitListeners(); } protected void setupElementsVisibility() { @@ -1470,40 +1517,50 @@ public abstract class VideoPlayerUi extends PlayerUi // SurfaceHolderCallback helpers //////////////////////////////////////////////////////////////////////////*/ //region SurfaceHolderCallback helpers - private void setupVideoSurface() { - // make sure there is nothing left over from previous calls - cleanupVideoSurface(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23 - surfaceHolderCallback = new SurfaceHolderCallback(context, player.getExoPlayer()); - binding.surfaceView.getHolder().addCallback(surfaceHolderCallback); - final Surface surface = binding.surfaceView.getHolder().getSurface(); + /** + * Connects the video surface to the exo player. This can be called anytime without the risk for + * issues to occur, since the player will run just fine when no surface is connected. Therefore + * the video surface will be setup only when all of these conditions are true: it is not already + * setup (this just prevents wasting resources to setup the surface again), there is an exo + * player, the root view is attached to a parent and the surface view is valid/unreleased (the + * latter two conditions prevent "The surface has been released" errors). So this function can + * be called many times and even while the UI is in unready states. + */ + public void setupVideoSurfaceIfNeeded() { + if (!surfaceIsSetup && player.getExoPlayer() != null + && binding.getRoot().getParent() != null) { + // make sure there is nothing left over from previous calls + clearVideoSurface(); - // ensure player is using an unreleased surface, which the surfaceView might not be - // when starting playback on background or during player switching - if (surface.isValid()) { - // initially set the surface manually otherwise - // onRenderedFirstFrame() will not be called - player.getExoPlayer().setVideoSurface(surface); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23 + surfaceHolderCallback = new SurfaceHolderCallback(context, player.getExoPlayer()); + binding.surfaceView.getHolder().addCallback(surfaceHolderCallback); + + // ensure player is using an unreleased surface, which the surfaceView might not be + // when starting playback on background or during player switching + if (binding.surfaceView.getHolder().getSurface().isValid()) { + // initially set the surface manually otherwise + // onRenderedFirstFrame() will not be called + player.getExoPlayer().setVideoSurfaceHolder(binding.surfaceView.getHolder()); + } + } else { + player.getExoPlayer().setVideoSurfaceView(binding.surfaceView); } - } else { - player.getExoPlayer().setVideoSurfaceView(binding.surfaceView); + surfaceIsSetup = true; } } - private void cleanupVideoSurface() { - final Optional exoPlayer = Optional.ofNullable(player.getExoPlayer()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23 - if (surfaceHolderCallback != null) { - binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); - surfaceHolderCallback.release(); - surfaceHolderCallback = null; - } - exoPlayer.ifPresent(simpleExoPlayer -> simpleExoPlayer.setVideoSurface(null)); - } else { - exoPlayer.ifPresent(simpleExoPlayer -> simpleExoPlayer.setVideoSurfaceView(null)); + private void clearVideoSurface() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M // >=API23 + && surfaceHolderCallback != null) { + binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); + surfaceHolderCallback.release(); + surfaceHolderCallback = null; } + Optional.ofNullable(player.getExoPlayer()).ifPresent(ExoPlayer::clearVideoSurface); + surfaceIsSetup = false; } //endregion diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt index cbba0a75b..d0782e1a1 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt @@ -38,14 +38,14 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : private var performListener: PerformListener? = null - fun performListener(listener: PerformListener) = apply { + fun performListener(listener: PerformListener?) = apply { performListener = listener } private var seekSecondsSupplier: () -> Int = { 0 } - fun seekSecondsSupplier(supplier: () -> Int) = apply { - seekSecondsSupplier = supplier + fun seekSecondsSupplier(supplier: (() -> Int)?) = apply { + seekSecondsSupplier = supplier ?: { 0 } } // Indicates whether this (double) tap is the first of a series