diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java index 3fb7a7716..2e90a4fc9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java @@ -7,7 +7,7 @@ import java.io.Serializable; class StackItem implements Serializable { private final int serviceId; private String title; - private final String url; + private String url; private final PlayQueue playQueue; StackItem(int serviceId, String url, String title, PlayQueue playQueue) { @@ -21,6 +21,10 @@ class StackItem implements Serializable { this.title = title; } + public void setUrl(String url) { + this.url = url; + } + public int getServiceId() { return serviceId; } 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 3d42942ba..d35e27c40 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 @@ -126,7 +126,7 @@ public class VideoDetailFragment @State protected PlayQueue playQueue; @State - int bottomSheetState = BottomSheetBehavior.STATE_HIDDEN; + int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; private StreamInfo currentInfo; private Disposable currentWorker; @@ -398,7 +398,8 @@ public class VideoDetailFragment public void onDestroy() { super.onDestroy(); - unbind(); + if (!activity.isFinishing()) unbind(); + else stopService(); PreferenceManager.getDefaultSharedPreferences(activity) .unregisterOnSharedPreferenceChangeListener(this); @@ -850,26 +851,6 @@ public class VideoDetailFragment */ protected final LinkedList stack = new LinkedList<>(); - public void pushToStack(int serviceId, String videoUrl, String name, PlayQueue playQueue) { - if (DEBUG) { - Log.d(TAG, "pushToStack() called with: serviceId = [" - + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "], playQueue = [" + playQueue + "]"); - } - - if (stack.size() > 0 - && stack.peek().getServiceId() == serviceId - && stack.peek().getUrl().equals(videoUrl) - && stack.peek().getPlayQueue().getClass().equals(playQueue.getClass())) { - Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" - + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); - return; - } else { - Log.d(TAG, "pushToStack() wasn't equal"); - } - - stack.push(new StackItem(serviceId, videoUrl, name, playQueue)); - } - public void setTitleToUrl(int serviceId, String videoUrl, String name) { if (name != null && !name.isEmpty()) { for (StackItem stackItem : stack) { @@ -885,12 +866,17 @@ public class VideoDetailFragment public boolean onBackPressed() { if (DEBUG) Log.d(TAG, "onBackPressed() called"); + // If we are in fullscreen mode just exit from it via first back press if (player != null && player.isInFullscreen()) { player.onPause(); restoreDefaultOrientation(); return true; } + // If we have something in history of played items we replay it here + if (player != null && player.getPlayQueue().previous()) { + return true; + } // That means that we are on the start of the stack, // return false to let the MainActivity handle the onBack if (stack.size() <= 1) { @@ -928,15 +914,15 @@ public class VideoDetailFragment } public void selectAndLoadVideo(int serviceId, String videoUrl, String name, PlayQueue playQueue) { - boolean streamIsTheSame = videoUrl.equals(url) && currentInfo != null; - setInitialData(serviceId, videoUrl, name, playQueue); - + boolean streamIsTheSame = this.playQueue != null && this.playQueue.equals(playQueue); // Situation when user switches from players to main player. All needed data is here, we can start watching if (streamIsTheSame) { - handleResult(currentInfo); + //TODO not sure about usefulness of this line in the case when user switches from one player to another + // handleResult(currentInfo); openVideoPlayer(); return; } + setInitialData(serviceId, videoUrl, name, playQueue); startLoading(false); } @@ -944,7 +930,6 @@ public class VideoDetailFragment if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]"); - setInitialData(info.getServiceId(), info.getUrl(), info.getName(), new SinglePlayQueue(info)); showLoading(); initTabs(); @@ -1390,8 +1375,6 @@ public class VideoDetailFragment setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName(), playQueue == null ? new SinglePlayQueue(info) : playQueue); - pushToStack(serviceId, url, name, playQueue); - if(showRelatedStreams){ if(null == relatedStreamsLayout){ //phone pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(info)); @@ -1627,6 +1610,20 @@ public class VideoDetailFragment // Player event listener //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onQueueUpdate(PlayQueue queue) { + playQueue = queue; + // This should be the only place where we push data to stack. It will allow to have live instance of PlayQueue with actual + // information about deleted/added items inside Channel/Playlist queue and makes possible to have a history of played items + if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(queue)) + stack.push(new StackItem(serviceId, url, name, playQueue)); + + if (DEBUG) { + Log.d(TAG, "onQueueUpdate() called with: serviceId = [" + + serviceId + "], videoUrl = [" + url + "], name = [" + name + "], playQueue = [" + playQueue + "]"); + } + } + @Override public void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters) { setOverlayPlayPauseImage(); @@ -1647,11 +1644,6 @@ public class VideoDetailFragment public void onProgressUpdate(int currentProgress, int duration, int bufferPercent) { // Progress updates every second even if media is paused. It's useless until playing if (!player.getPlayer().isPlaying() || playQueue == null) return; - - // Update current progress in cached playQueue because playQueue in popup and background players - // are different instances - playQueue.setRecovery(playQueue.getIndex(), currentProgress); - showPlaybackProgress(currentProgress, duration); // We don't want to interrupt playback and don't want to see notification if player is stopped @@ -1672,6 +1664,14 @@ public class VideoDetailFragment @Override public void onMetadataUpdate(StreamInfo info) { + if (!stack.isEmpty()) { + // When PlayQueue can have multiple streams (PlaylistPlayQueue or ChannelPlayQueue) every new played stream gives + // new title and url. StackItem contains information about first played stream. Let's update it here + StackItem peek = stack.peek(); + peek.setTitle(info.getName()); + peek.setUrl(info.getUrl()); + } + if (currentInfo == info) return; currentInfo = info; @@ -1865,7 +1865,7 @@ public class VideoDetailFragment case BottomSheetBehavior.STATE_COLLAPSED: // Re-enable clicks setOverlayElementsClickable(true); - if (player != null && player.isInFullscreen() && player.isPlaying()) showSystemUi(); + if (player != null && player.isInFullscreen()) showSystemUi(); break; case BottomSheetBehavior.STATE_DRAGGING: if (player != null && player.isControlsVisible()) player.hideControls(0, 0); @@ -1873,7 +1873,6 @@ public class VideoDetailFragment case BottomSheetBehavior.STATE_SETTLING: break; } - Log.d(TAG, "onStateChanged: " + newState); } @Override public void onSlide(@NonNull View bottomSheet, float slideOffset) { setOverlayLook(appBarLayout, behavior, slideOffset); diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 2ae822d7f..70ab82fb8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -274,6 +274,16 @@ public abstract class BasePlayer implements return; } + boolean same = playQueue != null && playQueue.equals(queue); + + // Do not re-init the same PlayQueue. Save time + if (same && !playQueue.isDisposed()) { + // Player can have state = IDLE when playback is stopped or failed and we should retry() in this case + if (simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) + simpleExoPlayer.retry(); + return; + } + final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()); final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); @@ -284,14 +294,17 @@ public abstract class BasePlayer implements if (simpleExoPlayer != null && queue.size() == 1 && playQueue != null + && playQueue.size() == 1 && playQueue.getItem() != null && queue.getItem().getUrl().equals(playQueue.getItem().getUrl()) && queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET - ) { + && !same) { simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition()); return; - } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && isPlaybackResumeEnabled()) { + } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) + && isPlaybackResumeEnabled() + && !same) { final PlayQueueItem item = queue.getItem(); if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { stateLoader = recordManager.loadStreamState(item) @@ -321,7 +334,8 @@ public abstract class BasePlayer implements } } // Good to go... - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, + // In a case of equal PlayQueues we can re-init old one but only when it is disposed + initPlayback(same ? playQueue : queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, /*playOnInit=*/true); } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java index 6e5082494..4e3c070a5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -126,6 +126,7 @@ public final class MainPlayer extends Service { if (playerImpl.getPlayer() != null) { playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); + // We can't pause the player here because it will make transition from one stream to a new stream not smooth playerImpl.getPlayer().stop(false); playerImpl.setRecovery(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index c8d564557..1c449c77e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -557,6 +557,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity // Binding Service Listener //////////////////////////////////////////////////////////////////////////// + @Override + public void onQueueUpdate(PlayQueue queue) { + } + @Override public void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters) { onStateChanged(state); diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java index d5bb5c86d..21934fb70 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -281,6 +281,7 @@ public class VideoPlayerImpl extends VideoPlayer private void setupElementsVisibility() { if (popupPlayerSelected()) { fullscreenButton.setVisibility(View.VISIBLE); + getRootView().findViewById(R.id.spaceBeforeControls).setVisibility(View.GONE); getRootView().findViewById(R.id.metadataView).setVisibility(View.GONE); queueButton.setVisibility(View.GONE); moreOptionsButton.setVisibility(View.GONE); @@ -294,10 +295,11 @@ public class VideoPlayerImpl extends VideoPlayer openInBrowser.setVisibility(View.GONE); } else { fullscreenButton.setVisibility(View.GONE); + getRootView().findViewById(R.id.spaceBeforeControls).setVisibility(View.VISIBLE); getRootView().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); moreOptionsButton.setVisibility(View.VISIBLE); getTopControlsRoot().setOrientation(LinearLayout.VERTICAL); - primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.MATCH_PARENT; + primaryControls.getLayoutParams().width = secondaryControls.getLayoutParams().width; secondaryControls.setVisibility(View.GONE); moreOptionsButton.setImageDrawable(service.getResources().getDrawable( R.drawable.ic_expand_more_white_24dp)); @@ -500,7 +502,13 @@ public class VideoPlayerImpl extends VideoPlayer triggerProgressUpdate(); } - /*////////////////////////////////////////////////////////////////////////// + @Override + protected void initPlayback(@NonNull PlayQueue queue, int repeatMode, float playbackSpeed, float playbackPitch, boolean playbackSkipSilence, boolean playOnReady) { + super.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, playOnReady); + updateQueue(queue); + } + + /*////////////////////////////////////////////////////////////////////////// // Player Overrides //////////////////////////////////////////////////////////////////////////*/ @@ -1088,7 +1096,7 @@ public class VideoPlayerImpl extends VideoPlayer } private void showSystemUIPartially() { - if (isInFullscreen()) { + if (isInFullscreen() && getParentActivity() != null) { int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_FULLSCREEN; @@ -1106,7 +1114,7 @@ public class VideoPlayerImpl extends VideoPlayer * This method measures width and height of controls visible on screen. It ensures that controls will be side-by-side with * NavigationBar and notches but not under them. Tablets have only bottom NavigationBar * */ - void setControlsSize() { + private void setControlsSize() { Point size = new Point(); Display display = getRootView().getDisplay(); if (display == null) return; @@ -1479,6 +1487,15 @@ public class VideoPlayerImpl extends VideoPlayer } } + private void updateQueue(PlayQueue queue) { + if (fragmentListener != null) { + fragmentListener.onQueueUpdate(queue); + } + if (activityListener != null) { + activityListener.onQueueUpdate(queue); + } + } + private void updateMetadata() { if (fragmentListener != null && getCurrentMetadata() != null) { fragmentListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java index 3a7b29954..37ad9798f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java @@ -4,8 +4,10 @@ package org.schabi.newpipe.player.event; import com.google.android.exoplayer2.PlaybackParameters; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.playqueue.PlayQueue; public interface PlayerEventListener { + void onQueueUpdate(PlayQueue queue); void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters); void onProgressUpdate(int currentProgress, int duration, int bufferPercent); void onMetadataUpdate(StreamInfo info); diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index fcb1e2819..12454bde9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -46,17 +46,23 @@ public abstract class PlayQueue implements Serializable { private ArrayList backup; private ArrayList streams; + private ArrayList history; @NonNull private final AtomicInteger queueIndex; private transient BehaviorSubject eventBroadcast; private transient Flowable broadcastReceiver; private transient Subscription reportingReactor; + private transient boolean disposed; + PlayQueue(final int index, final List startWith) { streams = new ArrayList<>(); streams.addAll(startWith); + history = new ArrayList<>(); + history.add(streams.get(index)); queueIndex = new AtomicInteger(index); + disposed = false; } /*////////////////////////////////////////////////////////////////////////// @@ -88,6 +94,7 @@ public abstract class PlayQueue implements Serializable { eventBroadcast = null; broadcastReceiver = null; reportingReactor = null; + disposed = true; } /** @@ -195,6 +202,7 @@ public abstract class PlayQueue implements Serializable { int newIndex = index; if (index < 0) newIndex = 0; if (index >= streams.size()) newIndex = isComplete() ? index % streams.size() : streams.size() - 1; + if (oldIndex != newIndex) history.add(streams.get(newIndex)); queueIndex.set(newIndex); broadcast(new SelectEvent(oldIndex, newIndex)); @@ -267,6 +275,9 @@ public abstract class PlayQueue implements Serializable { if (skippable) { queueIndex.incrementAndGet(); + if (streams.size() > queueIndex.get()) { + history.add(streams.get(queueIndex.get())); + } } else { removeInternal(index); } @@ -292,7 +303,9 @@ public abstract class PlayQueue implements Serializable { final int backupIndex = backup.indexOf(getItem(removeIndex)); backup.remove(backupIndex); } - streams.remove(removeIndex); + + history.remove(streams.remove(removeIndex)); + history.add(streams.get(queueIndex.get())); } /** @@ -366,6 +379,7 @@ public abstract class PlayQueue implements Serializable { streams.add(0, streams.remove(newIndex)); } queueIndex.set(0); + history.add(streams.get(0)); broadcast(new ReorderEvent(originIndex, queueIndex.get())); } @@ -393,10 +407,52 @@ public abstract class PlayQueue implements Serializable { } else { queueIndex.set(0); } + history.add(streams.get(queueIndex.get())); broadcast(new ReorderEvent(originIndex, queueIndex.get())); } + /** + * Selects previous played item + * + * This method removes currently playing item from history and + * starts playing the last item from history if it exists + * + * Returns true if history is not empty and the item can be played + * */ + public synchronized boolean previous() { + if (history.size() <= 1) return false; + + history.remove(history.size() - 1); + + PlayQueueItem last = history.remove(history.size() - 1); + setIndex(indexOf(last)); + + return true; + } + + /* + * Compares two PlayQueues. Useful when a user switches players but queue is the same so + * we don't have to do anything with new queue. This method also gives a chance to track history of items in a queue in + * VideoDetailFragment without duplicating items from two identical queues + * */ + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof PlayQueue) || getStreams().size() != ((PlayQueue) obj).getStreams().size()) + return false; + + PlayQueue other = (PlayQueue) obj; + for (int i = 0; i < getStreams().size(); i++) { + if (!getItem(i).getUrl().equals(other.getItem(i).getUrl())) + return false; + } + + return true; + } + + public boolean isDisposed() { + return disposed; + } /*////////////////////////////////////////////////////////////////////////// // Rx Broadcast //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index ff57bc2bf..d0602ed75 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -141,17 +141,29 @@ android:layout_height="match_parent" android:fitsSystemWindows="true"> + + + + + + android:paddingEnd="6dp" + android:layout_toEndOf="@id/spaceBeforeControls" + android:baselineAligned="false"> @@ -274,8 +284,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="top" - android:paddingStart="16dp" - android:paddingEnd="6dp" android:visibility="invisible" tools:ignore="RtlHardcoded" tools:visibility="visible"> @@ -302,6 +310,8 @@ android:layout_marginEnd="8dp" android:gravity="center|left" android:minHeight="35dp" + android:lines="1" + android:ellipsize="end" android:minWidth="50dp" android:textColor="@android:color/white" android:textStyle="bold" @@ -365,10 +375,11 @@ android:id="@+id/fullScreenButton" android:layout_width="40dp" android:layout_height="40dp" - android:paddingEnd="8dp" - android:paddingTop="8dp" + android:layout_marginTop="2dp" + android:layout_marginEnd="2dp" + android:padding="6dp" android:layout_alignParentRight="true" - android:background="@drawable/player_top_controls_bg" + android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" android:scaleType="fitCenter" diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 1022d2e95..bf9da748f 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -139,17 +139,29 @@ android:layout_height="match_parent" android:fitsSystemWindows="true"> + + + + + + android:paddingEnd="6dp" + android:layout_toEndOf="@id/spaceBeforeControls" + android:baselineAligned="false"> @@ -272,8 +282,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="top" - android:paddingStart="16dp" - android:paddingEnd="6dp" android:visibility="invisible" tools:ignore="RtlHardcoded" tools:visibility="visible"> @@ -300,6 +308,8 @@ android:layout_marginEnd="8dp" android:gravity="center|left" android:minHeight="35dp" + android:lines="1" + android:ellipsize="end" android:minWidth="50dp" android:textColor="@android:color/white" android:textStyle="bold" @@ -363,10 +373,11 @@ android:id="@+id/fullScreenButton" android:layout_width="40dp" android:layout_height="40dp" - android:paddingEnd="8dp" - android:paddingTop="8dp" + android:layout_marginTop="2dp" + android:layout_marginEnd="2dp" + android:padding="6dp" android:layout_alignParentRight="true" - android:background="@drawable/player_top_controls_bg" + android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" android:scaleType="fitCenter"