1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-06-25 14:43:20 +00:00

Fix some crashes / issues after player refactor

This commit is contained in:
Stypox 2022-04-09 10:48:34 +02:00
parent 76ced59b62
commit b3f99645a3
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
8 changed files with 192 additions and 119 deletions

View File

@ -240,10 +240,6 @@ public final class VideoDetailFragment
playerUi.ifPresent(MainPlayerUi::toggleFullscreen); playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
} }
if (playerIsNotStopped() && player.videoPlayerSelected()) {
addVideoPlayerView();
}
//noinspection SimplifyOptionalCallChains //noinspection SimplifyOptionalCallChains
if (playAfterConnect if (playAfterConnect
|| (currentInfo != null || (currentInfo != null
@ -335,6 +331,9 @@ public final class VideoDetailFragment
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
if (DEBUG) {
Log.d(TAG, "onResume() called");
}
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED)); activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
@ -1310,22 +1309,14 @@ public final class VideoDetailFragment
if (!isPlayerAvailable()) { if (!isPlayerAvailable()) {
return; return;
} }
final Optional<View> 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(); setHeightThumbnail();
// Prevent from re-adding a view multiple times // Prevent from re-adding a view multiple times
if (root.isPresent() && root.get().getParent() == null) { new Handler().post(() -> player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
binding.playerPlaceholder.addView(root.get()); playerUi.removeViewFromParent();
} binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
playerUi.setupVideoSurfaceIfNeeded();
}));
} }
private void removeVideoPlayerView() { private void removeVideoPlayerView() {
@ -1793,9 +1784,6 @@ public final class VideoDetailFragment
@Override @Override
public void onViewCreated() { 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(); addVideoPlayerView();
} }

View File

@ -485,6 +485,10 @@ public final class Player implements PlaybackListener, Listener {
// make sure UIs know whether a service is connected or not // make sure UIs know whether a service is connected or not
UIs.call(PlayerUi::onFragmentListenerSet); UIs.call(PlayerUi::onFragmentListenerSet);
} }
if (!exoPlayerIsNull()) {
UIs.call(PlayerUi::initPlayer);
UIs.call(PlayerUi::initPlayback);
}
} }
private void initPlayback(@NonNull final PlayQueue queue, private void initPlayback(@NonNull final PlayQueue queue,
@ -599,7 +603,7 @@ public final class Player implements PlaybackListener, Listener {
progressUpdateDisposable.set(null); progressUpdateDisposable.set(null);
PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading 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() { public void setRecovery() {
@ -737,7 +741,7 @@ public final class Player implements PlaybackListener, Listener {
case Intent.ACTION_CONFIGURATION_CHANGED: case Intent.ACTION_CONFIGURATION_CHANGED:
assureCorrectAppLanguage(service); assureCorrectAppLanguage(service);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onConfigurationChanged() called"); Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
} }
break; break;
case Intent.ACTION_HEADSET_PLUG: //FIXME case Intent.ACTION_HEADSET_PLUG: //FIXME

View File

@ -1,12 +1,12 @@
package org.schabi.newpipe.player.gesture package org.schabi.newpipe.player.gesture
import android.app.Activity
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.View.OnTouchListener import android.view.View.OnTouchListener
import android.widget.ProgressBar import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R import org.schabi.newpipe.R
@ -29,8 +29,6 @@ import kotlin.math.min
class MainPlayerGestureListener( class MainPlayerGestureListener(
private val playerUi: MainPlayerUi private val playerUi: MainPlayerUi
) : BasePlayerGestureListener(playerUi), OnTouchListener { ) : BasePlayerGestureListener(playerUi), OnTouchListener {
private val maxVolume: Int = player.audioReactor.maxVolume
private var isMoving = false private var isMoving = false
override fun onTouch(v: View, event: MotionEvent): Boolean { override fun onTouch(v: View, event: MotionEvent): Boolean {
@ -41,11 +39,11 @@ class MainPlayerGestureListener(
} }
return when (event.action) { return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent.requestDisallowInterceptTouchEvent(playerUi.isFullscreen) v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
true true
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
v.parent.requestDisallowInterceptTouchEvent(false) v.parent?.requestDisallowInterceptTouchEvent(false)
false false
} }
else -> true else -> true
@ -68,14 +66,15 @@ class MainPlayerGestureListener(
private fun onScrollVolume(distanceY: Float) { private fun onScrollVolume(distanceY: Float) {
// If we just started sliding, change the progress bar to match the system volume // If we just started sliding, change the progress bar to match the system volume
if (binding.volumeRelativeLayout.visibility != View.VISIBLE) { 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.progress = (volumePercent * MAX_GESTURE_LENGTH).toInt()
} }
binding.volumeProgressBar.incrementProgressBy(distanceY.toInt()) binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
val currentProgressPercent: Float = val currentProgressPercent: Float =
binding.volumeProgressBar.progress.toFloat() / MAX_GESTURE_LENGTH binding.volumeProgressBar.progress.toFloat() / MAX_GESTURE_LENGTH
val currentVolume = (maxVolume * currentProgressPercent).toInt() val currentVolume = (player.audioReactor.maxVolume * currentProgressPercent).toInt()
player.audioReactor.volume = currentVolume player.audioReactor.volume = currentVolume
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume") Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
@ -102,7 +101,7 @@ class MainPlayerGestureListener(
} }
private fun onScrollBrightness(distanceY: Float) { private fun onScrollBrightness(distanceY: Float) {
val parent: Activity = playerUi.parentActivity val parent: AppCompatActivity = playerUi.parentActivity.orElse(null) ?: return
val window = parent.window val window = parent.window
val layoutParams = window.attributes val layoutParams = window.attributes
val bar: ProgressBar = binding.brightnessProgressBar val bar: ProgressBar = binding.brightnessProgressBar

View File

@ -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.getTimeString;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import android.app.Activity;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Resources; import android.content.res.Resources;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.provider.Settings; import android.provider.Settings;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@ -28,7 +29,6 @@ import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewParent; import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@ -37,6 +37,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -68,8 +69,9 @@ import org.schabi.newpipe.util.external_communication.KoreUtils;
import java.util.List; import java.util.List;
import java.util.Objects; 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 static final String TAG = MainPlayerUi.class.getSimpleName();
private boolean isFullscreen = false; private boolean isFullscreen = false;
@ -113,7 +115,6 @@ public final class MainPlayerUi extends VideoPlayerUi {
super.setupAfterIntent(); super.setupAfterIntent();
binding.getRoot().setVisibility(View.VISIBLE);
initVideoPlayer(); initVideoPlayer();
// Android TV: without it focus will frame the whole player // Android TV: without it focus will frame the whole player
binding.playPauseButton.requestFocus(); binding.playPauseButton.requestFocus();
@ -139,7 +140,8 @@ public final class MainPlayerUi extends VideoPlayerUi {
binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked()); binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
binding.addToPlaylistButton.setOnClickListener(v -> binding.addToPlaylistButton.setOnClickListener(v ->
player.onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager())); getParentActivity().map(FragmentActivity::getSupportFragmentManager)
.ifPresent(player::onAddToPlaylistClicked));
settingsContentObserver = new ContentObserver(new Handler()) { settingsContentObserver = new ContentObserver(new Handler()) {
@Override @Override
@ -151,7 +153,20 @@ public final class MainPlayerUi extends VideoPlayerUi {
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
settingsContentObserver); 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 @Override
@ -178,7 +193,6 @@ public final class MainPlayerUi extends VideoPlayerUi {
@Override @Override
public void destroy() { public void destroy() {
super.destroy(); super.destroy();
context.getContentResolver().unregisterContentObserver(settingsContentObserver);
// Exit from fullscreen when user closes the player via notification // Exit from fullscreen when user closes the player via notification
if (isFullscreen) { if (isFullscreen) {
@ -324,9 +338,10 @@ public final class MainPlayerUi extends VideoPlayerUi {
player.useVideoSource(false); player.useVideoSource(false);
break; break;
case MINIMIZE_ON_EXIT_MODE_POPUP: case MINIMIZE_ON_EXIT_MODE_POPUP:
player.setRecovery(); getParentActivity().ifPresent(activity -> {
NavigationHelper.playOnPopupPlayer(getParentActivity(), player.setRecovery();
player.getPlayQueue(), true); NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(), true);
});
break; break;
case MINIMIZE_ON_EXIT_MODE_NONE: default: case MINIMIZE_ON_EXIT_MODE_NONE: default:
player.pause(); player.pause();
@ -385,14 +400,15 @@ public final class MainPlayerUi extends VideoPlayerUi {
@Override @Override
public void showSystemUIPartially() { public void showSystemUIPartially() {
if (isFullscreen) { if (isFullscreen) {
final Window window = getParentActivity().getWindow(); getParentActivity().map(Activity::getWindow).ifPresent(window -> {
window.setStatusBarColor(Color.TRANSPARENT); window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT); window.setNavigationBarColor(Color.TRANSPARENT);
final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
window.getDecorView().setSystemUiVisibility(visibility); window.getDecorView().setSystemUiVisibility(visibility);
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
});
} }
} }
@ -476,8 +492,9 @@ public final class MainPlayerUi extends VideoPlayerUi {
//region Gestures //region Gestures
@SuppressWarnings("checkstyle:ParameterNumber") @SuppressWarnings("checkstyle:ParameterNumber")
private void onLayoutChange(final View view, final int l, final int t, final int r, final int b, @Override
final int ol, final int ot, final int or, final int ob) { 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) { if (l != ol || t != ot || r != or || b != ob) {
// Use smaller value to be consistent between screen orientations // Use smaller value to be consistent between screen orientations
// (and to make usage easier) // (and to make usage easier)
@ -501,9 +518,8 @@ public final class MainPlayerUi extends VideoPlayerUi {
private void setInitialGestureValues() { private void setInitialGestureValues() {
if (player.getAudioReactor() != null) { if (player.getAudioReactor() != null) {
final float currentVolumeNormalized = final float currentVolumeNormalized = (float) player.getAudioReactor().getVolume()
(float) player.getAudioReactor().getVolume() / player.getAudioReactor().getMaxVolume();
/ player.getAudioReactor().getMaxVolume();
binding.volumeProgressBar.setProgress( binding.volumeProgressBar.setProgress(
(int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized)); (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized));
} }
@ -714,7 +730,7 @@ public final class MainPlayerUi extends VideoPlayerUi {
@Override @Override
public void held(final PlayQueueItem item, final View view) { public void held(final PlayQueueItem item, final View view) {
@Nullable final PlayQueue playQueue = player.getPlayQueue(); @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) { if (playQueue != null && parentActivity != null && playQueue.indexOf(item) != -1) {
openPopupMenu(player.getPlayQueue(), item, view, true, openPopupMenu(player.getPlayQueue(), item, view, true,
parentActivity.getSupportFragmentManager(), context); parentActivity.getSupportFragmentManager(), context);
@ -801,10 +817,15 @@ public final class MainPlayerUi extends VideoPlayerUi {
@Override @Override
protected void onPlaybackSpeedClicked() { protected void onPlaybackSpeedClicked() {
final AppCompatActivity activity = getParentActivity().orElse(null);
if (activity == null) {
return;
}
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
player.getPlaybackSkipSilence(), (speed, pitch, skipSilence) player.getPlaybackSkipSilence(), (speed, pitch, skipSilence)
-> player.setPlaybackParameters(speed, pitch, skipSilence)) -> player.setPlaybackParameters(speed, pitch, skipSilence))
.show(getParentActivity().getSupportFragmentManager(), null); .show(activity.getSupportFragmentManager(), null);
} }
@Override @Override
@ -876,15 +897,15 @@ public final class MainPlayerUi extends VideoPlayerUi {
} }
isFullscreen = !isFullscreen; isFullscreen = !isFullscreen;
if (!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 {
// Android needs tens milliseconds to send new insets but a user is able to see // 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. // how controls changes it's position from `0` to `nav bar height` padding.
// So just hide the controls to hide this visual inconsistency // So just hide the controls to hide this visual inconsistency
hideControls(0, 0); 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); fragmentListener.onFullscreenStateChanged(isFullscreen);
@ -924,14 +945,22 @@ public final class MainPlayerUi extends VideoPlayerUi {
return binding; return binding;
} }
public AppCompatActivity getParentActivity() { public Optional<AppCompatActivity> getParentActivity() {
return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext(); 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() { public boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature // DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't // while DisplayMetrics from app context doesn't
return DeviceUtils.isLandscape(getParentActivity()); return DeviceUtils.isLandscape(
getParentActivity().map(Context.class::cast).orElse(player.getService()));
} }
//endregion //endregion
} }

View File

@ -19,7 +19,6 @@ import org.schabi.newpipe.player.Player;
import java.util.List; import java.util.List;
public abstract class PlayerUi { public abstract class PlayerUi {
private static final String TAG = PlayerUi.class.getSimpleName();
@NonNull protected Context context; @NonNull protected Context context;
@NonNull protected Player player; @NonNull protected Player player;

View File

@ -69,11 +69,9 @@ public final class PopupPlayerUi extends VideoPlayerUi {
@Override @Override
public void setupAfterIntent() { public void setupAfterIntent() {
setupElementsVisibility(); super.setupAfterIntent();
binding.getRoot().setVisibility(View.VISIBLE);
initPopup(); initPopup();
initPopupCloseOverlay(); initPopupCloseOverlay();
binding.playPauseButton.requestFocus();
} }
@Override @Override
@ -103,6 +101,7 @@ public final class PopupPlayerUi extends VideoPlayerUi {
binding.loadingPanel.setMinimumHeight(popupLayoutParams.height); binding.loadingPanel.setMinimumHeight(popupLayoutParams.height);
windowManager.addView(binding.getRoot(), popupLayoutParams); 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 // Popup doesn't have aspectRatio selector, using FIT automatically
setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
@ -304,25 +303,23 @@ public final class PopupPlayerUi extends VideoPlayerUi {
} }
public void removePopupFromView() { public void removePopupFromView() {
if (windowManager != null) { // wrap in try-catch since it could sometimes generate errors randomly
// wrap in try-catch since it could sometimes generate errors randomly try {
try { if (popupHasParent()) {
if (popupHasParent()) { windowManager.removeView(binding.getRoot());
windowManager.removeView(binding.getRoot());
}
} catch (final IllegalArgumentException e) {
Log.w(TAG, "Failed to remove popup from window manager", e);
} }
} catch (final IllegalArgumentException e) {
Log.w(TAG, "Failed to remove popup from window manager", e);
}
try { try {
final boolean closeOverlayHasParent = closeOverlayBinding != null final boolean closeOverlayHasParent = closeOverlayBinding != null
&& closeOverlayBinding.getRoot().getParent() != null; && closeOverlayBinding.getRoot().getParent() != null;
if (closeOverlayHasParent) { if (closeOverlayHasParent) {
windowManager.removeView(closeOverlayBinding.getRoot()); windowManager.removeView(closeOverlayBinding.getRoot());
}
} catch (final IllegalArgumentException e) {
Log.w(TAG, "Failed to remove popup overlay from window manager", e);
} }
} catch (final IllegalArgumentException e) {
Log.w(TAG, "Failed to remove popup overlay from window manager", e);
} }
} }

View File

@ -32,7 +32,6 @@ import android.view.Gravity;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.Surface;
import android.view.View; import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
@ -107,6 +106,7 @@ public abstract class VideoPlayerUi extends PlayerUi
protected PlayerBinding binding; protected PlayerBinding binding;
private final Handler controlsVisibilityHandler = new Handler(); private final Handler controlsVisibilityHandler = new Handler();
@Nullable private SurfaceHolderCallback surfaceHolderCallback; @Nullable private SurfaceHolderCallback surfaceHolderCallback;
boolean surfaceIsSetup = false;
@Nullable private Bitmap thumbnail = null; @Nullable private Bitmap thumbnail = null;
@ -130,6 +130,7 @@ public abstract class VideoPlayerUi extends PlayerUi
private GestureDetector gestureDetector; private GestureDetector gestureDetector;
private BasePlayerGestureListener playerGestureListener; private BasePlayerGestureListener playerGestureListener;
@Nullable private View.OnLayoutChangeListener onLayoutChangeListener = null;
@NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder = @NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
new SeekbarPreviewThumbnailHolder(); new SeekbarPreviewThumbnailHolder();
@ -138,6 +139,7 @@ public abstract class VideoPlayerUi extends PlayerUi
@NonNull final PlayerBinding playerBinding) { @NonNull final PlayerBinding playerBinding) {
super(player); super(player);
binding = playerBinding; binding = playerBinding;
setupFromView();
} }
@ -222,8 +224,8 @@ public abstract class VideoPlayerUi extends PlayerUi
// PlaybackControlRoot already consumed window insets but we should pass them to // 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. // player_overlays and fast_seek_overlay too. Without it they will be off-centered.
binding.playbackControlRoot.addOnLayoutChangeListener( onLayoutChangeListener
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
binding.playerOverlays.setPadding( binding.playerOverlays.setPadding(
v.getPaddingLeft(), v.getPaddingLeft(),
v.getPaddingTop(), v.getPaddingTop(),
@ -240,7 +242,43 @@ public abstract class VideoPlayerUi extends PlayerUi
fastSeekParams.topMargin = -v.getPaddingBottom(); fastSeekParams.topMargin = -v.getPaddingBottom();
fastSeekParams.rightMargin = -v.getPaddingLeft(); fastSeekParams.rightMargin = -v.getPaddingLeft();
fastSeekParams.bottomMargin = -v.getPaddingTop(); 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); playerGestureListener.doubleTapControls(binding.fastSeekOverlay);
} }
public void deinitPlayerSeekOverlay() {
binding.fastSeekOverlay
.seekSecondsSupplier(null)
.performListener(null);
}
@Override @Override
public void setupAfterIntent() { public void setupAfterIntent() {
super.setupAfterIntent(); super.setupAfterIntent();
setupElementsVisibility(); setupElementsVisibility();
setupElementsSize(context.getResources()); setupElementsSize(context.getResources());
binding.getRoot().setVisibility(View.VISIBLE);
binding.playPauseButton.requestFocus();
} }
@Override @Override
public void initPlayer() { public void initPlayer() {
super.initPlayer(); super.initPlayer();
setupVideoSurface(); setupVideoSurfaceIfNeeded();
setupFromView();
} }
@Override @Override
@ -331,7 +376,7 @@ public abstract class VideoPlayerUi extends PlayerUi
@Override @Override
public void destroyPlayer() { public void destroyPlayer() {
super.destroyPlayer(); super.destroyPlayer();
cleanupVideoSurface(); clearVideoSurface();
} }
@Override @Override
@ -340,6 +385,8 @@ public abstract class VideoPlayerUi extends PlayerUi
if (binding != null) { if (binding != null) {
binding.endScreen.setImageBitmap(null); binding.endScreen.setImageBitmap(null);
} }
deinitPlayerSeekOverlay();
deinitListeners();
} }
protected void setupElementsVisibility() { protected void setupElementsVisibility() {
@ -1470,40 +1517,50 @@ public abstract class VideoPlayerUi extends PlayerUi
// SurfaceHolderCallback helpers // SurfaceHolderCallback helpers
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region 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()); * Connects the video surface to the exo player. This can be called anytime without the risk for
binding.surfaceView.getHolder().addCallback(surfaceHolderCallback); * issues to occur, since the player will run just fine when no surface is connected. Therefore
final Surface surface = binding.surfaceView.getHolder().getSurface(); * 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 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
// when starting playback on background or during player switching surfaceHolderCallback = new SurfaceHolderCallback(context, player.getExoPlayer());
if (surface.isValid()) { binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
// initially set the surface manually otherwise
// onRenderedFirstFrame() will not be called // ensure player is using an unreleased surface, which the surfaceView might not be
player.getExoPlayer().setVideoSurface(surface); // 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 { surfaceIsSetup = true;
player.getExoPlayer().setVideoSurfaceView(binding.surfaceView);
} }
} }
private void cleanupVideoSurface() { private void clearVideoSurface() {
final Optional<ExoPlayer> exoPlayer = Optional.ofNullable(player.getExoPlayer()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M // >=API23
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23 && surfaceHolderCallback != null) {
if (surfaceHolderCallback != null) { binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); surfaceHolderCallback.release();
surfaceHolderCallback.release(); surfaceHolderCallback = null;
surfaceHolderCallback = null;
}
exoPlayer.ifPresent(simpleExoPlayer -> simpleExoPlayer.setVideoSurface(null));
} else {
exoPlayer.ifPresent(simpleExoPlayer -> simpleExoPlayer.setVideoSurfaceView(null));
} }
Optional.ofNullable(player.getExoPlayer()).ifPresent(ExoPlayer::clearVideoSurface);
surfaceIsSetup = false;
} }
//endregion //endregion

View File

@ -38,14 +38,14 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
private var performListener: PerformListener? = null private var performListener: PerformListener? = null
fun performListener(listener: PerformListener) = apply { fun performListener(listener: PerformListener?) = apply {
performListener = listener performListener = listener
} }
private var seekSecondsSupplier: () -> Int = { 0 } private var seekSecondsSupplier: () -> Int = { 0 }
fun seekSecondsSupplier(supplier: () -> Int) = apply { fun seekSecondsSupplier(supplier: (() -> Int)?) = apply {
seekSecondsSupplier = supplier seekSecondsSupplier = supplier ?: { 0 }
} }
// Indicates whether this (double) tap is the first of a series // Indicates whether this (double) tap is the first of a series