From 5fa56f2aca5c5ce8bb64fd0b7f5b2dbe65d5ec0b Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 24 Mar 2018 12:58:11 -0700 Subject: [PATCH 01/14] -Modified playback parameter dialog to use translucent background. --- .../newpipe/player/ServicePlayerActivity.java | 4 +- .../helper/PlaybackParameterDialog.java | 40 +++++++++++++------ .../res/layout/dialog_playback_parameter.xml | 16 ++++++-- 3 files changed, 42 insertions(+), 18 deletions(-) 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 239c9c8d3..83c09ef04 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -453,8 +453,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void openPlaybackParameterDialog() { if (player == null) return; - PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), - player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch()) + .show(getSupportFragmentManager(), getTag()); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 7c7d87791..289661aea 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -2,11 +2,12 @@ package org.schabi.newpipe.player.helper; import android.app.Dialog; import android.content.Context; +import android.graphics.drawable.ColorDrawable; import android.os.Bundle; +import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; import android.util.Log; import android.view.View; import android.widget.CheckBox; @@ -15,6 +16,7 @@ import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.util.SliderStrategy; +import org.schabi.newpipe.util.ThemeHelper; import static org.schabi.newpipe.player.BasePlayer.DEBUG; @@ -68,6 +70,7 @@ public class PlaybackParameterDialog extends DialogFragment { @Nullable private CheckBox unhookingCheckbox; @Nullable private TextView nightCorePresetText; + @Nullable private TextView defaultPresetText; @Nullable private TextView resetPresetText; public static PlaybackParameterDialog newInstance(final double playbackTempo, @@ -115,19 +118,23 @@ public class PlaybackParameterDialog extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final Context context = requireContext(); + final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); setupControlViews(view); - final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) - .setTitle(R.string.playback_speed_control) - .setView(view) - .setCancelable(true) - .setNegativeButton(R.string.cancel, (dialogInterface, i) -> - setPlaybackParameters(initialTempo, initialPitch)) - .setPositiveButton(R.string.finish, (dialogInterface, i) -> - setCurrentPlaybackParameters()); + Dialog dialog = new Dialog(context); + dialog.setCancelable(true); + dialog.setCanceledOnTouchOutside(true); + dialog.setTitle(R.string.playback_speed_control); + dialog.setContentView(view); + if (dialog.getWindow() != null) { + @ColorInt final int backgroundColor = ThemeHelper.resolveColorFromAttr(context, + R.attr.queue_background_color); + dialog.getWindow().setBackgroundDrawable(new ColorDrawable(backgroundColor)); + } - return dialogBuilder.create(); + return dialog; } /*////////////////////////////////////////////////////////////////////////// @@ -244,11 +251,20 @@ public class PlaybackParameterDialog extends DialogFragment { }); } + defaultPresetText = rootView.findViewById(R.id.presetDefault); + if (defaultPresetText != null) { + defaultPresetText.setOnClickListener(view -> { + setTempoSlider(DEFAULT_TEMPO); + setPitchSlider(DEFAULT_PITCH); + setCurrentPlaybackParameters(); + }); + } + resetPresetText = rootView.findViewById(R.id.presetReset); if (resetPresetText != null) { resetPresetText.setOnClickListener(view -> { - setTempoSlider(DEFAULT_TEMPO); - setPitchSlider(DEFAULT_PITCH); + setTempoSlider(initialTempo); + setPitchSlider(initialPitch); setCurrentPlaybackParameters(); }); } diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml index a8c6a5dcd..a3062567e 100644 --- a/app/src/main/res/layout/dialog_playback_parameter.xml +++ b/app/src/main/res/layout/dialog_playback_parameter.xml @@ -4,9 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="false" - android:paddingLeft="@dimen/video_item_search_padding" - android:paddingRight="@dimen/video_item_search_padding" - android:paddingTop="@dimen/video_item_search_padding"> + android:padding="@dimen/video_item_search_padding"> + + From 1d017d3cbcc273a0a44ac9d5288bf1d4ce682195 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 24 Mar 2018 13:01:21 -0700 Subject: [PATCH 02/14] -Modified LIVE button to be untranslatable on all players. --- app/src/main/res/layout-land/activity_player_queue_control.xml | 2 +- app/src/main/res/layout/activity_main_player.xml | 2 +- app/src/main/res/layout/activity_player_queue_control.xml | 2 +- app/src/main/res/layout/player_popup.xml | 2 +- app/src/main/res/values/strings.xml | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index 11765f901..72f673ffc 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -304,7 +304,7 @@ android:paddingLeft="4dp" android:paddingRight="4dp" android:gravity="center" - android:text="@string/duration_live" + android:text="@string/duration_live_button" android:textAllCaps="true" android:textColor="?attr/colorAccent" android:maxLength="4" diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index c581c3203..d97f1fe72 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -406,7 +406,7 @@ android:paddingLeft="4dp" android:paddingRight="4dp" android:gravity="center" - android:text="@string/duration_live" + android:text="@string/duration_live_button" android:textAllCaps="true" android:textColor="@android:color/white" android:maxLength="4" diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml index 7f649e382..e81a19553 100644 --- a/app/src/main/res/layout/activity_player_queue_control.xml +++ b/app/src/main/res/layout/activity_player_queue_control.xml @@ -154,7 +154,7 @@ android:paddingLeft="4dp" android:paddingRight="4dp" android:gravity="center" - android:text="@string/duration_live" + android:text="@string/duration_live_button" android:textAllCaps="true" android:textColor="?attr/colorAccent" android:maxLength="4" diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml index 0c3ea77df..5e8ed664e 100644 --- a/app/src/main/res/layout/player_popup.xml +++ b/app/src/main/res/layout/player_popup.xml @@ -198,7 +198,7 @@ android:paddingLeft="4dp" android:paddingRight="4dp" android:gravity="center_vertical" - android:text="@string/duration_live" + android:text="@string/duration_live_button" android:textAllCaps="true" android:textColor="@android:color/white" android:maxLength="4" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f22f42e95..33ce46412 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,6 +119,7 @@ Show age restricted content Age Restricted Video. Allowing such material is possible from Settings. live + LIVE Downloads Downloads Error report From b0a09c787698964891c6b06b43f4f2666c83497e Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 25 Mar 2018 10:15:55 -0700 Subject: [PATCH 03/14] -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. --- .../org/schabi/newpipe/player/BasePlayer.java | 18 ++- .../newpipe/player/ServicePlayerActivity.java | 50 ++++-- .../ManagedMediaSourcePlaylist.java | 144 ++++++++++++++++++ .../player/playback/MediaSourceManager.java | 119 +++++---------- 4 files changed, 232 insertions(+), 99 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java 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)); - } } From 7219c8d33c0c9ff8b70d26b7b7cab974019e967e Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 25 Mar 2018 11:33:31 -0700 Subject: [PATCH 04/14] -Fixed main player multiwindow pauses when focus changes. --- .../java/org/schabi/newpipe/player/MainVideoPlayer.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index dbc34b11a..48503eda5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -150,6 +150,7 @@ public final class MainVideoPlayer extends AppCompatActivity protected void onResume() { super.onResume(); if (DEBUG) Log.d(TAG, "onResume() called"); + if (isInMultiWindow()) return; if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() && !playerImpl.isPlaying()) { playerImpl.onPlay(); @@ -184,7 +185,7 @@ public final class MainVideoPlayer extends AppCompatActivity protected void onPause() { super.onPause(); if (DEBUG) Log.d(TAG, "onPause() called"); - + if (isInMultiWindow()) return; if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) { playerImpl.wasPlaying = playerImpl.isPlaying(); playerImpl.onPause(); @@ -342,6 +343,10 @@ public final class MainVideoPlayer extends AppCompatActivity } } + private boolean isInMultiWindow() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode(); + } + //////////////////////////////////////////////////////////////////////////// // Playback Parameters Listener //////////////////////////////////////////////////////////////////////////// From ece93cadd523e297383d0990d5152cf343c74b61 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 27 Mar 2018 12:10:48 -0700 Subject: [PATCH 05/14] -Added better player exception handling to player. -Added expired media source cleaning to media source manager. --- .../local/bookmark/BookmarkFragment.java | 3 +- .../org/schabi/newpipe/player/BasePlayer.java | 29 ++++---------- .../player/mediasource/FailedMediaSource.java | 32 ++++++++++++--- .../ManagedMediaSourcePlaylist.java | 13 ++++--- .../player/playback/MediaSourceManager.java | 39 ++++++++++--------- 5 files changed, 65 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index b740cb15e..e1f724b6e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -56,7 +56,8 @@ public final class BookmarkFragment @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final AppDatabase database = NewPipeDatabase.getInstance(getContext()); + if (activity == null) return; + final AppDatabase database = NewPipeDatabase.getInstance(activity); localPlaylistManager = new LocalPlaylistManager(database); remotePlaylistManager = new RemotePlaylistManager(database); disposables = new CompositeDisposable(); 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 8d0c22a4d..e2fd2e6f7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -64,6 +64,7 @@ import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.MediaSessionManager; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.playback.BasePlayerMediaSession; import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.MediaSourceManager; @@ -700,26 +701,6 @@ public abstract class BasePlayer implements } } - /** - * Processes {@link ExoPlaybackException} tagged with {@link ExoPlaybackException#TYPE_SOURCE}. - *

- * If the current {@link com.google.android.exoplayer2.Timeline.Window window} is valid, - * then we know the error is produced by transitioning into a bad window, therefore we report - * an error to the play queue based on if the current error can be skipped. - *

- * This is done because ExoPlayer reports the source exceptions before window is - * transitioned on seamless playback. Because player error causes ExoPlayer to go - * back to {@link Player#STATE_IDLE STATE_IDLE}, we reset and prepare the media source - * again to resume playback. - *

- * In the event that this error is produced during a valid stream playback, we save the - * current position so the playback may be recovered and resumed manually by the user. This - * happens only if the playback is {@link #RECOVERY_SKIP_THRESHOLD} milliseconds until complete. - *

- * In the event of livestreaming being lagged behind for any reason, most notably pausing for - * too long, a {@link BehindLiveWindowException} will be produced. This will trigger a reload - * instead of skipping or removal. - * */ private void processSourceError(final IOException error) { if (simpleExoPlayer == null || playQueue == null) return; @@ -733,8 +714,14 @@ public abstract class BasePlayer implements reload(); } else if (cause instanceof UnknownHostException) { playQueue.error(/*isNetworkProblem=*/true); + } else if (isCurrentWindowValid()) { + playQueue.error(/*isTransitioningToBadStream=*/true); + } else if (error instanceof FailedMediaSource.MediaSourceResolutionException) { + playQueue.error(/*recoverableWithNoAvailableStream=*/false); + } else if (error instanceof FailedMediaSource.StreamInfoLoadException) { + playQueue.error(/*recoverableIfLoadFailsWhenNetworkIsFine=*/false); } else { - playQueue.error(isCurrentWindowValid()); + playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 5f029cc50..625c5d416 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -14,13 +14,35 @@ import java.io.IOException; public class FailedMediaSource implements ManagedMediaSource { private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); + public static class FailedMediaSourceException extends IOException { + FailedMediaSourceException(String message) { + super(message); + } + + FailedMediaSourceException(Throwable cause) { + super(cause); + } + } + + public static final class MediaSourceResolutionException extends FailedMediaSourceException { + public MediaSourceResolutionException(String message) { + super(message); + } + } + + public static final class StreamInfoLoadException extends FailedMediaSourceException { + public StreamInfoLoadException(Throwable cause) { + super(cause); + } + } + private final PlayQueueItem playQueueItem; - private final Throwable error; + private final FailedMediaSourceException error; private final long retryTimestamp; public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, - @NonNull final Throwable error, + @NonNull final FailedMediaSourceException error, final long retryTimestamp) { this.playQueueItem = playQueueItem; this.error = error; @@ -32,7 +54,7 @@ public class FailedMediaSource implements ManagedMediaSource { * The error will always be propagated to ExoPlayer. * */ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, - @NonNull final Throwable error) { + @NonNull final FailedMediaSourceException error) { this.playQueueItem = playQueueItem; this.error = error; this.retryTimestamp = Long.MAX_VALUE; @@ -42,7 +64,7 @@ public class FailedMediaSource implements ManagedMediaSource { return playQueueItem; } - public Throwable getError() { + public FailedMediaSourceException getError() { return error; } @@ -57,7 +79,7 @@ public class FailedMediaSource implements ManagedMediaSource { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - throw new IOException(error); + throw error; } @Override 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 index 30a5f9e76..3cbc75395 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java @@ -129,15 +129,16 @@ public class ManagedMediaSourcePlaylist { 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. + // message queue must receive and process add before remove, effectively treating them + // as atomic. - // 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. + // Since the finalizing action occurs strictly after the timeline has completed + // all its changes on the playback thread, thus, it is possible, in the meantime, + // other calls that modifies the playlist media source occur in between. This makes + // it unsafe 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 + // Because of the above race condition, 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 38b0bf9a4..33cd7cdc0 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 @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.playback; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.util.ArraySet; import android.util.Log; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; @@ -24,7 +25,6 @@ import org.schabi.newpipe.playlist.events.ReorderEvent; import org.schabi.newpipe.util.ServiceHelper; import java.util.Collections; -import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -39,6 +39,7 @@ import io.reactivex.functions.Consumer; import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.subjects.PublishSubject; +import static org.schabi.newpipe.player.mediasource.FailedMediaSource.*; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; public class MediaSourceManager { @@ -144,7 +145,7 @@ public class MediaSourceManager { this.playlist = new ManagedMediaSourcePlaylist(); - this.loadingItems = Collections.synchronizedSet(new HashSet<>()); + this.loadingItems = Collections.synchronizedSet(new ArraySet<>()); playQueue.getBroadcastReceiver() .observeOn(AndroidSchedulers.mainThread()) @@ -321,9 +322,9 @@ public class MediaSourceManager { } private void maybeSynchronizePlayer() { - cleanSweep(); maybeUnblock(); maybeSync(); + cleanPlaylist(); } /*////////////////////////////////////////////////////////////////////////// @@ -366,7 +367,7 @@ public class MediaSourceManager { final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); final int rightLimit = currentIndex + WINDOW_SIZE + 1; final int rightBound = Math.min(playQueue.size(), rightLimit); - final Set items = new HashSet<>( + final Set items = new ArraySet<>( playQueue.getStreams().subList(leftBound,rightBound)); // Do a round robin @@ -402,19 +403,19 @@ public class MediaSourceManager { return stream.getStream().map(streamInfo -> { final MediaSource source = playbackListener.sourceOf(stream, streamInfo); if (source == null) { - final Exception exception = new IllegalStateException( - "Unable to resolve source from stream info." + - " URL: " + stream.getUrl() + - ", audio count: " + streamInfo.getAudioStreams().size() + - ", video count: " + streamInfo.getVideoOnlyStreams().size() + - streamInfo.getVideoStreams().size()); - return new FailedMediaSource(stream, exception); + final String message = "Unable to resolve source from stream info." + + " URL: " + stream.getUrl() + + ", audio count: " + streamInfo.getAudioStreams().size() + + ", video count: " + streamInfo.getVideoOnlyStreams().size() + + streamInfo.getVideoStreams().size(); + return new FailedMediaSource(stream, new MediaSourceResolutionException(message)); } final long expiration = System.currentTimeMillis() + ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); return new LoadedMediaSource(source, stream, expiration); - }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); + }).onErrorReturn(throwable -> new FailedMediaSource(stream, + new StreamInfoLoadException(throwable))); } private void onMediaSourceReceived(@NonNull final PlayQueueItem item, @@ -478,13 +479,15 @@ public class MediaSourceManager { } /** - * Scans the entire playlist for {@link MediaSource}s that requires correction, - * and replace these sources with a {@link PlaceholderMediaSource}. + * Scans the entire playlist for {@link ManagedMediaSource}s that requires correction, + * and replaces these sources with a {@link PlaceholderMediaSource} if they are not part + * of the excluded items. * */ - private void cleanSweep() { - for (int index = 0; index < playlist.size(); index++) { - if (isCorrectionNeeded(playQueue.getItem(index))) { - playlist.invalidate(index); + private void cleanPlaylist() { + if (DEBUG) Log.d(TAG, "cleanPlaylist() called."); + for (final PlayQueueItem item : playQueue.getStreams()) { + if (isCorrectionNeeded(item)) { + playlist.invalidate(playQueue.indexOf(item)); } } } From 3de9da052883f02074caf520da3147c85a01f90a Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 29 Mar 2018 18:24:30 -0700 Subject: [PATCH 06/14] -Bump exoplayer dependency to 2.7.2. --- app/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e20535ee4..cf4db5ccc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,7 +42,7 @@ android { ext { supportLibVersion = '27.1.0' - exoPlayerLibVersion = '2.7.1' + exoPlayerLibVersion = '2.7.2' roomDbLibVersion = '1.0.0' leakCanaryLibVersion = '1.5.4' okHttpLibVersion = '1.5.0' @@ -73,6 +73,7 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.nononsenseapps:filepicker:4.2.1' + implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion" implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion" From 50392ed67dc82387742d2ef75475a82dd587f955 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 30 Mar 2018 13:51:18 -0700 Subject: [PATCH 07/14] -Changed failed media source exception to use cause instead of top level exception. --- app/src/main/java/org/schabi/newpipe/player/BasePlayer.java | 4 ++-- .../schabi/newpipe/player/mediasource/FailedMediaSource.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 e2fd2e6f7..bb1851efa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -716,9 +716,9 @@ public abstract class BasePlayer implements playQueue.error(/*isNetworkProblem=*/true); } else if (isCurrentWindowValid()) { playQueue.error(/*isTransitioningToBadStream=*/true); - } else if (error instanceof FailedMediaSource.MediaSourceResolutionException) { + } else if (cause instanceof FailedMediaSource.MediaSourceResolutionException) { playQueue.error(/*recoverableWithNoAvailableStream=*/false); - } else if (error instanceof FailedMediaSource.StreamInfoLoadException) { + } else if (cause instanceof FailedMediaSource.StreamInfoLoadException) { playQueue.error(/*recoverableIfLoadFailsWhenNetworkIsFine=*/false); } else { playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 625c5d416..878d7c711 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -14,7 +14,7 @@ import java.io.IOException; public class FailedMediaSource implements ManagedMediaSource { private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); - public static class FailedMediaSourceException extends IOException { + public static class FailedMediaSourceException extends Exception { FailedMediaSourceException(String message) { super(message); } @@ -79,7 +79,7 @@ public class FailedMediaSource implements ManagedMediaSource { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - throw error; + throw new IOException(error); } @Override From 111a0f9f171341f2c35f1c10cdddcb9dcf53f405 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 31 Mar 2018 10:58:11 -0700 Subject: [PATCH 08/14] -Modified playback parameter dialog to use translucent background. (reverted from commit 0d25254d4831aca92ad6cf6c0c772279b32b4a07) --- .../newpipe/player/ServicePlayerActivity.java | 4 +- .../helper/PlaybackParameterDialog.java | 40 ++++++------------- .../res/layout/dialog_playback_parameter.xml | 16 ++------ 3 files changed, 18 insertions(+), 42 deletions(-) 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 4138ead7d..ccaa6f225 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -459,8 +459,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void openPlaybackParameterDialog() { if (player == null) return; - PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch()) - .show(getSupportFragmentManager(), getTag()); + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), + player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 289661aea..7c7d87791 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -2,12 +2,11 @@ package org.schabi.newpipe.player.helper; import android.app.Dialog; import android.content.Context; -import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; import android.util.Log; import android.view.View; import android.widget.CheckBox; @@ -16,7 +15,6 @@ import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.util.SliderStrategy; -import org.schabi.newpipe.util.ThemeHelper; import static org.schabi.newpipe.player.BasePlayer.DEBUG; @@ -70,7 +68,6 @@ public class PlaybackParameterDialog extends DialogFragment { @Nullable private CheckBox unhookingCheckbox; @Nullable private TextView nightCorePresetText; - @Nullable private TextView defaultPresetText; @Nullable private TextView resetPresetText; public static PlaybackParameterDialog newInstance(final double playbackTempo, @@ -118,23 +115,19 @@ public class PlaybackParameterDialog extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - final Context context = requireContext(); - final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); setupControlViews(view); - Dialog dialog = new Dialog(context); - dialog.setCancelable(true); - dialog.setCanceledOnTouchOutside(true); - dialog.setTitle(R.string.playback_speed_control); - dialog.setContentView(view); - if (dialog.getWindow() != null) { - @ColorInt final int backgroundColor = ThemeHelper.resolveColorFromAttr(context, - R.attr.queue_background_color); - dialog.getWindow().setBackgroundDrawable(new ColorDrawable(backgroundColor)); - } + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) + .setTitle(R.string.playback_speed_control) + .setView(view) + .setCancelable(true) + .setNegativeButton(R.string.cancel, (dialogInterface, i) -> + setPlaybackParameters(initialTempo, initialPitch)) + .setPositiveButton(R.string.finish, (dialogInterface, i) -> + setCurrentPlaybackParameters()); - return dialog; + return dialogBuilder.create(); } /*////////////////////////////////////////////////////////////////////////// @@ -251,20 +244,11 @@ public class PlaybackParameterDialog extends DialogFragment { }); } - defaultPresetText = rootView.findViewById(R.id.presetDefault); - if (defaultPresetText != null) { - defaultPresetText.setOnClickListener(view -> { - setTempoSlider(DEFAULT_TEMPO); - setPitchSlider(DEFAULT_PITCH); - setCurrentPlaybackParameters(); - }); - } - resetPresetText = rootView.findViewById(R.id.presetReset); if (resetPresetText != null) { resetPresetText.setOnClickListener(view -> { - setTempoSlider(initialTempo); - setPitchSlider(initialPitch); + setTempoSlider(DEFAULT_TEMPO); + setPitchSlider(DEFAULT_PITCH); setCurrentPlaybackParameters(); }); } diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml index a3062567e..a8c6a5dcd 100644 --- a/app/src/main/res/layout/dialog_playback_parameter.xml +++ b/app/src/main/res/layout/dialog_playback_parameter.xml @@ -4,7 +4,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="false" - android:padding="@dimen/video_item_search_padding"> + android:paddingLeft="@dimen/video_item_search_padding" + android:paddingRight="@dimen/video_item_search_padding" + android:paddingTop="@dimen/video_item_search_padding"> - - From de534b58c5926166ae6ca6fa931461fe6804b0b6 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 31 Mar 2018 10:59:49 -0700 Subject: [PATCH 09/14] -Removed playlist cleaning. --- .../player/playback/MediaSourceManager.java | 15 --------------- 1 file changed, 15 deletions(-) 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 33cd7cdc0..583c4b8e7 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 @@ -324,7 +324,6 @@ public class MediaSourceManager { private void maybeSynchronizePlayer() { maybeUnblock(); maybeSync(); - cleanPlaylist(); } /*////////////////////////////////////////////////////////////////////////// @@ -477,20 +476,6 @@ public class MediaSourceManager { "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); playlist.invalidate(currentIndex, this::loadImmediate); } - - /** - * Scans the entire playlist for {@link ManagedMediaSource}s that requires correction, - * and replaces these sources with a {@link PlaceholderMediaSource} if they are not part - * of the excluded items. - * */ - private void cleanPlaylist() { - if (DEBUG) Log.d(TAG, "cleanPlaylist() called."); - for (final PlayQueueItem item : playQueue.getStreams()) { - if (isCorrectionNeeded(item)) { - playlist.invalidate(playQueue.indexOf(item)); - } - } - } /*////////////////////////////////////////////////////////////////////////// // MediaSource Playlist Helpers //////////////////////////////////////////////////////////////////////////*/ From 238bff1feeb6adc1e9070b9d8eb33d2ffc8c6b60 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 2 Apr 2018 16:07:43 -0700 Subject: [PATCH 10/14] -Modified adaptive track selection to upgrade with 1 second of buffer. -Modified dynamic track updates to seek to default time instead of clamping to 0 time when negative progress is reached. --- .../org/schabi/newpipe/player/BasePlayer.java | 68 +++++++++++-------- .../newpipe/player/helper/PlayerHelper.java | 14 ++++ 2 files changed, 54 insertions(+), 28 deletions(-) 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 bb1851efa..e3d351671 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -46,7 +46,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Util; @@ -125,7 +125,6 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; - protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f}; protected PlayQueue playQueue; protected PlayQueueAdapter playQueueAdapter; @@ -141,10 +140,11 @@ public abstract class BasePlayer implements // Player //////////////////////////////////////////////////////////////////////////*/ - protected final static int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds - protected final static int PLAY_PREV_ACTIVATION_LIMIT = 5000; // 5 seconds - protected final static int PROGRESS_LOOP_INTERVAL = 500; - protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds + protected final static int FAST_FORWARD_REWIND_AMOUNT_MILLIS = 10000; // 10 Seconds + protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds + protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500; + protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds + protected final static int SHORT_LIVESTREAM_CHUNK_LENGTH_MILLIS = 60000; // 1 minute protected CustomTrackSelector trackSelector; protected PlayerDataSource dataSource; @@ -192,8 +192,8 @@ public abstract class BasePlayer implements final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); - final AdaptiveTrackSelection.Factory trackSelectionFactory = - new AdaptiveTrackSelection.Factory(bandwidthMeter); + final TrackSelection.Factory trackSelectionFactory = + PlayerHelper.getQualitySelector(context, bandwidthMeter); trackSelector = new CustomTrackSelector(trackSelectionFactory); final LoadControl loadControl = new LoadController(context); @@ -519,15 +519,16 @@ public abstract class BasePlayer implements } public void triggerProgressUpdate() { + if (simpleExoPlayer == null) return; onUpdateProgress( - (int) simpleExoPlayer.getCurrentPosition(), + Math.max((int) simpleExoPlayer.getCurrentPosition(), 0), (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage() ); } private Disposable getProgressReactor() { - return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) + return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> triggerProgressUpdate()); } @@ -554,8 +555,8 @@ public abstract class BasePlayer implements // Ensure dynamic/livestream timeline changes does not cause negative position if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) { if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " + - "clamping position to 0ms."); - seekTo(/*clampToTime=*/0); + "clamping to default position."); + seekToDefault(); } break; } @@ -703,11 +704,7 @@ public abstract class BasePlayer implements private void processSourceError(final IOException error) { if (simpleExoPlayer == null || playQueue == null) return; - - if (simpleExoPlayer.getCurrentPosition() < - simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { - setRecovery(); - } + setRecovery(); final Throwable cause = error.getCause(); if (cause instanceof BehindLiveWindowException) { @@ -972,22 +969,22 @@ public abstract class BasePlayer implements public void onFastRewind() { if (DEBUG) Log.d(TAG, "onFastRewind() called"); - seekBy(-FAST_FORWARD_REWIND_AMOUNT); + seekBy(-FAST_FORWARD_REWIND_AMOUNT_MILLIS); } public void onFastForward() { if (DEBUG) Log.d(TAG, "onFastForward() called"); - seekBy(FAST_FORWARD_REWIND_AMOUNT); + seekBy(FAST_FORWARD_REWIND_AMOUNT_MILLIS); } public void onPlayPrevious() { if (simpleExoPlayer == null || playQueue == null) return; if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); - /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, + /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds, * restart current track. Also restart the track if the current track * is the first in a queue.*/ - if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || + if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS || playQueue.getIndex() == 0) { seekToDefault(); playQueue.offsetIndex(0); @@ -1036,8 +1033,27 @@ public abstract class BasePlayer implements && simpleExoPlayer.getCurrentPosition() >= 0; } + /** + * Seeks to the default position of the currently playing + * {@link com.google.android.exoplayer2.Timeline.Window}. Does nothing if the + * {@link #simpleExoPlayer} is not initialized. + *

+ * If the current window is non-live, then this will seek to the start of the window. + * If the window is live but has a buffer length greater than + * {@link #SHORT_LIVESTREAM_CHUNK_LENGTH_MILLIS}, then this will seek to the default + * live edge position through {@link SimpleExoPlayer#seekToDefaultPosition}. + * Otherwise, this will seek to the maximum position possible for the current buffer + * given by {@link SimpleExoPlayer#getDuration}. + * + * @see SimpleExoPlayer#seekToDefaultPosition + * */ public void seekToDefault() { - if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition(); + if (simpleExoPlayer == null) return; + if (isLive() && simpleExoPlayer.getDuration() < SHORT_LIVESTREAM_CHUNK_LENGTH_MILLIS) { + simpleExoPlayer.seekTo(simpleExoPlayer.getDuration()); + } else { + simpleExoPlayer.seekToDefaultPosition(); + } } /*////////////////////////////////////////////////////////////////////////// @@ -1078,9 +1094,9 @@ public abstract class BasePlayer implements private void savePlaybackState() { if (simpleExoPlayer == null || currentInfo == null) return; - if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD && + if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS && simpleExoPlayer.getCurrentPosition() < - simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { + simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) { savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); } } @@ -1165,10 +1181,6 @@ public abstract class BasePlayer implements setPlaybackParameters(speed, getPlaybackPitch()); } - public void setPlaybackPitch(float pitch) { - setPlaybackParameters(getPlaybackSpeed(), pitch); - } - public PlaybackParameters getPlaybackParameters() { final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f); if (simpleExoPlayer == null) return defaultParameters; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 63ac7e8a1..84c7a619b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -7,7 +7,11 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; @@ -203,6 +207,16 @@ public class PlayerHelper { return 60000; } + public static TrackSelection.Factory getQualitySelector(@NonNull final Context context, + @NonNull final BandwidthMeter meter) { + return new AdaptiveTrackSelection.Factory(meter, + AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE, + /*bufferDurationRequiredForQualityIncrease=*/1000, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION); + } + public static boolean isUsingDSP(@NonNull final Context context) { return true; } From 74199c862485cc698a6bc221d513b8fa78b7469e Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 2 Apr 2018 22:11:53 -0700 Subject: [PATCH 11/14] -Designated background player as default media button receiver. -Fixed media button intent causing illegal state exception when sent from external apps. --- app/src/main/AndroidManifest.xml | 6 +++++- .../newpipe/player/BackgroundPlayer.java | 6 +++++- .../player/helper/MediaSessionManager.java | 18 ++++++++++-------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7447c81ed..1e55270be 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,7 +42,11 @@ + android:exported="false"> + + + + Date: Sat, 7 Apr 2018 16:40:38 -0700 Subject: [PATCH 12/14] -Rollbacks the original main player UI to display nav and status bar on click. -Changed system UI color to translucent on Lollipop and above. --- .../newpipe/player/MainVideoPlayer.java | 39 ++++++------------- .../main/res/drawable/player_controls_bg.xml | 2 +- .../res/drawable/player_top_controls_bg.xml | 2 +- .../main/res/layout/activity_main_player.xml | 2 +- app/src/main/res/values/colors.xml | 2 +- 5 files changed, 16 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 48503eda5..2554ef720 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -30,8 +30,10 @@ import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.Settings; +import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; @@ -248,22 +250,6 @@ public final class MainVideoPlayer extends AppCompatActivity // View //////////////////////////////////////////////////////////////////////////*/ - /** - * Prior to Kitkat, hiding system ui causes the player view to be overlaid and require two - * clicks to get rid of that invisible overlay. By showing the system UI on actions/events, - * that overlay is removed and the player view is put to the foreground. - * - * Post Kitkat, navbar and status bar can be pulled out by swiping the edge of - * screen, therefore, we can do nothing or hide the UI on actions/events. - * */ - private void changeSystemUi() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - showSystemUi(); - } else { - hideSystemUi(); - } - } - private void showSystemUi() { if (DEBUG) Log.d(TAG, "showSystemUi() called"); if (playerImpl != null && playerImpl.queueVisible) return; @@ -276,6 +262,14 @@ public final class MainVideoPlayer extends AppCompatActivity } else { visibility = View.STATUS_BAR_VISIBLE; } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + @ColorInt final int systenUiColor = + ActivityCompat.getColor(getApplicationContext(), R.color.video_overlay_color); + getWindow().setStatusBarColor(systenUiColor); + getWindow().setNavigationBarColor(systenUiColor); + } + getWindow().getDecorView().setSystemUiVisibility(visibility); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } @@ -416,15 +410,6 @@ public final class MainVideoPlayer extends AppCompatActivity this.itemsListCloseButton = findViewById(R.id.playQueueClose); this.itemsList = findViewById(R.id.playQueue); - this.windowRootLayout = rootView.findViewById(R.id.playbackWindowRoot); - // Prior to Kitkat, there is no way of setting translucent navbar programmatically. - // Thus, fit system windows is opted instead. - // See https://stackoverflow.com/questions/29069070/completely-transparent-status-bar-and-navigation-bar-on-lollipop - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - windowRootLayout.setFitsSystemWindows(false); - windowRootLayout.invalidate(); - } - titleTextView.setSelected(true); channelTextView.setSelected(true); @@ -732,7 +717,7 @@ public final class MainVideoPlayer extends AppCompatActivity animatePlayButtons(true, 200); }); - changeSystemUi(); + showSystemUi(); getRootView().setKeepScreenOn(false); } @@ -905,7 +890,7 @@ public final class MainVideoPlayer extends AppCompatActivity playerImpl.hideControls(150, 0); } else { playerImpl.showControlsThenHide(); - changeSystemUi(); + showSystemUi(); } return true; } diff --git a/app/src/main/res/drawable/player_controls_bg.xml b/app/src/main/res/drawable/player_controls_bg.xml index 7e1981347..f250e3558 100644 --- a/app/src/main/res/drawable/player_controls_bg.xml +++ b/app/src/main/res/drawable/player_controls_bg.xml @@ -3,5 +3,5 @@ + android:startColor="@color/video_overlay_color"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/player_top_controls_bg.xml b/app/src/main/res/drawable/player_top_controls_bg.xml index f1e8b98fc..ba62ce863 100644 --- a/app/src/main/res/drawable/player_top_controls_bg.xml +++ b/app/src/main/res/drawable/player_top_controls_bg.xml @@ -3,5 +3,5 @@ + android:startColor="@color/video_overlay_color"/> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index d97f1fe72..616f93536 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -129,7 +129,7 @@ android:id="@+id/playbackControlRoot" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="#64000000" + android:background="@color/video_overlay_color" android:visibility="gone" tools:visibility="visible"> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6b43999a0..df2c73ec8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -40,7 +40,7 @@ #e6000000 #EEFFFFFF #ffffff - #66000000 + #64000000 #323232 #ffffff From c9915bba18eab44c82d9236f12a29a993678a257 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 7 Apr 2018 16:45:42 -0700 Subject: [PATCH 13/14] -Reversed special seek logic for short buffer livestreams. -Fixed loader cleaning potentially canceling existing correct loading items. -Updated ExoPlayer to 2.7.3. --- app/build.gradle | 2 +- .../org/schabi/newpipe/player/BasePlayer.java | 22 +--- .../player/playback/MediaSourceManager.java | 105 ++++++++++++------ 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cf4db5ccc..a0b4da510 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,7 +42,7 @@ android { ext { supportLibVersion = '27.1.0' - exoPlayerLibVersion = '2.7.2' + exoPlayerLibVersion = '2.7.3' roomDbLibVersion = '1.0.0' leakCanaryLibVersion = '1.5.4' okHttpLibVersion = '1.5.0' 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 e3d351671..c9b67a7c9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -144,7 +144,6 @@ public abstract class BasePlayer implements protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500; protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds - protected final static int SHORT_LIVESTREAM_CHUNK_LENGTH_MILLIS = 60000; // 1 minute protected CustomTrackSelector trackSelector; protected PlayerDataSource dataSource; @@ -647,7 +646,7 @@ public abstract class BasePlayer implements // Is still synchronizing? seekToDefault(); - } else if (isSynchronizing && presetStartPositionMillis != 0L) { + } else if (isSynchronizing && presetStartPositionMillis > 0L) { if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + "position=[" + presetStartPositionMillis + "]"); // Has another start position? @@ -1033,25 +1032,8 @@ public abstract class BasePlayer implements && simpleExoPlayer.getCurrentPosition() >= 0; } - /** - * Seeks to the default position of the currently playing - * {@link com.google.android.exoplayer2.Timeline.Window}. Does nothing if the - * {@link #simpleExoPlayer} is not initialized. - *

- * If the current window is non-live, then this will seek to the start of the window. - * If the window is live but has a buffer length greater than - * {@link #SHORT_LIVESTREAM_CHUNK_LENGTH_MILLIS}, then this will seek to the default - * live edge position through {@link SimpleExoPlayer#seekToDefaultPosition}. - * Otherwise, this will seek to the maximum position possible for the current buffer - * given by {@link SimpleExoPlayer#getDuration}. - * - * @see SimpleExoPlayer#seekToDefaultPosition - * */ public void seekToDefault() { - if (simpleExoPlayer == null) return; - if (isLive() && simpleExoPlayer.getDuration() < SHORT_LIVESTREAM_CHUNK_LENGTH_MILLIS) { - simpleExoPlayer.seekTo(simpleExoPlayer.getDuration()); - } else { + if (simpleExoPlayer != null) { simpleExoPlayer.seekToDefaultPosition(); } } 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 583c4b8e7..a0f67d398 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 @@ -24,6 +24,7 @@ import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.ReorderEvent; import org.schabi.newpipe.util.ServiceHelper; +import java.util.Collection; import java.util.Collections; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -39,7 +40,8 @@ import io.reactivex.functions.Consumer; import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.subjects.PublishSubject; -import static org.schabi.newpipe.player.mediasource.FailedMediaSource.*; +import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException; +import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; public class MediaSourceManager { @@ -267,6 +269,8 @@ public class MediaSourceManager { } private boolean isPlaybackReady() { + if (playlist.size() != playQueue.size()) return false; + final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex()); if (mediaSource == null) return false; @@ -288,7 +292,7 @@ public class MediaSourceManager { private void maybeUnblock() { if (DEBUG) Log.d(TAG, "maybeUnblock() called."); - if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) { + if (isBlocked.get()) { isBlocked.set(false); playbackListener.onPlaybackUnblock(playlist.getParentMediaSource()); } @@ -299,10 +303,10 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private void maybeSync() { - if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); + if (DEBUG) Log.d(TAG, "maybeSync() called."); final PlayQueueItem currentItem = playQueue.getItem(); - if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return; + if (isBlocked.get() || currentItem == null) return; final Consumer onSuccess = info -> syncInternal(currentItem, info); final Consumer onError = throwable -> syncInternal(currentItem, null); @@ -321,9 +325,11 @@ public class MediaSourceManager { } } - private void maybeSynchronizePlayer() { - maybeUnblock(); - maybeSync(); + private synchronized void maybeSynchronizePlayer() { + if (isPlayQueueReady() && isPlaybackReady()) { + maybeUnblock(); + maybeSync(); + } } /*////////////////////////////////////////////////////////////////////////// @@ -346,37 +352,16 @@ public class MediaSourceManager { debouncedSignal.onNext(System.currentTimeMillis()); } - private void loadImmediate() { + private synchronized void loadImmediate() { if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called"); - // The current item has higher priority - final int currentIndex = playQueue.getIndex(); - final PlayQueueItem currentItem = playQueue.getItem(currentIndex); - if (currentItem == null) return; + final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue, WINDOW_SIZE); + if (itemsToLoad == null) return; - // Evict the items being loaded to free up memory - if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) { - loaderReactor.clear(); - loadingItems.clear(); - } - maybeLoadItem(currentItem); + // Evict the previous items being loaded to free up memory, before start loading new ones + maybeClearLoaders(); - // The rest are just for seamless playback - // Although timeline is not updated prior to the current index, these sources are still - // loaded into the cache for faster retrieval at a potentially later time. - final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); - final int rightLimit = currentIndex + WINDOW_SIZE + 1; - final int rightBound = Math.min(playQueue.size(), rightLimit); - final Set items = new ArraySet<>( - playQueue.getStreams().subList(leftBound,rightBound)); - - // Do a round robin - final int excess = rightLimit - playQueue.size(); - if (excess >= 0) { - items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); - } - items.remove(currentItem); - - for (final PlayQueueItem item : items) { + maybeLoadItem(itemsToLoad.center); + for (final PlayQueueItem item : itemsToLoad.neighbors) { maybeLoadItem(item); } } @@ -476,6 +461,15 @@ public class MediaSourceManager { "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); playlist.invalidate(currentIndex, this::loadImmediate); } + + private void maybeClearLoaders() { + if (DEBUG) Log.d(TAG, "MediaSource - maybeClearLoaders() called."); + if (!loadingItems.contains(playQueue.getItem()) && + loaderReactor.size() > MAXIMUM_LOADER_SIZE) { + loaderReactor.clear(); + loadingItems.clear(); + } + } /*////////////////////////////////////////////////////////////////////////// // MediaSource Playlist Helpers //////////////////////////////////////////////////////////////////////////*/ @@ -493,4 +487,45 @@ public class MediaSourceManager { playlist.expand(); } } + + /*////////////////////////////////////////////////////////////////////////// + // Manager Helpers + //////////////////////////////////////////////////////////////////////////*/ + @Nullable + private static ItemsToLoad getItemsToLoad(@NonNull final PlayQueue playQueue, + final int windowSize) { + // The current item has higher priority + final int currentIndex = playQueue.getIndex(); + final PlayQueueItem currentItem = playQueue.getItem(currentIndex); + if (currentItem == null) return null; + + // The rest are just for seamless playback + // Although timeline is not updated prior to the current index, these sources are still + // loaded into the cache for faster retrieval at a potentially later time. + final int leftBound = Math.max(0, currentIndex - windowSize); + final int rightLimit = currentIndex + windowSize + 1; + final int rightBound = Math.min(playQueue.size(), rightLimit); + final Set neighbors = new ArraySet<>( + playQueue.getStreams().subList(leftBound,rightBound)); + + // Do a round robin + final int excess = rightLimit - playQueue.size(); + if (excess >= 0) { + neighbors.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); + } + neighbors.remove(currentItem); + + return new ItemsToLoad(currentItem, neighbors); + } + + private static class ItemsToLoad { + @NonNull final private PlayQueueItem center; + @NonNull final private Collection neighbors; + + ItemsToLoad(@NonNull final PlayQueueItem center, + @NonNull final Collection neighbors) { + this.center = center; + this.neighbors = neighbors; + } + } } From 42d19d98ad7d60f6c3c9933bddc6350fc983a2a8 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 11 Apr 2018 20:22:54 -0700 Subject: [PATCH 14/14] -Changed media source manager near edge loading to no longer load while player position is not progressing. -Changed main video player to always self-destruct on stop. -Extracted main video player lifecycle states into separate data class. -Fixed play queue in full repeat mode does not load first item after expiring. --- .../newpipe/player/BackgroundPlayer.java | 6 +- .../org/schabi/newpipe/player/BasePlayer.java | 18 ++-- .../newpipe/player/MainVideoPlayer.java | 87 +++++++----------- .../schabi/newpipe/player/PlayerState.java | 88 +++++++++++++++++++ .../schabi/newpipe/player/VideoPlayer.java | 4 +- .../ManagedMediaSourcePlaylist.java | 10 --- .../player/playback/MediaSourceManager.java | 11 ++- .../player/playback/PlaybackListener.java | 8 +- 8 files changed, 150 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/PlayerState.java diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index d7e2f5089..2d990e43e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -164,6 +164,11 @@ public final class BackgroundPlayer extends Service { if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); shouldUpdateOnProgress = on; basePlayerImpl.triggerProgressUpdate(); + if (on) { + basePlayerImpl.startProgressLoop(); + } else { + basePlayerImpl.stopProgressLoop(); + } } /*////////////////////////////////////////////////////////////////////////// @@ -549,7 +554,6 @@ public final class BackgroundPlayer extends Service { super.onPaused(); updateNotification(R.drawable.ic_play_arrow_white); - if (isProgressLoopRunning()) stopProgressLoop(); lockManager.releaseWifiAndCpu(); } 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 c9b67a7c9..5e5518ee9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -177,11 +177,11 @@ public abstract class BasePlayer implements } public void setup() { - if (simpleExoPlayer == null) initPlayer(); + if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true); initListeners(); } - public void initPlayer() { + public void initPlayer(final boolean playOnReady) { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); @@ -199,7 +199,7 @@ public abstract class BasePlayer implements final RenderersFactory renderFactory = new DefaultRenderersFactory(context); simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); simpleExoPlayer.addListener(this); - simpleExoPlayer.setPlayWhenReady(true); + simpleExoPlayer.setPlayWhenReady(playOnReady); simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); audioReactor = new AudioReactor(context, simpleExoPlayer); @@ -237,15 +237,16 @@ public abstract class BasePlayer implements final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); // Good to go... - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch); + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, /*playOnInit=*/true); } protected void initPlayback(@NonNull final PlayQueue queue, @Player.RepeatMode final int repeatMode, final float playbackSpeed, - final float playbackPitch) { + final float playbackPitch, + final boolean playOnReady) { destroyPlayer(); - initPlayer(); + initPlayer(playOnReady); setRepeatMode(repeatMode); setPlaybackParameters(playbackSpeed, playbackPitch); @@ -770,9 +771,10 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ @Override - public boolean isNearPlaybackEdge(final long timeToEndMillis) { + public boolean isApproachingPlaybackEdge(final long timeToEndMillis) { // If live, then not near playback edge - if (simpleExoPlayer == null || isLive()) return false; + // If not playing, then not approaching playback edge + if (simpleExoPlayer == null || isLive() || !isPlaying()) return false; final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); final long currentDurationMillis = simpleExoPlayer.getDuration(); diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 2554ef720..19621593c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -61,7 +61,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemHolder; @@ -97,12 +96,12 @@ public final class MainVideoPlayer extends AppCompatActivity private GestureDetector gestureDetector; - private boolean activityPaused; private VideoPlayerImpl playerImpl; private SharedPreferences defaultPreferences; - @Nullable private StateSaver.SavedState savedState; + @Nullable private PlayerState playerState; + private boolean isInMultiWindow; /*////////////////////////////////////////////////////////////////////////// // Activity LifeCycle @@ -137,8 +136,9 @@ public final class MainVideoPlayer extends AppCompatActivity @Override protected void onRestoreInstanceState(@NonNull Bundle bundle) { + if (DEBUG) Log.d(TAG, "onRestoreInstanceState() called"); super.onRestoreInstanceState(bundle); - savedState = StateSaver.tryToRestore(bundle, this); + StateSaver.tryToRestore(bundle, this); } @Override @@ -150,27 +150,28 @@ public final class MainVideoPlayer extends AppCompatActivity @Override protected void onResume() { - super.onResume(); if (DEBUG) Log.d(TAG, "onResume() called"); - if (isInMultiWindow()) return; - if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() - && !playerImpl.isPlaying()) { - playerImpl.onPlay(); - } - activityPaused = false; + super.onResume(); - if(globalScreenOrientationLocked()) { - boolean lastOrientationWasLandscape - = defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false); + if (globalScreenOrientationLocked()) { + boolean lastOrientationWasLandscape = defaultPreferences.getBoolean( + getString(R.string.last_orientation_landscape_key), false); setLandscape(lastOrientationWasLandscape); } - } - @Override - public void onBackPressed() { - if (DEBUG) Log.d(TAG, "onBackPressed() called"); - super.onBackPressed(); - if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); + // Upon going in or out of multiwindow mode, isInMultiWindow will always be false, + // since the first onResume needs to restore the player. + // Subsequent onResume calls while multiwindow mode remains the same and the player is + // prepared should be ignored. + if (isInMultiWindow) return; + isInMultiWindow = isInMultiWindow(); + + if (playerState != null) { + playerImpl.setPlaybackQuality(playerState.getPlaybackQuality()); + playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(), + playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(), + playerState.wasPlaying()); + } } @Override @@ -183,33 +184,24 @@ public final class MainVideoPlayer extends AppCompatActivity } } - @Override - protected void onPause() { - super.onPause(); - if (DEBUG) Log.d(TAG, "onPause() called"); - if (isInMultiWindow()) return; - if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) { - playerImpl.wasPlaying = playerImpl.isPlaying(); - playerImpl.onPause(); - } - activityPaused = true; - } - @Override protected void onSaveInstanceState(Bundle outState) { + if (DEBUG) Log.d(TAG, "onSaveInstanceState() called"); super.onSaveInstanceState(outState); if (playerImpl == null) return; playerImpl.setRecovery(); - savedState = StateSaver.tryToSave(isChangingConfigurations(), savedState, - outState, this); + playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(), + playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(), + playerImpl.getPlaybackQuality(), playerImpl.isPlaying()); + StateSaver.tryToSave(isChangingConfigurations(), null, outState, this); } @Override - protected void onDestroy() { - super.onDestroy(); - if (DEBUG) Log.d(TAG, "onDestroy() called"); - if (playerImpl != null) playerImpl.destroy(); + protected void onStop() { + if (DEBUG) Log.d(TAG, "onStop() called"); + super.onStop(); + playerImpl.destroy(); } /*////////////////////////////////////////////////////////////////////////// @@ -224,26 +216,13 @@ public final class MainVideoPlayer extends AppCompatActivity @Override public void writeTo(Queue objectsToSave) { if (objectsToSave == null) return; - objectsToSave.add(playerImpl.getPlayQueue()); - objectsToSave.add(playerImpl.getRepeatMode()); - objectsToSave.add(playerImpl.getPlaybackSpeed()); - objectsToSave.add(playerImpl.getPlaybackPitch()); - objectsToSave.add(playerImpl.getPlaybackQuality()); + objectsToSave.add(playerState); } @Override @SuppressWarnings("unchecked") - public void readFrom(@NonNull Queue savedObjects) throws Exception { - @NonNull final PlayQueue queue = (PlayQueue) savedObjects.poll(); - final int repeatMode = (int) savedObjects.poll(); - final float playbackSpeed = (float) savedObjects.poll(); - final float playbackPitch = (float) savedObjects.poll(); - @NonNull final String playbackQuality = (String) savedObjects.poll(); - - playerImpl.setPlaybackQuality(playbackQuality); - playerImpl.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch); - - StateSaver.onDestroy(savedState); + public void readFrom(@NonNull Queue savedObjects) { + playerState = (PlayerState) savedObjects.poll(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java new file mode 100644 index 000000000..6f38ce835 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java @@ -0,0 +1,88 @@ +package org.schabi.newpipe.player; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +import org.schabi.newpipe.playlist.PlayQueue; + +import java.io.Serializable; + +public class PlayerState implements Serializable { + private final static String TAG = "PlayerState"; + + @NonNull private final PlayQueue playQueue; + private final int repeatMode; + private final float playbackSpeed; + private final float playbackPitch; + @Nullable private final String playbackQuality; + private final boolean wasPlaying; + + PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, + final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) { + this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying); + } + + PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, + final float playbackSpeed, final float playbackPitch, + @Nullable final String playbackQuality, final boolean wasPlaying) { + this.playQueue = playQueue; + this.repeatMode = repeatMode; + this.playbackSpeed = playbackSpeed; + this.playbackPitch = playbackPitch; + this.playbackQuality = playbackQuality; + this.wasPlaying = wasPlaying; + } + + /*////////////////////////////////////////////////////////////////////////// + // Serdes + //////////////////////////////////////////////////////////////////////////*/ + + @Nullable + public static PlayerState fromJson(@NonNull final String json) { + try { + return new Gson().fromJson(json, PlayerState.class); + } catch (JsonSyntaxException error) { + Log.e(TAG, "Failed to deserialize PlayerState from json=[" + json + "]", error); + return null; + } + } + + @NonNull + public String toJson() { + return new Gson().toJson(this); + } + + /*////////////////////////////////////////////////////////////////////////// + // Getters + //////////////////////////////////////////////////////////////////////////*/ + + @NonNull + public PlayQueue getPlayQueue() { + return playQueue; + } + + public int getRepeatMode() { + return repeatMode; + } + + public float getPlaybackSpeed() { + return playbackSpeed; + } + + public float getPlaybackPitch() { + return playbackPitch; + } + + @Nullable + public String getPlaybackQuality() { + return playbackQuality; + } + + public boolean wasPlaying() { + return wasPlaying; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index b019ea91e..0e0dca983 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -228,8 +228,8 @@ public abstract class VideoPlayer extends BasePlayer } @Override - public void initPlayer() { - super.initPlayer(); + public void initPlayer(final boolean playOnReady) { + super.initPlayer(playOnReady); // Setup video view simpleExoPlayer.setVideoSurfaceView(surfaceView); 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 index 3cbc75395..310f1062b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java @@ -87,16 +87,6 @@ public class ManagedMediaSourcePlaylist { 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}. 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 a0f67d398..b4236d3c5 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 @@ -24,8 +24,10 @@ import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.ReorderEvent; import org.schabi.newpipe.util.ServiceHelper; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -38,6 +40,7 @@ import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; import io.reactivex.functions.Consumer; import io.reactivex.internal.subscriptions.EmptySubscription; +import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException; @@ -338,12 +341,14 @@ public class MediaSourceManager { private Observable getEdgeIntervalSignal() { return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) - .filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis)); + .filter(ignored -> + playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis)); } private Disposable getDebouncedLoader() { return debouncedSignal.mergeWith(nearEndIntervalSignal) .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.single()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(timestamp -> loadImmediate()); } @@ -352,7 +357,7 @@ public class MediaSourceManager { debouncedSignal.onNext(System.currentTimeMillis()); } - private synchronized void loadImmediate() { + private void loadImmediate() { if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called"); final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue, WINDOW_SIZE); if (itemsToLoad == null) return; @@ -411,7 +416,7 @@ public class MediaSourceManager { final int itemIndex = playQueue.indexOf(item); // Only update the playlist timeline for items at the current index or after. - if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) { + if (isCorrectionNeeded(item)) { if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " + "title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]"); playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer); diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index 34c7702bc..daf58d5dd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -13,13 +13,13 @@ import java.util.List; public interface PlaybackListener { /** - * Called to check if the currently playing stream is close to the end of its playback. - * Implementation should return true when the current playback position is within - * timeToEndMillis or less until its playback completes or transitions. + * Called to check if the currently playing stream is approaching the end of its playback. + * Implementation should return true when the current playback position is progressing within + * timeToEndMillis or less to its playback during. * * May be called at any time. * */ - boolean isNearPlaybackEdge(final long timeToEndMillis); + boolean isApproachingPlaybackEdge(final long timeToEndMillis); /** * Called when the stream at the current queue index is not ready yet.