mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-01-11 09:50:32 +00:00
-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:
parent
77da40e507
commit
b4668367c6
@ -55,7 +55,7 @@ dependencies {
|
|||||||
exclude module: 'support-annotations'
|
exclude module: 'support-annotations'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'com.github.karyogamy:NewPipeExtractor:837dbd6b86'
|
implementation 'com.github.karyogamy:NewPipeExtractor:4cf4ee394f'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||||
|
@ -511,15 +511,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
|
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
|
||||||
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
|
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
|
@Override
|
||||||
@ -654,6 +645,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||||||
} else {
|
} else {
|
||||||
playQueue.offsetIndex(+1);
|
playQueue.offsetIndex(+1);
|
||||||
}
|
}
|
||||||
|
playbackManager.load();
|
||||||
break;
|
break;
|
||||||
case DISCONTINUITY_REASON_SEEK:
|
case DISCONTINUITY_REASON_SEEK:
|
||||||
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||||
@ -661,7 +653,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
playbackManager.load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -724,8 +715,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||||||
"], queue index=[" + playQueue.getIndex() + "]");
|
"], queue index=[" + playQueue.getIndex() + "]");
|
||||||
} else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) {
|
} else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) {
|
||||||
final long startPos = info != null ? info.start_position : 0;
|
final long startPos = info != null ? info.start_position : 0;
|
||||||
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex +
|
if (DEBUG) Log.d(TAG, "Rewinding to correct window=[" + currentSourceIndex + "]," +
|
||||||
" at: " + getTimeString((int)startPos));
|
" at=[" + getTimeString((int)startPos) + "]," +
|
||||||
|
" from=[" + simpleExoPlayer.getCurrentPeriodIndex() + "].");
|
||||||
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -974,7 +966,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPlaying() {
|
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() {
|
public int getRepeatMode() {
|
||||||
|
@ -339,11 +339,16 @@ public abstract class VideoPlayer extends BasePlayer
|
|||||||
switch (streamType) {
|
switch (streamType) {
|
||||||
case AUDIO_STREAM:
|
case AUDIO_STREAM:
|
||||||
surfaceView.setVisibility(View.GONE);
|
surfaceView.setVisibility(View.GONE);
|
||||||
|
playbackEndTime.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AUDIO_LIVE_STREAM:
|
case AUDIO_LIVE_STREAM:
|
||||||
surfaceView.setVisibility(View.GONE);
|
surfaceView.setVisibility(View.GONE);
|
||||||
|
playbackLiveSync.setVisibility(View.VISIBLE);
|
||||||
|
break;
|
||||||
|
|
||||||
case LIVE_STREAM:
|
case LIVE_STREAM:
|
||||||
|
surfaceView.setVisibility(View.VISIBLE);
|
||||||
playbackLiveSync.setVisibility(View.VISIBLE);
|
playbackLiveSync.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
|||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
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_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS;
|
||||||
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES;
|
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";
|
public static final String TAG = "LoadController";
|
||||||
|
|
||||||
|
private final long initialPlaybackBufferUs;
|
||||||
private final LoadControl internalLoadControl;
|
private final LoadControl internalLoadControl;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -26,18 +26,24 @@ public class LoadController implements LoadControl {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public LoadController(final Context context) {
|
public LoadController(final Context context) {
|
||||||
this(PlayerHelper.getMinBufferMs(context),
|
this(PlayerHelper.getPlaybackStartBufferMs(context),
|
||||||
PlayerHelper.getMaxBufferMs(context),
|
PlayerHelper.getPlaybackMinimumBufferMs(context),
|
||||||
PlayerHelper.getBufferForPlaybackMs(context));
|
PlayerHelper.getPlaybackOptimalBufferMs(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoadController(final int minBufferMs, final int maxBufferMs,
|
private LoadController(final int initialPlaybackBufferMs,
|
||||||
final int bufferForPlaybackMs) {
|
final int minimumPlaybackbufferMs,
|
||||||
|
final int optimalPlaybackBufferMs) {
|
||||||
|
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||||
|
|
||||||
final DefaultAllocator allocator = new DefaultAllocator(true,
|
final DefaultAllocator allocator = new DefaultAllocator(true,
|
||||||
C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||||
|
|
||||||
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs,
|
internalLoadControl = new DefaultLoadControl(allocator,
|
||||||
bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
|
/*minBufferMs=*/minimumPlaybackbufferMs,
|
||||||
|
/*maxBufferMs=*/optimalPlaybackBufferMs,
|
||||||
|
/*bufferForPlaybackMs=*/initialPlaybackBufferMs,
|
||||||
|
/*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs,
|
||||||
DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
|
DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +95,10 @@ public class LoadController implements LoadControl {
|
|||||||
@Override
|
@Override
|
||||||
public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed,
|
public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed,
|
||||||
boolean rebuffering) {
|
boolean rebuffering) {
|
||||||
return internalLoadControl.shouldStartPlayback(bufferedDurationUs, playbackSpeed,
|
final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >=
|
||||||
rebuffering);
|
this.initialPlaybackBufferUs * playbackSpeed;
|
||||||
|
final boolean isInternalStartingPlayback = internalLoadControl.shouldStartPlayback(
|
||||||
|
bufferedDurationUs, playbackSpeed, rebuffering);
|
||||||
|
return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,16 +117,27 @@ public class PlayerHelper {
|
|||||||
return 512 * 1024L;
|
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) {
|
public static boolean isUsingDSP(@NonNull final Context context) {
|
||||||
|
@ -12,7 +12,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class FailedMediaSource implements ManagedMediaSource {
|
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 PlayQueueItem playQueueItem;
|
||||||
private final Throwable error;
|
private final Throwable error;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.schabi.newpipe.player.playback;
|
package org.schabi.newpipe.player.playback;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
@ -28,7 +29,6 @@ import java.util.concurrent.TimeUnit;
|
|||||||
|
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.annotations.NonNull;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.disposables.SerialDisposable;
|
import io.reactivex.disposables.SerialDisposable;
|
||||||
@ -38,23 +38,26 @@ import io.reactivex.subjects.PublishSubject;
|
|||||||
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
|
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
|
||||||
|
|
||||||
public class MediaSourceManager {
|
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.
|
// WINDOW_SIZE determines how many streams AFTER the current stream should be loaded.
|
||||||
// The default value (1) ensures seamless playback under typical network settings.
|
// The default value (1) ensures seamless playback under typical network settings.
|
||||||
private final static int WINDOW_SIZE = 1;
|
private final static int WINDOW_SIZE = 1;
|
||||||
|
|
||||||
private final PlaybackListener playbackListener;
|
@NonNull private final PlaybackListener playbackListener;
|
||||||
private final PlayQueue playQueue;
|
@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 long expirationTimeMillis;
|
||||||
private final TimeUnit expirationTimeUnit;
|
|
||||||
|
|
||||||
// Process only the last load order when receiving a stream of load orders (lessens I/O)
|
// 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
|
// The higher it is, the less loading occurs during rapid noncritical timeline changes
|
||||||
// Not recommended to go below 100ms
|
// Not recommended to go below 100ms
|
||||||
private final long loadDebounceMillis;
|
private final long loadDebounceMillis;
|
||||||
private final PublishSubject<Long> debouncedLoadSignal;
|
@NonNull private final Disposable debouncedLoader;
|
||||||
private final Disposable debouncedLoader;
|
@NonNull private final PublishSubject<Long> debouncedSignal;
|
||||||
|
|
||||||
private DynamicConcatenatingMediaSource sources;
|
private DynamicConcatenatingMediaSource sources;
|
||||||
|
|
||||||
@ -71,23 +74,20 @@ public class MediaSourceManager {
|
|||||||
@NonNull final PlayQueue playQueue) {
|
@NonNull final PlayQueue playQueue) {
|
||||||
this(listener, playQueue,
|
this(listener, playQueue,
|
||||||
/*loadDebounceMillis=*/400L,
|
/*loadDebounceMillis=*/400L,
|
||||||
/*expirationTimeMillis=*/2,
|
/*expirationTimeMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES));
|
||||||
/*expirationTimeUnit=*/TimeUnit.HOURS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSourceManager(@NonNull final PlaybackListener listener,
|
private MediaSourceManager(@NonNull final PlaybackListener listener,
|
||||||
@NonNull final PlayQueue playQueue,
|
@NonNull final PlayQueue playQueue,
|
||||||
final long loadDebounceMillis,
|
final long loadDebounceMillis,
|
||||||
final long expirationTimeMillis,
|
final long expirationTimeMillis) {
|
||||||
@NonNull final TimeUnit expirationTimeUnit) {
|
|
||||||
this.playbackListener = listener;
|
this.playbackListener = listener;
|
||||||
this.playQueue = playQueue;
|
this.playQueue = playQueue;
|
||||||
this.loadDebounceMillis = loadDebounceMillis;
|
this.loadDebounceMillis = loadDebounceMillis;
|
||||||
this.expirationTimeMillis = expirationTimeMillis;
|
this.expirationTimeMillis = expirationTimeMillis;
|
||||||
this.expirationTimeUnit = expirationTimeUnit;
|
|
||||||
|
|
||||||
this.loaderReactor = new CompositeDisposable();
|
this.loaderReactor = new CompositeDisposable();
|
||||||
this.debouncedLoadSignal = PublishSubject.create();
|
this.debouncedSignal = PublishSubject.create();
|
||||||
this.debouncedLoader = getDebouncedLoader();
|
this.debouncedLoader = getDebouncedLoader();
|
||||||
|
|
||||||
this.sources = new DynamicConcatenatingMediaSource();
|
this.sources = new DynamicConcatenatingMediaSource();
|
||||||
@ -109,8 +109,11 @@ public class MediaSourceManager {
|
|||||||
* Dispose the manager and releases all message buses and loaders.
|
* Dispose the manager and releases all message buses and loaders.
|
||||||
* */
|
* */
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete();
|
if (DEBUG) Log.d(TAG, "dispose() called.");
|
||||||
if (debouncedLoader != null) debouncedLoader.dispose();
|
|
||||||
|
debouncedSignal.onComplete();
|
||||||
|
debouncedLoader.dispose();
|
||||||
|
|
||||||
if (playQueueReactor != null) playQueueReactor.cancel();
|
if (playQueueReactor != null) playQueueReactor.cancel();
|
||||||
if (loaderReactor != null) loaderReactor.dispose();
|
if (loaderReactor != null) loaderReactor.dispose();
|
||||||
if (syncReactor != null) syncReactor.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.
|
* Unblocks the player once the item at the current index is loaded.
|
||||||
* */
|
* */
|
||||||
public void load() {
|
public void load() {
|
||||||
|
if (DEBUG) Log.d(TAG, "load() called.");
|
||||||
loadDebounced();
|
loadDebounced();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +143,8 @@ public class MediaSourceManager {
|
|||||||
* through {@link #load() load}.
|
* through {@link #load() load}.
|
||||||
* */
|
* */
|
||||||
public void reset() {
|
public void reset() {
|
||||||
|
if (DEBUG) Log.d(TAG, "reset() called.");
|
||||||
|
|
||||||
tryBlock();
|
tryBlock();
|
||||||
|
|
||||||
syncedItem = null;
|
syncedItem = null;
|
||||||
@ -205,11 +211,11 @@ public class MediaSourceManager {
|
|||||||
case INIT:
|
case INIT:
|
||||||
case REORDER:
|
case REORDER:
|
||||||
case ERROR:
|
case ERROR:
|
||||||
|
case SELECT:
|
||||||
loadImmediate(); // low frequency, critical events
|
loadImmediate(); // low frequency, critical events
|
||||||
break;
|
break;
|
||||||
case APPEND:
|
case APPEND:
|
||||||
case REMOVE:
|
case REMOVE:
|
||||||
case SELECT:
|
|
||||||
case MOVE:
|
case MOVE:
|
||||||
case RECOVERY:
|
case RECOVERY:
|
||||||
default:
|
default:
|
||||||
@ -229,16 +235,12 @@ public class MediaSourceManager {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private boolean isPlayQueueReady() {
|
private boolean isPlayQueueReady() {
|
||||||
if (playQueue == null) return false;
|
|
||||||
|
|
||||||
final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
|
final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
|
||||||
return playQueue.isComplete() || isWindowLoaded;
|
return playQueue.isComplete() || isWindowLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPlaybackReady() {
|
private boolean isPlaybackReady() {
|
||||||
if (sources == null || playQueue == null || sources.getSize() != playQueue.size()) {
|
if (sources == null || sources.getSize() != playQueue.size()) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex());
|
final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex());
|
||||||
final PlayQueueItem playQueueItem = playQueue.getItem();
|
final PlayQueueItem playQueueItem = playQueue.getItem();
|
||||||
@ -252,6 +254,8 @@ public class MediaSourceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void tryBlock() {
|
private void tryBlock() {
|
||||||
|
if (DEBUG) Log.d(TAG, "tryBlock() called.");
|
||||||
|
|
||||||
if (isBlocked) return;
|
if (isBlocked) return;
|
||||||
|
|
||||||
playbackListener.block();
|
playbackListener.block();
|
||||||
@ -261,6 +265,8 @@ public class MediaSourceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void tryUnblock() {
|
private void tryUnblock() {
|
||||||
|
if (DEBUG) Log.d(TAG, "tryUnblock() called.");
|
||||||
|
|
||||||
if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) {
|
if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) {
|
||||||
isBlocked = false;
|
isBlocked = false;
|
||||||
playbackListener.unblock(sources);
|
playbackListener.unblock(sources);
|
||||||
@ -272,6 +278,8 @@ public class MediaSourceManager {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void sync() {
|
private void sync() {
|
||||||
|
if (DEBUG) Log.d(TAG, "sync() called.");
|
||||||
|
|
||||||
final PlayQueueItem currentItem = playQueue.getItem();
|
final PlayQueueItem currentItem = playQueue.getItem();
|
||||||
if (isBlocked || currentItem == null) return;
|
if (isBlocked || currentItem == null) return;
|
||||||
|
|
||||||
@ -289,7 +297,6 @@ public class MediaSourceManager {
|
|||||||
|
|
||||||
private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item,
|
private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item,
|
||||||
@Nullable final StreamInfo info) {
|
@Nullable final StreamInfo info) {
|
||||||
if (playQueue == null || playbackListener == null) return;
|
|
||||||
// Ensure the current item is up to date with the play queue
|
// Ensure the current item is up to date with the play queue
|
||||||
if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) {
|
if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) {
|
||||||
playbackListener.sync(syncedItem, info);
|
playbackListener.sync(syncedItem, info);
|
||||||
@ -301,14 +308,14 @@ public class MediaSourceManager {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private Disposable getDebouncedLoader() {
|
private Disposable getDebouncedLoader() {
|
||||||
return debouncedLoadSignal
|
return debouncedSignal
|
||||||
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
|
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(timestamp -> loadImmediate());
|
.subscribe(timestamp -> loadImmediate());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadDebounced() {
|
private void loadDebounced() {
|
||||||
debouncedLoadSignal.onNext(System.currentTimeMillis());
|
debouncedSignal.onNext(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadImmediate() {
|
private void loadImmediate() {
|
||||||
@ -316,7 +323,7 @@ public class MediaSourceManager {
|
|||||||
final int currentIndex = playQueue.getIndex();
|
final int currentIndex = playQueue.getIndex();
|
||||||
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
|
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
|
||||||
if (currentItem == null) return;
|
if (currentItem == null) return;
|
||||||
loadItem(currentItem);
|
maybeLoadItem(currentItem);
|
||||||
|
|
||||||
// The rest are just for seamless playback
|
// The rest are just for seamless playback
|
||||||
final int leftBound = currentIndex + 1;
|
final int leftBound = currentIndex + 1;
|
||||||
@ -331,10 +338,14 @@ public class MediaSourceManager {
|
|||||||
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
|
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;
|
if (sources == null || item == null) return;
|
||||||
|
|
||||||
final int index = playQueue.indexOf(item);
|
final int index = playQueue.indexOf(item);
|
||||||
@ -368,11 +379,6 @@ public class MediaSourceManager {
|
|||||||
|
|
||||||
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
|
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
|
||||||
return stream.getStream().map(streamInfo -> {
|
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);
|
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
final Exception exception = new IllegalStateException(
|
final Exception exception = new IllegalStateException(
|
||||||
@ -384,21 +390,34 @@ public class MediaSourceManager {
|
|||||||
return new FailedMediaSource(stream, new IllegalStateException(exception));
|
return new FailedMediaSource(stream, new IllegalStateException(exception));
|
||||||
}
|
}
|
||||||
|
|
||||||
final long expiration = System.currentTimeMillis() +
|
final long expiration = System.currentTimeMillis() + expirationTimeMillis;
|
||||||
TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit);
|
|
||||||
return new LoadedMediaSource(source, stream, expiration);
|
return new LoadedMediaSource(source, stream, expiration);
|
||||||
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
|
}).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) {
|
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);
|
final int index = playQueue.indexOf(item);
|
||||||
if (index == -1 || index >= sources.getSize()) return false;
|
if (index == -1 || index >= sources.getSize()) return false;
|
||||||
|
|
||||||
final MediaSource mediaSource = sources.getMediaSource(index);
|
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
|
||||||
return !(mediaSource instanceof ManagedMediaSource) ||
|
|
||||||
((ManagedMediaSource) mediaSource).canReplace(item);
|
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() {
|
private void resetSources() {
|
||||||
|
if (DEBUG) Log.d(TAG, "resetSources() called.");
|
||||||
|
|
||||||
if (this.sources != null) this.sources.releaseSource();
|
if (this.sources != null) this.sources.releaseSource();
|
||||||
this.sources = new DynamicConcatenatingMediaSource();
|
this.sources = new DynamicConcatenatingMediaSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populateSources() {
|
private void populateSources() {
|
||||||
|
if (DEBUG) Log.d(TAG, "populateSources() called.");
|
||||||
if (sources == null || sources.getSize() >= playQueue.size()) return;
|
if (sources == null || sources.getSize() >= playQueue.size()) return;
|
||||||
|
|
||||||
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
|
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
|
||||||
@ -462,8 +484,12 @@ public class MediaSourceManager {
|
|||||||
* Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
|
* Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
|
||||||
* at the given index with a given {@link MediaSource}. If the index is out of bound,
|
* at the given index with a given {@link MediaSource}. If the index is out of bound,
|
||||||
* then the replacement is ignored.
|
* 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 (sources == null) return;
|
||||||
if (index < 0 || index >= sources.getSize()) return;
|
if (index < 0 || index >= sources.getSize()) return;
|
||||||
|
|
||||||
|
@ -401,8 +401,8 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playbackLiveSync"
|
android:id="@+id/playbackLiveSync"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center"
|
||||||
android:text="@string/live_sync"
|
android:text="@string/live_sync"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
Loading…
Reference in New Issue
Block a user