-Added "add to playlist" button to service player play queue item drop down.

-Refactored playlist manipulations out from media source manager.
-Fixed potential ArrayOutOfBound exception when checking if player window is live.
-Fixed service player play queue potentially not refreshing when current play queue is replaced.
This commit is contained in:
John Zhen Mo 2018-03-25 10:15:55 -07:00
parent 1d017d3cbc
commit b0a09c7876
4 changed files with 232 additions and 99 deletions

View File

@ -640,7 +640,7 @@ public abstract class BasePlayer implements
seekTo(recoveryPositionMillis);
playQueue.unsetRecovery(currentSourceIndex);
} else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) {
} else if (isSynchronizing && isLive()) {
if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
// Is still synchronizing?
seekToDefault();
@ -789,7 +789,7 @@ public abstract class BasePlayer implements
@Override
public boolean isNearPlaybackEdge(final long timeToEndMillis) {
// If live, then not near playback edge
if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false;
if (simpleExoPlayer == null || isLive()) return false;
final long currentPositionMillis = simpleExoPlayer.getCurrentPosition();
final long currentDurationMillis = simpleExoPlayer.getDuration();
@ -1127,9 +1127,7 @@ public abstract class BasePlayer implements
/** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
public boolean isLiveEdge() {
if (simpleExoPlayer == null) return false;
final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic();
if (!isLive) return false;
if (simpleExoPlayer == null || !isLive()) return false;
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
@ -1143,6 +1141,16 @@ public abstract class BasePlayer implements
return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
}
public boolean isLive() {
if (simpleExoPlayer == null) return false;
try {
return simpleExoPlayer.isCurrentWindowDynamic();
} catch (@NonNull IndexOutOfBoundsException ignored) {
// Why would this even happen =(
return false;
}
}
public boolean isPlaying() {
final int state = simpleExoPlayer.getPlaybackState();
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)

View File

@ -32,6 +32,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.playlist.PlayQueueAdapter;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
@ -40,6 +41,9 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
@ -151,7 +155,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
finish();
return true;
case R.id.action_append_playlist:
appendToPlaylist();
appendAllToPlaylist();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
@ -187,13 +191,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
private void appendToPlaylist() {
if (this.player != null && this.player.getPlayQueue() != null) {
PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams())
.show(getSupportFragmentManager(), getTag());
}
}
////////////////////////////////////////////////////////////////////////////
// Service Connection
////////////////////////////////////////////////////////////////////////////
@ -319,7 +316,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu menu = new PopupMenu(this, view);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, R.string.play_queue_remove);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/0,
Menu.NONE, R.string.play_queue_remove);
remove.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
@ -328,12 +326,20 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true;
});
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, R.string.play_queue_stream_detail);
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/1,
Menu.NONE, R.string.play_queue_stream_detail);
detail.setOnMenuItemClickListener(menuItem -> {
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
return true;
});
final MenuItem append = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/2,
Menu.NONE, R.string.append_playlist);
append.setOnMenuItemClickListener(menuItem -> {
openPlaylistAppendDialog(Collections.singletonList(item));
return true;
});
menu.show();
}
@ -488,6 +494,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
seeking = false;
}
////////////////////////////////////////////////////////////////////////////
// Playlist append
////////////////////////////////////////////////////////////////////////////
private void appendAllToPlaylist() {
if (player != null && player.getPlayQueue() != null) {
openPlaylistAppendDialog(player.getPlayQueue().getStreams());
}
}
private void openPlaylistAppendDialog(final List<PlayQueueItem> playlist) {
PlaylistAppendDialog.fromPlayQueueItems(playlist)
.show(getSupportFragmentManager(), getTag());
}
////////////////////////////////////////////////////////////////////////////
// Binding Service Listener
////////////////////////////////////////////////////////////////////////////
@ -497,6 +518,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters);
onMaybePlaybackAdapterChanged();
}
@Override
@ -609,4 +631,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
playbackPitchButton.setText(formatPitch(parameters.pitch));
}
}
private void onMaybePlaybackAdapterChanged() {
if (itemsList == null || player == null) return;
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) {
itemsList.setAdapter(maybeNewAdapter);
}
}
}

View File

@ -0,0 +1,144 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
public class ManagedMediaSourcePlaylist {
@NonNull private final DynamicConcatenatingMediaSource internalSource;
public ManagedMediaSourcePlaylist() {
internalSource = new DynamicConcatenatingMediaSource(/*isPlaylistAtomic=*/false,
new ShuffleOrder.UnshuffledShuffleOrder(0));
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Delegations
//////////////////////////////////////////////////////////////////////////*/
public int size() {
return internalSource.getSize();
}
/**
* Returns the {@link ManagedMediaSource} at the given index of the playlist.
* If the index is invalid, then null is returned.
* */
@Nullable
public ManagedMediaSource get(final int index) {
return (index < 0 || index >= size()) ?
null : (ManagedMediaSource) internalSource.getMediaSource(index);
}
public void dispose() {
internalSource.releaseSource();
}
@NonNull
public DynamicConcatenatingMediaSource getParentMediaSource() {
return internalSource;
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist Manipulation
//////////////////////////////////////////////////////////////////////////*/
/**
* Expands the {@link DynamicConcatenatingMediaSource} by appending it with a
* {@link PlaceholderMediaSource}.
*
* @see #append(ManagedMediaSource)
* */
public synchronized void expand() {
append(new PlaceholderMediaSource());
}
/**
* Appends a {@link ManagedMediaSource} to the end of {@link DynamicConcatenatingMediaSource}.
* @see DynamicConcatenatingMediaSource#addMediaSource
* */
public synchronized void append(@NonNull final ManagedMediaSource source) {
internalSource.addMediaSource(source);
}
/**
* Removes a {@link ManagedMediaSource} from {@link DynamicConcatenatingMediaSource}
* at the given index. If this index is out of bound, then the removal is ignored.
* @see DynamicConcatenatingMediaSource#removeMediaSource(int)
* */
public synchronized void remove(final int index) {
if (index < 0 || index > internalSource.getSize()) return;
internalSource.removeMediaSource(index);
}
/**
* Moves a {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
* from the given source index to the target index. If either index is out of bound,
* then the call is ignored.
* @see DynamicConcatenatingMediaSource#moveMediaSource(int, int)
* */
public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= internalSource.getSize() || target >= internalSource.getSize()) return;
internalSource.moveMediaSource(source, target);
}
/**
* Invalidates the {@link ManagedMediaSource} at the given index by replacing it
* with a {@link PlaceholderMediaSource}.
* @see #invalidate(int, Runnable)
* @see #update(int, ManagedMediaSource, Runnable)
* */
public synchronized void invalidate(final int index) {
invalidate(index, /*doNothing=*/null);
}
/**
* Invalidates the {@link ManagedMediaSource} at the given index by replacing it
* with a {@link PlaceholderMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable)
* */
public synchronized void invalidate(final int index,
@Nullable final Runnable finalizingAction) {
if (get(index) instanceof PlaceholderMediaSource) return;
update(index, new PlaceholderMediaSource(), finalizingAction);
}
/**
* Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable)
* */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source) {
update(index, source, /*doNothing=*/null);
}
/**
* Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
* then the replacement is ignored.
* @see DynamicConcatenatingMediaSource#addMediaSource
* @see DynamicConcatenatingMediaSource#removeMediaSource(int, Runnable)
* */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source,
@Nullable final Runnable finalizingAction) {
if (index < 0 || index >= internalSource.getSize()) return;
// Add and remove are sequential on the same thread, therefore here, the exoplayer
// message queue must receive and process add before remove.
// However, finalizing action occurs strictly after the timeline has completed
// all its changes on the playback thread, so it is possible, in the meantime, other calls
// that modifies the playlist media source may occur in between. Therefore,
// it is not safe to call remove as the finalizing action of add.
internalSource.addMediaSource(index + 1, source);
// Also, because of the above, it is thus only safe to synchronize the player
// in the finalizing action AFTER the removal is complete and the timeline has changed.
internalSource.removeMediaSource(index, finalizingAction);
}
}

