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 cd1451d37..8d0c22a4d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -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) 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 83c09ef04..4138ead7d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -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 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); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java new file mode 100644 index 000000000..30a5f9e76 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java @@ -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); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 477358113..38b0bf9a4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -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. - *

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