-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.
This commit is contained in:
John Zhen Mo 2018-02-28 17:45:05 -08:00
parent 77da40e507
commit b4668367c6
8 changed files with 117 additions and 72 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -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<Long> debouncedLoadSignal;
private final Disposable debouncedLoader;
@NonNull private final Disposable debouncedLoader;
@NonNull private final PublishSubject<Long> 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<ManagedMediaSource> 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.
* <br><br>
* 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.
* <br><br>
* 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;

View File

@ -401,8 +401,8 @@
<TextView
android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/live_sync"
android:textColor="@android:color/white"
android:visibility="gone"