From 42d19d98ad7d60f6c3c9933bddc6350fc983a2a8 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 11 Apr 2018 20:22:54 -0700 Subject: [PATCH] -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.