Fourth block of fixes for review

- wrote more methods to PlayQueue. Now it supports internal history of played items with ability to play previous() item. Also it has equals() to check whether queues has the same content or not
- backstack in fragment is more powerful now with help of PlayQueue's history and able to work great with playlists' PlayQueue and SinglePlayQueue at the same time
- simplified logic inside fragment. Easy to understand. New PlayQueue will be added in backstack from only one place; less number of setInitialData() calls
- BasePlayer now able to check PlayQueue and compare it with currently playing. And if it is the same queue it tries to not init() it twice. It gives possibility to have a great backstack in fragment since the same queue will not be played from two different instances and will not be added to backstack twice  with duplicated history inside
- better support of Player.STATE_IDLE
- worked with layouts of player and made them better and more universal
- service will be stopped when activity finishes by a user decision
- fixed a problem related to ChannelPlayQueue and PlaylistPlayQueue in initial start of fragment
- fixed crash in popup
This commit is contained in:
Avently 2020-01-06 13:39:01 +03:00
parent e063967734
commit a2d5314cf7
10 changed files with 184 additions and 65 deletions

View file

@ -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;
}

View file

@ -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<StackItem> 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);

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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);

View file

@ -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,6 +502,12 @@ 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());

View file

@ -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);

View file

@ -46,17 +46,23 @@ public abstract class PlayQueue implements Serializable {
private ArrayList<PlayQueueItem> backup;
private ArrayList<PlayQueueItem> streams;
private ArrayList<PlayQueueItem> history;
@NonNull private final AtomicInteger queueIndex;
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
private transient Subscription reportingReactor;
private transient boolean disposed;
PlayQueue(final int index, final List<PlayQueueItem> 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
//////////////////////////////////////////////////////////////////////////*/

View file

@ -141,17 +141,29 @@
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_top_controls_bg"
android:layout_alignParentTop="true" />
<!-- It will be hidden in popup -->
<Space
android:id="@+id/spaceBeforeControls"
android:layout_width="16dp"
android:layout_height="0dp" />
<LinearLayout
android:id="@+id/topControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@drawable/player_top_controls_bg"
android:orientation="vertical"
android:gravity="top"
android:paddingTop="4dp"
android:baselineAligned="false"
android:layout_toStartOf="@id/fullScreenButton">
android:paddingEnd="6dp"
android:layout_toEndOf="@id/spaceBeforeControls"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/primaryControls"
@ -161,7 +173,6 @@
android:gravity="top"
android:paddingBottom="7dp"
android:paddingLeft="2dp"
android:paddingRight="6dp"
tools:ignore="RtlHardcoded">
<LinearLayout
@ -171,7 +182,6 @@
android:gravity="top"
android:orientation="vertical"
android:paddingTop="6dp"
android:paddingLeft="16dp"
android:paddingRight="8dp"
tools:ignore="RtlHardcoded"
android:layout_weight="1">
@ -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"

View file

@ -139,17 +139,29 @@
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_top_controls_bg"
android:layout_alignParentTop="true" />
<!-- It will be hidden in popup -->
<Space
android:id="@+id/spaceBeforeControls"
android:layout_width="16dp"
android:layout_height="0dp" />
<LinearLayout
android:id="@+id/topControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@drawable/player_top_controls_bg"
android:orientation="vertical"
android:gravity="top"
android:paddingTop="4dp"
android:baselineAligned="false"
android:layout_toStartOf="@id/fullScreenButton">
android:paddingEnd="6dp"
android:layout_toEndOf="@id/spaceBeforeControls"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/primaryControls"
@ -159,7 +171,6 @@
android:gravity="top"
android:paddingBottom="7dp"
android:paddingLeft="2dp"
android:paddingRight="6dp"
tools:ignore="RtlHardcoded">
<LinearLayout
@ -169,7 +180,6 @@
android:gravity="top"
android:orientation="vertical"
android:paddingTop="6dp"
android:paddingLeft="16dp"
android:paddingRight="8dp"
tools:ignore="RtlHardcoded"
android:layout_weight="1">
@ -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"