-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 + "]");
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();
}

View File

@ -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();

View File

@ -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<Object> 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<Object> 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<Object> savedObjects) {
playerState = (PlayerState) savedObjects.poll();
}
/*//////////////////////////////////////////////////////////////////////////

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
public void initPlayer() {
super.initPlayer();
public void initPlayer(final boolean playOnReady) {
super.initPlayer(playOnReady);
// Setup video view
simpleExoPlayer.setVideoSurfaceView(surfaceView);

View File

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

View File

@ -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<Long> 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);

View File

@ -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.