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 58047639c..73d0b9180 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -68,6 +68,7 @@ import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueAdapter; import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.SerializedCache; import java.io.IOException; @@ -818,22 +819,32 @@ public abstract class BasePlayer implements initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); } - final int currentSourceIndex = playQueue.indexOf(item); - onMetadataChanged(item, info, currentSourceIndex, hasPlayQueueItemChanged); + final int currentPlayQueueIndex = playQueue.indexOf(item); + onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); - // Check if on wrong window if (simpleExoPlayer == null) return; - if (currentSourceIndex != playQueue.getIndex()) { - Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex + - "], queue index=[" + playQueue.getIndex() + "]"); + final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + // Check if on wrong window + if (currentPlayQueueIndex != playQueue.getIndex()) { + Log.e(TAG, "Play Queue may be desynchronized: item " + + "index=[" + currentPlayQueueIndex + "], " + + "queue index=[" + playQueue.getIndex() + "]"); // on metadata changed - } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) { - final long startPos = info != null ? info.start_position : 0; - if (DEBUG) Log.d(TAG, "Rewinding to correct window=[" + currentSourceIndex + "]," + + } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { + final long startPos = info != null ? info.start_position : C.TIME_UNSET; + if (DEBUG) Log.d(TAG, "Rewinding to correct" + + " window=[" + currentPlayQueueIndex + "]," + " at=[" + getTimeString((int)startPos) + "]," + " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); - simpleExoPlayer.seekTo(currentSourceIndex, startPos); + simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); + } + + // when starting playback on the last item, maybe auto queue + if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && + PlayerHelper.isAutoQueueEnabled(context)) { + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); + if (autoQueue != null) playQueue.append(autoQueue.getStreams()); } } 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 0bfdcd32a..146b511c4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -129,6 +129,7 @@ public final class MainVideoPlayer extends Activity { super.onSaveInstanceState(outState); if (this.playerImpl == null) return; + playerImpl.setRecovery(); final Intent intent = NavigationHelper.getPlayerIntent( getApplicationContext(), this.getClass(), @@ -156,31 +157,27 @@ public final class MainVideoPlayer extends Activity { } @Override - protected void onStop() { - super.onStop(); - if (DEBUG) Log.d(TAG, "onStop() called"); - activityPaused = true; + protected void onPause() { + super.onPause(); + if (DEBUG) Log.d(TAG, "onPause() called"); - if (playerImpl.getPlayer() != null) { - playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); - playerImpl.setRecovery(); - playerImpl.destroyPlayer(); + if (playerImpl.getPlayer() != null && playerImpl.isPlaying() && !activityPaused) { + playerImpl.wasPlaying = true; + playerImpl.onVideoPlayPause(); } + activityPaused = true; } @Override protected void onResume() { super.onResume(); if (DEBUG) Log.d(TAG, "onResume() called"); - if (activityPaused) { - playerImpl.initPlayer(); - playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white); - - playerImpl.getPlayer().setPlayWhenReady(playerImpl.wasPlaying); - playerImpl.initPlayback(playerImpl.playQueue); - - activityPaused = false; + if (playerImpl.getPlayer() != null && playerImpl.wasPlaying() + && !playerImpl.isPlaying() && activityPaused) { + playerImpl.onVideoPlayPause(); } + activityPaused = false; + if(globalScreenOrientationLocked()) { boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false); @@ -873,6 +870,13 @@ public final class MainVideoPlayer extends Activity { return true; } + @Override + public boolean onDown(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); + + return super.onDown(e); + } + private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; 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 553163d21..87b0f701f 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 @@ -4,22 +4,33 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.Subtitles; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.SubtitlesFormat; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.playlist.SinglePlayQueue; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; import java.util.Formatter; +import java.util.HashSet; +import java.util.List; import java.util.Locale; +import java.util.Set; import javax.annotation.Nonnull; @@ -76,6 +87,7 @@ public class PlayerHelper { return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); } + @NonNull public static String resizeTypeOf(@NonNull final Context context, @AspectRatioFrameLayout.ResizeMode final int resizeMode) { switch (resizeMode) { @@ -86,14 +98,58 @@ public class PlayerHelper { } } + @NonNull public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) { return info.getUrl() + video.getResolution() + video.getFormat().getName(); } + @NonNull public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) { return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName(); } + /** + * Given a {@link StreamInfo} and the existing queue items, provide the + * {@link SinglePlayQueue} consisting of the next video for auto queuing. + *

+ * This method detects and prevents cycle by naively checking if a + * candidate next video's url already exists in the existing items. + *

+ * To select the next video, {@link StreamInfo#getNextVideo()} is first + * checked. If it is nonnull and is not part of the existing items, then + * it will be used as the next video. Otherwise, an random item with + * non-repeating url will be selected from the {@link StreamInfo#getRelatedStreams()}. + * */ + @Nullable + public static PlayQueue autoQueueOf(@NonNull final StreamInfo info, + @NonNull final List existingItems) { + Set urls = new HashSet<>(existingItems.size()); + for (final PlayQueueItem item : existingItems) { + urls.add(item.getUrl()); + } + + final StreamInfoItem nextVideo = info.getNextVideo(); + if (nextVideo != null && !urls.contains(nextVideo.getUrl())) { + return new SinglePlayQueue(nextVideo); + } + + final List relatedItems = info.getRelatedStreams(); + if (relatedItems == null) return null; + + List autoQueueItems = new ArrayList<>(); + for (final InfoItem item : info.getRelatedStreams()) { + if (item instanceof StreamInfoItem && !urls.contains(item.getUrl())) { + autoQueueItems.add((StreamInfoItem) item); + } + } + Collections.shuffle(autoQueueItems); + return autoQueueItems.isEmpty() ? null : new SinglePlayQueue(autoQueueItems.get(0)); + } + + //////////////////////////////////////////////////////////////////////////// + // Settings Resolution + //////////////////////////////////////////////////////////////////////////// + public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { return isResumeAfterAudioFocusGain(context, false); } @@ -110,6 +166,10 @@ public class PlayerHelper { return isRememberingPopupDimensions(context, true); } + public static boolean isAutoQueueEnabled(@NonNull final Context context) { + return isAutoQueueEnabled(context, false); + } + @NonNull public static SeekParameters getSeekParameters(@NonNull final Context context) { return isUsingInexactSeek(context, false) ? @@ -187,4 +247,8 @@ public class PlayerHelper { private static boolean isUsingInexactSeek(@NonNull final Context context, final boolean b) { return getPreferences(context).getBoolean(context.getString(R.string.use_inexact_seek_key), b); } + + private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) { + return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b); + } } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index c207ed712..a897aa185 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -21,6 +21,7 @@ resume_on_audio_focus_gain popup_remember_size_pos_key use_inexact_seek_key + auto_queue_key default_resolution 360p diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a8123bde..d875036e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,6 +74,8 @@ Remember last size and position of popup Use fast inexact seek Inexact seek allows the player to seek to positions faster with reduced precision + Auto-queue next stream + Automatically append a related stream when playback starts on the last stream in play queue. Player gesture controls Use gestures to control the brightness and volume of the player Search suggestions diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index 20099d5c0..c8c1efb12 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -30,6 +30,13 @@ android:key="@string/show_search_suggestions_key" android:summary="@string/show_search_suggestions_summary" android:title="@string/show_search_suggestions_title"/> + + +