-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.
This commit is contained in:
John Zhen Mo 2018-04-11 20:22:54 -07:00
parent c9915bba18
commit 42d19d98ad
8 changed files with 150 additions and 82 deletions

View file

@ -164,6 +164,11 @@ public final class BackgroundPlayer extends Service {
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
shouldUpdateOnProgress = on; shouldUpdateOnProgress = on;
basePlayerImpl.triggerProgressUpdate(); basePlayerImpl.triggerProgressUpdate();
if (on) {
basePlayerImpl.startProgressLoop();
} else {
basePlayerImpl.stopProgressLoop();
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -549,7 +554,6 @@ public final class BackgroundPlayer extends Service {
super.onPaused(); super.onPaused();
updateNotification(R.drawable.ic_play_arrow_white); updateNotification(R.drawable.ic_play_arrow_white);
if (isProgressLoopRunning()) stopProgressLoop();
lockManager.releaseWifiAndCpu(); lockManager.releaseWifiAndCpu();
} }

View file

@ -177,11 +177,11 @@ public abstract class BasePlayer implements
} }
public void setup() { public void setup() {
if (simpleExoPlayer == null) initPlayer(); if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true);
initListeners(); initListeners();
} }
public void initPlayer() { public void initPlayer(final boolean playOnReady) {
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
@ -199,7 +199,7 @@ public abstract class BasePlayer implements
final RenderersFactory renderFactory = new DefaultRenderersFactory(context); final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
simpleExoPlayer.addListener(this); simpleExoPlayer.addListener(this);
simpleExoPlayer.setPlayWhenReady(true); simpleExoPlayer.setPlayWhenReady(playOnReady);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
audioReactor = new AudioReactor(context, simpleExoPlayer); audioReactor = new AudioReactor(context, simpleExoPlayer);
@ -237,15 +237,16 @@ public abstract class BasePlayer implements
final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch());
// Good to go... // Good to go...
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch); initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, /*playOnInit=*/true);
} }
protected void initPlayback(@NonNull final PlayQueue queue, protected void initPlayback(@NonNull final PlayQueue queue,
@Player.RepeatMode final int repeatMode, @Player.RepeatMode final int repeatMode,
final float playbackSpeed, final float playbackSpeed,
final float playbackPitch) { final float playbackPitch,
final boolean playOnReady) {
destroyPlayer(); destroyPlayer();
initPlayer(); initPlayer(playOnReady);
setRepeatMode(repeatMode); setRepeatMode(repeatMode);
setPlaybackParameters(playbackSpeed, playbackPitch); setPlaybackParameters(playbackSpeed, playbackPitch);
@ -770,9 +771,10 @@ public abstract class BasePlayer implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public boolean isNearPlaybackEdge(final long timeToEndMillis) { public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
// If live, then not near playback edge // 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 currentPositionMillis = simpleExoPlayer.getCurrentPosition();
final long currentDurationMillis = simpleExoPlayer.getDuration(); final long currentDurationMillis = simpleExoPlayer.getDuration();

View file

@ -61,7 +61,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder; import org.schabi.newpipe.playlist.PlayQueueItemHolder;
@ -97,12 +96,12 @@ public final class MainVideoPlayer extends AppCompatActivity
private GestureDetector gestureDetector; private GestureDetector gestureDetector;
private boolean activityPaused;
private VideoPlayerImpl playerImpl; private VideoPlayerImpl playerImpl;
private SharedPreferences defaultPreferences; private SharedPreferences defaultPreferences;
@Nullable private StateSaver.SavedState savedState; @Nullable private PlayerState playerState;
private boolean isInMultiWindow;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Activity LifeCycle // Activity LifeCycle
@ -137,8 +136,9 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override @Override
protected void onRestoreInstanceState(@NonNull Bundle bundle) { protected void onRestoreInstanceState(@NonNull Bundle bundle) {
if (DEBUG) Log.d(TAG, "onRestoreInstanceState() called");
super.onRestoreInstanceState(bundle); super.onRestoreInstanceState(bundle);
savedState = StateSaver.tryToRestore(bundle, this); StateSaver.tryToRestore(bundle, this);
} }
@Override @Override
@ -150,27 +150,28 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume();
if (DEBUG) Log.d(TAG, "onResume() called"); if (DEBUG) Log.d(TAG, "onResume() called");
if (isInMultiWindow()) return; super.onResume();
if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying()
&& !playerImpl.isPlaying()) {
playerImpl.onPlay();
}
activityPaused = false;
if (globalScreenOrientationLocked()) { if (globalScreenOrientationLocked()) {
boolean lastOrientationWasLandscape boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
= defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false); getString(R.string.last_orientation_landscape_key), false);
setLandscape(lastOrientationWasLandscape); setLandscape(lastOrientationWasLandscape);
} }
}
@Override // Upon going in or out of multiwindow mode, isInMultiWindow will always be false,
public void onBackPressed() { // since the first onResume needs to restore the player.
if (DEBUG) Log.d(TAG, "onBackPressed() called"); // Subsequent onResume calls while multiwindow mode remains the same and the player is
super.onBackPressed(); // prepared should be ignored.
if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); 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 @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 @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called");
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
if (playerImpl == null) return; if (playerImpl == null) return;
playerImpl.setRecovery(); playerImpl.setRecovery();
savedState = StateSaver.tryToSave(isChangingConfigurations(), savedState, playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
outState, this); playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
playerImpl.getPlaybackQuality(), playerImpl.isPlaying());
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
} }
@Override @Override
protected void onDestroy() { protected void onStop() {
super.onDestroy(); if (DEBUG) Log.d(TAG, "onStop() called");
if (DEBUG) Log.d(TAG, "onDestroy() called"); super.onStop();
if (playerImpl != null) playerImpl.destroy(); playerImpl.destroy();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -224,26 +216,13 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override @Override
public void writeTo(Queue<Object> objectsToSave) { public void writeTo(Queue<Object> objectsToSave) {
if (objectsToSave == null) return; if (objectsToSave == null) return;
objectsToSave.add(playerImpl.getPlayQueue()); objectsToSave.add(playerState);
objectsToSave.add(playerImpl.getRepeatMode());
objectsToSave.add(playerImpl.getPlaybackSpeed());
objectsToSave.add(playerImpl.getPlaybackPitch());
objectsToSave.add(playerImpl.getPlaybackQuality());
} }
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception { public void readFrom(@NonNull Queue<Object> savedObjects) {
@NonNull final PlayQueue queue = (PlayQueue) savedObjects.poll(); playerState = (PlayerState) 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);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////

View file

@ -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;
}
}

View file

@ -228,8 +228,8 @@ public abstract class VideoPlayer extends BasePlayer
} }
@Override @Override
public void initPlayer() { public void initPlayer(final boolean playOnReady) {
super.initPlayer(); super.initPlayer(playOnReady);
// Setup video view // Setup video view
simpleExoPlayer.setVideoSurfaceView(surfaceView); simpleExoPlayer.setVideoSurfaceView(surfaceView);

View file

@ -87,16 +87,6 @@ public class ManagedMediaSourcePlaylist {
internalSource.moveMediaSource(source, target); 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 * Invalidates the {@link ManagedMediaSource} at the given index by replacing it
* with a {@link PlaceholderMediaSource}. * with a {@link PlaceholderMediaSource}.

View file

@ -24,8 +24,10 @@ import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent; import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -38,6 +40,7 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.SerialDisposable; import io.reactivex.disposables.SerialDisposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.internal.subscriptions.EmptySubscription;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException; import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
@ -338,12 +341,14 @@ public class MediaSourceManager {
private Observable<Long> getEdgeIntervalSignal() { private Observable<Long> getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
.filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis)); .filter(ignored ->
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
} }
private Disposable getDebouncedLoader() { private Disposable getDebouncedLoader() {
return debouncedSignal.mergeWith(nearEndIntervalSignal) return debouncedSignal.mergeWith(nearEndIntervalSignal)
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate()); .subscribe(timestamp -> loadImmediate());
} }
@ -352,7 +357,7 @@ public class MediaSourceManager {
debouncedSignal.onNext(System.currentTimeMillis()); debouncedSignal.onNext(System.currentTimeMillis());
} }
private synchronized void loadImmediate() { private void loadImmediate() {
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called"); if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue, WINDOW_SIZE); final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue, WINDOW_SIZE);
if (itemsToLoad == null) return; if (itemsToLoad == null) return;
@ -411,7 +416,7 @@ public class MediaSourceManager {
final int itemIndex = playQueue.indexOf(item); final int itemIndex = playQueue.indexOf(item);
// Only update the playlist timeline for items at the current index or after. // 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 " + if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " +
"title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]"); "title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer); playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer);

View file

@ -13,13 +13,13 @@ import java.util.List;
public interface PlaybackListener { public interface PlaybackListener {
/** /**
* Called to check if the currently playing stream is close to the end of its playback. * 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 within * Implementation should return true when the current playback position is progressing within
* timeToEndMillis or less until its playback completes or transitions. * timeToEndMillis or less to its playback during.
* *
* May be called at any time. * 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. * Called when the stream at the current queue index is not ready yet.