From b4668367c6640986cd78f2ac06436520fddb9399 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 28 Feb 2018 17:45:05 -0800 Subject: [PATCH] -Added better assertions and documentations to new mechanism in MediaSourceManager. -Modified LoadController to allow fast playback start and increased buffer zigzag window. -Removed unnecessary loading on timeline changes. -Changed select message in MediaSourceManager to cause immediate load. -Reduced default expiration time in MediaSourceManager. -Fixed main video player not showing end time on audio-only streams. -Fixed live stream has player view disabled after transitioning from audio stream. -Fixed inconsistent progress bar height between live and non-live video on main player. --- app/build.gradle | 2 +- .../org/schabi/newpipe/player/BasePlayer.java | 20 ++-- .../schabi/newpipe/player/VideoPlayer.java | 5 + .../newpipe/player/helper/LoadController.java | 29 +++-- .../newpipe/player/helper/PlayerHelper.java | 23 +++- .../player/mediasource/FailedMediaSource.java | 2 +- .../player/playback/MediaSourceManager.java | 104 +++++++++++------- .../main/res/layout/activity_main_player.xml | 4 +- 8 files changed, 117 insertions(+), 72 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ba6406d4b..bfc22c76b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.karyogamy:NewPipeExtractor:837dbd6b86' + implementation 'com.github.karyogamy:NewPipeExtractor:4cf4ee394f' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' 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 1854e9a01..a449b4a36 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -511,15 +511,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen @Override public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); - - switch (reason) { - case Player.TIMELINE_CHANGE_REASON_PREPARED: - case Player.TIMELINE_CHANGE_REASON_RESET: - case Player.TIMELINE_CHANGE_REASON_DYNAMIC: - default: - if (playbackManager != null) playbackManager.load(); - break; - } } @Override @@ -654,6 +645,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } else { playQueue.offsetIndex(+1); } + playbackManager.load(); break; case DISCONTINUITY_REASON_SEEK: case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: @@ -661,7 +653,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen default: break; } - playbackManager.load(); } @Override @@ -724,8 +715,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen "], queue index=[" + playQueue.getIndex() + "]"); } else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) { final long startPos = info != null ? info.start_position : 0; - if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + - " at: " + getTimeString((int)startPos)); + if (DEBUG) Log.d(TAG, "Rewinding to correct window=[" + currentSourceIndex + "]," + + " at=[" + getTimeString((int)startPos) + "]," + + " from=[" + simpleExoPlayer.getCurrentPeriodIndex() + "]."); simpleExoPlayer.seekTo(currentSourceIndex, startPos); } @@ -974,7 +966,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } public boolean isPlaying() { - return simpleExoPlayer.getPlaybackState() == Player.STATE_READY && simpleExoPlayer.getPlayWhenReady(); + final int state = simpleExoPlayer.getPlaybackState(); + return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) + && simpleExoPlayer.getPlayWhenReady(); } public int getRepeatMode() { 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 eca6415f6..5a7a9a462 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -339,11 +339,16 @@ public abstract class VideoPlayer extends BasePlayer switch (streamType) { case AUDIO_STREAM: surfaceView.setVisibility(View.GONE); + playbackEndTime.setVisibility(View.VISIBLE); break; case AUDIO_LIVE_STREAM: surfaceView.setVisibility(View.GONE); + playbackLiveSync.setVisibility(View.VISIBLE); + break; + case LIVE_STREAM: + surfaceView.setVisibility(View.VISIBLE); playbackLiveSync.setVisibility(View.VISIBLE); break; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index 15668be90..7670deb98 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -11,7 +11,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; -import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS; import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES; @@ -19,6 +18,7 @@ public class LoadController implements LoadControl { public static final String TAG = "LoadController"; + private final long initialPlaybackBufferUs; private final LoadControl internalLoadControl; /*////////////////////////////////////////////////////////////////////////// @@ -26,18 +26,24 @@ public class LoadController implements LoadControl { //////////////////////////////////////////////////////////////////////////*/ public LoadController(final Context context) { - this(PlayerHelper.getMinBufferMs(context), - PlayerHelper.getMaxBufferMs(context), - PlayerHelper.getBufferForPlaybackMs(context)); + this(PlayerHelper.getPlaybackStartBufferMs(context), + PlayerHelper.getPlaybackMinimumBufferMs(context), + PlayerHelper.getPlaybackOptimalBufferMs(context)); } - private LoadController(final int minBufferMs, final int maxBufferMs, - final int bufferForPlaybackMs) { + private LoadController(final int initialPlaybackBufferMs, + final int minimumPlaybackbufferMs, + final int optimalPlaybackBufferMs) { + this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000; + final DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); - internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, - bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + internalLoadControl = new DefaultLoadControl(allocator, + /*minBufferMs=*/minimumPlaybackbufferMs, + /*maxBufferMs=*/optimalPlaybackBufferMs, + /*bufferForPlaybackMs=*/initialPlaybackBufferMs, + /*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs, DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } @@ -89,7 +95,10 @@ public class LoadController implements LoadControl { @Override public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { - return internalLoadControl.shouldStartPlayback(bufferedDurationUs, playbackSpeed, - rebuffering); + final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >= + this.initialPlaybackBufferUs * playbackSpeed; + final boolean isInternalStartingPlayback = internalLoadControl.shouldStartPlayback( + bufferedDurationUs, playbackSpeed, rebuffering); + return isInitialPlaybackBufferFilled || isInternalStartingPlayback; } } 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 6e2ff0ac9..813c69c22 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 @@ -117,16 +117,27 @@ public class PlayerHelper { return 512 * 1024L; } - public static int getMinBufferMs(@NonNull final Context context) { - return 15000; + /** + * Returns the number of milliseconds the player buffers for before starting playback. + * */ + public static int getPlaybackStartBufferMs(@NonNull final Context context) { + return 500; } - public static int getMaxBufferMs(@NonNull final Context context) { - return 30000; + /** + * Returns the minimum number of milliseconds the player always buffers to after starting + * playback. + * */ + public static int getPlaybackMinimumBufferMs(@NonNull final Context context) { + return 25000; } - public static int getBufferForPlaybackMs(@NonNull final Context context) { - return 2500; + /** + * Returns the maximum/optimal number of milliseconds the player will buffer to once the buffer + * hits the point of {@link #getPlaybackMinimumBufferMs(Context)}. + * */ + public static int getPlaybackOptimalBufferMs(@NonNull final Context context) { + return 60000; } public static boolean isUsingDSP(@NonNull final Context context) { 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 d88385d2d..d07baf2a7 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 @@ -12,7 +12,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem; import java.io.IOException; public class FailedMediaSource implements ManagedMediaSource { - private final String TAG = "ManagedMediaSource@" + Integer.toHexString(hashCode()); + private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final PlayQueueItem playQueueItem; private final Throwable error; 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 0f30169a7..8f91e53c2 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 @@ -1,5 +1,6 @@ package org.schabi.newpipe.player.playback; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -28,7 +29,6 @@ import java.util.concurrent.TimeUnit; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.annotations.NonNull; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; @@ -38,23 +38,26 @@ import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; public class MediaSourceManager { - private final static String TAG = "MediaSourceManager"; + @NonNull private final static String TAG = "MediaSourceManager"; // WINDOW_SIZE determines how many streams AFTER the current stream should be loaded. // The default value (1) ensures seamless playback under typical network settings. private final static int WINDOW_SIZE = 1; - private final PlaybackListener playbackListener; - private final PlayQueue playQueue; + @NonNull private final PlaybackListener playbackListener; + @NonNull private final PlayQueue playQueue; + + // Once a MediaSource item has been detected to be expired, the manager will immediately + // trigger a reload on the associated PlayQueueItem, which may disrupt playback, + // if the item is being played private final long expirationTimeMillis; - private final TimeUnit expirationTimeUnit; // Process only the last load order when receiving a stream of load orders (lessens I/O) // The higher it is, the less loading occurs during rapid noncritical timeline changes // Not recommended to go below 100ms private final long loadDebounceMillis; - private final PublishSubject debouncedLoadSignal; - private final Disposable debouncedLoader; + @NonNull private final Disposable debouncedLoader; + @NonNull private final PublishSubject debouncedSignal; private DynamicConcatenatingMediaSource sources; @@ -71,23 +74,20 @@ public class MediaSourceManager { @NonNull final PlayQueue playQueue) { this(listener, playQueue, /*loadDebounceMillis=*/400L, - /*expirationTimeMillis=*/2, - /*expirationTimeUnit=*/TimeUnit.HOURS); + /*expirationTimeMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES)); } private MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue, final long loadDebounceMillis, - final long expirationTimeMillis, - @NonNull final TimeUnit expirationTimeUnit) { + final long expirationTimeMillis) { this.playbackListener = listener; this.playQueue = playQueue; this.loadDebounceMillis = loadDebounceMillis; this.expirationTimeMillis = expirationTimeMillis; - this.expirationTimeUnit = expirationTimeUnit; this.loaderReactor = new CompositeDisposable(); - this.debouncedLoadSignal = PublishSubject.create(); + this.debouncedSignal = PublishSubject.create(); this.debouncedLoader = getDebouncedLoader(); this.sources = new DynamicConcatenatingMediaSource(); @@ -109,8 +109,11 @@ public class MediaSourceManager { * Dispose the manager and releases all message buses and loaders. * */ public void dispose() { - if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete(); - if (debouncedLoader != null) debouncedLoader.dispose(); + if (DEBUG) Log.d(TAG, "dispose() called."); + + debouncedSignal.onComplete(); + debouncedLoader.dispose(); + if (playQueueReactor != null) playQueueReactor.cancel(); if (loaderReactor != null) loaderReactor.dispose(); if (syncReactor != null) syncReactor.dispose(); @@ -129,6 +132,7 @@ public class MediaSourceManager { * Unblocks the player once the item at the current index is loaded. * */ public void load() { + if (DEBUG) Log.d(TAG, "load() called."); loadDebounced(); } @@ -139,6 +143,8 @@ public class MediaSourceManager { * through {@link #load() load}. * */ public void reset() { + if (DEBUG) Log.d(TAG, "reset() called."); + tryBlock(); syncedItem = null; @@ -205,11 +211,11 @@ public class MediaSourceManager { case INIT: case REORDER: case ERROR: + case SELECT: loadImmediate(); // low frequency, critical events break; case APPEND: case REMOVE: - case SELECT: case MOVE: case RECOVERY: default: @@ -229,16 +235,12 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private boolean isPlayQueueReady() { - if (playQueue == null) return false; - final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > WINDOW_SIZE; return playQueue.isComplete() || isWindowLoaded; } private boolean isPlaybackReady() { - if (sources == null || playQueue == null || sources.getSize() != playQueue.size()) { - return false; - } + if (sources == null || sources.getSize() != playQueue.size()) return false; final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); final PlayQueueItem playQueueItem = playQueue.getItem(); @@ -252,6 +254,8 @@ public class MediaSourceManager { } private void tryBlock() { + if (DEBUG) Log.d(TAG, "tryBlock() called."); + if (isBlocked) return; playbackListener.block(); @@ -261,6 +265,8 @@ public class MediaSourceManager { } private void tryUnblock() { + if (DEBUG) Log.d(TAG, "tryUnblock() called."); + if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) { isBlocked = false; playbackListener.unblock(sources); @@ -272,6 +278,8 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private void sync() { + if (DEBUG) Log.d(TAG, "sync() called."); + final PlayQueueItem currentItem = playQueue.getItem(); if (isBlocked || currentItem == null) return; @@ -289,7 +297,6 @@ public class MediaSourceManager { private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { - if (playQueue == null || playbackListener == null) return; // Ensure the current item is up to date with the play queue if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { playbackListener.sync(syncedItem, info); @@ -301,14 +308,14 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private Disposable getDebouncedLoader() { - return debouncedLoadSignal + return debouncedSignal .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(timestamp -> loadImmediate()); } private void loadDebounced() { - debouncedLoadSignal.onNext(System.currentTimeMillis()); + debouncedSignal.onNext(System.currentTimeMillis()); } private void loadImmediate() { @@ -316,7 +323,7 @@ public class MediaSourceManager { final int currentIndex = playQueue.getIndex(); final PlayQueueItem currentItem = playQueue.getItem(currentIndex); if (currentItem == null) return; - loadItem(currentItem); + maybeLoadItem(currentItem); // The rest are just for seamless playback final int leftBound = currentIndex + 1; @@ -331,10 +338,14 @@ public class MediaSourceManager { items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); } - for (final PlayQueueItem item: items) loadItem(item); + for (final PlayQueueItem item : items) { + maybeLoadItem(item); + } } - private void loadItem(@Nullable final PlayQueueItem item) { + private void maybeLoadItem(@Nullable final PlayQueueItem item) { + if (DEBUG) Log.d(TAG, "maybeLoadItem() called."); + if (sources == null || item == null) return; final int index = playQueue.indexOf(item); @@ -368,11 +379,6 @@ public class MediaSourceManager { private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { return stream.getStream().map(streamInfo -> { - if (playbackListener == null) { - return new FailedMediaSource(stream, new IllegalStateException( - "MediaSourceManager playback listener unavailable")); - } - final MediaSource source = playbackListener.sourceOf(stream, streamInfo); if (source == null) { final Exception exception = new IllegalStateException( @@ -384,21 +390,34 @@ public class MediaSourceManager { return new FailedMediaSource(stream, new IllegalStateException(exception)); } - final long expiration = System.currentTimeMillis() + - TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit); + final long expiration = System.currentTimeMillis() + expirationTimeMillis; return new LoadedMediaSource(source, stream, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); } + /** + * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource} + * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback + * readiness or playlist desynchronization. + *

+ * If the given {@link PlayQueueItem} is currently being played and is already loaded, + * then correction is not only needed if the playlist is desynchronized. Otherwise, the + * check depends on the status (e.g. expiration or placeholder) of the + * {@link ManagedMediaSource}. + * */ private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { - if (playQueue == null || sources == null) return false; + if (sources == null) return false; final int index = playQueue.indexOf(item); if (index == -1 || index >= sources.getSize()) return false; - final MediaSource mediaSource = sources.getMediaSource(index); - return !(mediaSource instanceof ManagedMediaSource) || - ((ManagedMediaSource) mediaSource).canReplace(item); + final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index); + + if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) { + return item != ((LoadedMediaSource) mediaSource).getStream(); + } else { + return mediaSource.canReplace(item); + } } /*////////////////////////////////////////////////////////////////////////// @@ -406,11 +425,14 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private void resetSources() { + if (DEBUG) Log.d(TAG, "resetSources() called."); + if (this.sources != null) this.sources.releaseSource(); this.sources = new DynamicConcatenatingMediaSource(); } private void populateSources() { + if (DEBUG) Log.d(TAG, "populateSources() called."); if (sources == null || sources.getSize() >= playQueue.size()) return; for (int index = sources.getSize() - 1; index < playQueue.size(); index++) { @@ -462,8 +484,12 @@ public class MediaSourceManager { * 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 cause desynchronization + * on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}. * */ - private void update(final int index, final MediaSource source) { + private synchronized void update(final int index, final MediaSource source) { if (sources == null) return; if (index < 0 || index >= sources.getSize()) return; diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index be778e7c8..9abe43715 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -401,8 +401,8 @@