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 b0fed3d7d..f8ea7bc90 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -447,7 +447,7 @@ public final class Player implements PlaybackListener, Listener { private void initUIsForCurrentPlayerType() { //noinspection SimplifyOptionalCallChains if (!UIs.get(NotificationPlayerUi.class).isPresent()) { - UIs.add(new NotificationPlayerUi(this)); + UIs.addAndPrepare(new NotificationPlayerUi(this)); } if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN) @@ -469,24 +469,15 @@ public final class Player implements PlaybackListener, Listener { switch (playerType) { case MAIN: UIs.destroyAll(PopupPlayerUi.class); - UIs.add(new MainPlayerUi(this, binding)); + UIs.addAndPrepare(new MainPlayerUi(this, binding)); + break; + case POPUP: + UIs.destroyAll(MainPlayerUi.class); + UIs.addAndPrepare(new PopupPlayerUi(this, binding)); break; case AUDIO: UIs.destroyAll(VideoPlayerUi.class); break; - case POPUP: - UIs.destroyAll(MainPlayerUi.class); - UIs.add(new PopupPlayerUi(this, binding)); - break; - } - - if (fragmentListener != null) { - // 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); } } @@ -1968,9 +1959,9 @@ public final class Player implements PlaybackListener, Listener { /*////////////////////////////////////////////////////////////////////////// - // Video size, resize, orientation, fullscreen + // Video size //////////////////////////////////////////////////////////////////////////*/ - //region Video size, resize, orientation, fullscreen + //region Video size @Override // exoplayer listener public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { if (DEBUG) { 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 81e93ca23..c4db1f334 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 @@ -18,50 +18,105 @@ import org.schabi.newpipe.player.Player; import java.util.List; +/** + * A player UI is a component that can seamlessly connect and disconnect from the {@link Player} and + * provide a user interface of some sort. Try to extend this class instead of adding more code to + * {@link Player}! + */ public abstract class PlayerUi { - @NonNull protected Context context; - @NonNull protected Player player; + @NonNull protected final Context context; + @NonNull protected final Player player; + /** + * @param player the player instance that will be usable throughout the lifetime of this UI + */ public PlayerUi(@NonNull final Player player) { this.context = player.getContext(); this.player = player; } + /** + * @return the player instance this UI was constructed with + */ @NonNull public Player getPlayer() { return player; } + /** + * Called after the player received an intent and processed it + */ public void setupAfterIntent() { } + /** + * Called right after the exoplayer instance is constructed, or right after this UI is + * constructed if the exoplayer is already available then. Note that the exoplayer instance + * could be built and destroyed multiple times during the lifetime of the player, so this method + * might be called multiple times. + */ public void initPlayer() { } + /** + * Called when playback in the exoplayer is about to start, or right after this UI is + * constructed if the exoplayer and the play queue are already available then. The play queue + * will therefore always be not null. + */ public void initPlayback() { } + /** + * Called when the exoplayer instance is about to be destroyed. Note that the exoplayer instance + * could be built and destroyed multiple times during the lifetime of the player, so this method + * might be called multiple times. Be sure to unset any video surface view or play queue + * listeners! This will also be called when this UI is being discarded, just before {@link + * #destroy()}. + */ public void destroyPlayer() { } + /** + * Called when this UI is being discarded, either because the player is switching to a different + * UI or because the player is shutting down completely + */ public void destroy() { } + /** + * Called when the player is smooth-stopping, that is, transitioning smoothly to a new play + * queue after the user tapped on a new video stream while a stream was playing in the video + * detail fragment + */ public void smoothStopForImmediateReusing() { } + /** + * Called when the video detail fragment listener is connected with the player, or right after + * this UI is constructed if the listener is already connected then + */ public void onFragmentListenerSet() { } /** - * If you want to register new broadcast actions to receive here, add them to - * {@link Player#setupBroadcastReceiver()}. + * Broadcasts that the player receives will also be notified to UIs here. If you want to + * register new broadcast actions to receive here, add them to {@link + * Player#setupBroadcastReceiver()}. */ public void onBroadcastReceived(final Intent intent) { } + /** + * Called when stream progress (i.e. the current time in the seekbar) or stream duration change. + * Will surely be called every {@link Player#PROGRESS_LOOP_INTERVAL_MILLIS} while a stream is + * playing. + * @param currentProgress the current progress in milliseconds + * @param duration the duration of the stream being played + * @param bufferPercent the percentage of stream already buffered, see {@link + * com.google.android.exoplayer2.BasePlayer#getBufferedPercentage()} + */ public void onUpdateProgress(final int currentProgress, final int duration, final int bufferPercent) { @@ -97,27 +152,56 @@ public abstract class PlayerUi { public void onMuteUnmuteChanged(final boolean isMuted) { } + /** + * @see com.google.android.exoplayer2.Player.Listener#onTracksChanged(Tracks) + */ public void onTextTracksChanged(@NonNull final Tracks currentTracks) { } + /** + * @see com.google.android.exoplayer2.Player.Listener#onPlaybackParametersChanged + */ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) { } + /** + * @see com.google.android.exoplayer2.Player.Listener#onRenderedFirstFrame + */ public void onRenderedFirstFrame() { } + /** + * @see com.google.android.exoplayer2.text.TextOutput#onCues + */ public void onCues(@NonNull final List cues) { } + /** + * Called when the stream being played changes + * @param info the {@link StreamInfo} metadata object, along with data about the selected and + * available video streams (to be used to build the resolution menus, for example) + */ public void onMetadataChanged(@NonNull final StreamInfo info) { } + /** + * Called when the thumbnail for the current metadata was loaded + * @param bitmap the thumbnail to process, or null if there is no thumbnail or there was an + * error when loading the thumbnail + */ public void onThumbnailLoaded(@Nullable final Bitmap bitmap) { } + /** + * Called when the play queue was edited: a stream was appended, moved or removed. + */ public void onPlayQueueEdited() { } + /** + * @param videoSize the new video size, useful to set the surface aspect ratio + * @see com.google.android.exoplayer2.Player.Listener#onVideoSizeChanged + */ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { } } diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java index 8c5c0dbfa..749cda02c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java @@ -8,10 +8,39 @@ import java.util.function.Consumer; public final class PlayerUiList { final List playerUis = new ArrayList<>(); - public void add(final PlayerUi playerUi) { + /** + * Adds the provided player ui to the list and calls on it the initialization functions that + * apply based on the current player state. The preparation step needs to be done since when UIs + * are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer + * is already initialized, but we need to notify the newly built UI that the player is ready + * nonetheless. + * @param playerUi the player ui to prepare and add to the list; its {@link + * PlayerUi#getPlayer()} will be used to query information about the player + * state + */ + public void addAndPrepare(final PlayerUi playerUi) { + if (playerUi.getPlayer().getFragmentListener().isPresent()) { + // make sure UIs know whether a service is connected or not + playerUi.onFragmentListenerSet(); + } + + if (!playerUi.getPlayer().exoPlayerIsNull()) { + playerUi.initPlayer(); + if (playerUi.getPlayer().getPlayQueue() != null) { + playerUi.initPlayback(); + } + } + playerUis.add(playerUi); } + /** + * Destroys all matching player UIs and removes them from the list + * @param playerUiType the class of the player UI to destroy; the {@link + * Class#isInstance(Object)} method will be used, so even subclasses will be + * destroyed and removed + * @param the class type parameter + */ public void destroyAll(final Class playerUiType) { playerUis.stream() .filter(playerUiType::isInstance) @@ -22,6 +51,14 @@ public final class PlayerUiList { playerUis.removeIf(playerUiType::isInstance); } + /** + * @param playerUiType the class of the player UI to return; the {@link + * Class#isInstance(Object)} method will be used, so even subclasses could + * be returned + * @param the class type parameter + * @return the first player UI of the required type found in the list, or an empty {@link + * Optional} otherwise + */ public Optional get(final Class playerUiType) { return playerUis.stream() .filter(playerUiType::isInstance) @@ -29,6 +66,10 @@ public final class PlayerUiList { .findFirst(); } + /** + * Calls the provided consumer on all player UIs in the list + * @param consumer the consumer to call with player UIs + */ public void call(final Consumer consumer) { //noinspection SimplifyStreamApiCallChains playerUis.stream().forEach(consumer);