View File

@ -6,7 +6,6 @@ import android.util.Log;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@ -14,6 +13,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
@ -52,7 +52,6 @@ public class MediaSourceManager {
* streams before will only be cached for future usage.
*
* @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource)
* @see #update(int, MediaSource, Runnable)
* */
private final static int WINDOW_SIZE = 1;
@ -103,7 +102,7 @@ public class MediaSourceManager {
@NonNull private final AtomicBoolean isBlocked;
@NonNull private DynamicConcatenatingMediaSource sources;
@NonNull private ManagedMediaSourcePlaylist playlist;
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
@ -143,7 +142,7 @@ public class MediaSourceManager {
this.isBlocked = new AtomicBoolean(false);
this.sources = new DynamicConcatenatingMediaSource();
this.playlist = new ManagedMediaSourcePlaylist();
this.loadingItems = Collections.synchronizedSet(new HashSet<>());
@ -167,7 +166,7 @@ public class MediaSourceManager {
playQueueReactor.cancel();
loaderReactor.dispose();
syncReactor.dispose();
sources.releaseSource();
playlist.dispose();
}
/*//////////////////////////////////////////////////////////////////////////
@ -215,17 +214,18 @@ public class MediaSourceManager {
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex());
playlist.remove(removeEvent.getRemoveIndex());
break;
case MOVE:
final MoveEvent moveEvent = (MoveEvent) event;
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
playlist.move(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case REORDER:
// Need to move to ensure the playing index from play queue matches that of
// the source timeline, and then window correction can take care of the rest
final ReorderEvent reorderEvent = (ReorderEvent) event;
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex());
playlist.move(reorderEvent.getFromSelectedIndex(),
reorderEvent.getToSelectedIndex());
break;
case RECOVERY:
default:
@ -266,10 +266,9 @@ public class MediaSourceManager {
}
private boolean isPlaybackReady() {
if (sources.getSize() != playQueue.size()) return false;
final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex());
if (mediaSource == null) return false;
final ManagedMediaSource mediaSource =
(ManagedMediaSource) sources.getMediaSource(playQueue.getIndex());
final PlayQueueItem playQueueItem = playQueue.getItem();
return mediaSource.isStreamEqual(playQueueItem);
}
@ -290,7 +289,7 @@ public class MediaSourceManager {
if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) {
isBlocked.set(false);
playbackListener.onPlaybackUnblock(sources);
playbackListener.onPlaybackUnblock(playlist.getParentMediaSource());
}
}
@ -322,6 +321,7 @@ public class MediaSourceManager {
}
private void maybeSynchronizePlayer() {
cleanSweep();
maybeUnblock();
maybeSync();
}
@ -383,7 +383,7 @@ public class MediaSourceManager {
private void maybeLoadItem(@NonNull final PlayQueueItem item) {
if (DEBUG) Log.d(TAG, "maybeLoadItem() called.");
if (playQueue.indexOf(item) >= sources.getSize()) return;
if (playQueue.indexOf(item) >= playlist.size()) return;
if (!loadingItems.contains(item) && isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() +
@ -429,7 +429,7 @@ public class MediaSourceManager {
if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " +
"title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
}
}
@ -445,10 +445,8 @@ public class MediaSourceManager {
* */
private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) {
final int index = playQueue.indexOf(item);
if (index == -1 || index >= sources.getSize()) return false;
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
return mediaSource.shouldBeReplacedWith(item,
final ManagedMediaSource mediaSource = playlist.get(index);
return mediaSource != null && mediaSource.shouldBeReplacedWith(item,
/*mightBeInProgress=*/index != playQueue.getIndex());
}
@ -465,10 +463,9 @@ public class MediaSourceManager {
* */
private void maybeRenewCurrentIndex() {
final int currentIndex = playQueue.getIndex();
if (sources.getSize() <= currentIndex) return;
final ManagedMediaSource currentSource = playlist.get(currentIndex);
if (currentSource == null) return;
final ManagedMediaSource currentSource =
(ManagedMediaSource) sources.getMediaSource(currentIndex);
final PlayQueueItem currentItem = playQueue.getItem();
if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) {
maybeSynchronizePlayer();
@ -477,7 +474,19 @@ public class MediaSourceManager {
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate);
playlist.invalidate(currentIndex, this::loadImmediate);
}
/**
* Scans the entire playlist for {@link MediaSource}s that requires correction,
* and replace these sources with a {@link PlaceholderMediaSource}.
* */
private void cleanSweep() {
for (int index = 0; index < playlist.size(); index++) {
if (isCorrectionNeeded(playQueue.getItem(index))) {
playlist.invalidate(index);
}
}
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Helpers
@ -486,72 +495,14 @@ public class MediaSourceManager {
private void resetSources() {
if (DEBUG) Log.d(TAG, "resetSources() called.");
this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource(false,
// Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
new ShuffleOrder.UnshuffledShuffleOrder(0));
playlist.dispose();
playlist = new ManagedMediaSourcePlaylist();
}
private void populateSources() {
if (DEBUG) Log.d(TAG, "populateSources() called.");
if (sources.getSize() >= playQueue.size()) return;
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
emplace(index, new PlaceholderMediaSource());
while (playlist.size() < playQueue.size()) {
playlist.expand();
}
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Manipulation
//////////////////////////////////////////////////////////////////////////*/
/**
* Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource}
* with position in respect to the play queue only if no {@link MediaSource}
* already exists at the given index.
* */
private synchronized void emplace(final int index, @NonNull final MediaSource source) {
if (index < sources.getSize()) return;
sources.addMediaSource(index, source);
}
/**
* Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource}
* at the given index. If this index is out of bound, then the removal is ignored.
* */
private synchronized void remove(final int index) {
if (index < 0 || index > sources.getSize()) return;
sources.removeMediaSource(index);
}
/**
* Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
* from the given source index to the target index. If either index is out of bound,
* then the call is ignored.
* */
private synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= sources.getSize() || target >= sources.getSize()) return;
sources.moveMediaSource(source, target);
}
/**
* Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
* at the given index with a given {@link MediaSource}. If the index is out of bound,
* then the replacement is ignored.
* <br><br>
* Not recommended to use on indices LESS THAN the currently playing index, since
* this will modify the playback timeline prior to the index and may cause desynchronization
* on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}.
* */
private synchronized void update(final int index, @NonNull final MediaSource source,
@Nullable final Runnable finalizingAction) {
if (index < 0 || index >= sources.getSize()) return;
sources.addMediaSource(index + 1, source, () ->
sources.removeMediaSource(index, finalizingAction));
}
}