-Updated Exoplayer to 2.7.0.

-PoC for new seamless stream loading mechanism.
This commit is contained in:
John Zhen Mo 2018-02-22 13:30:48 -08:00
parent e9f59ae769
commit 8803b60b28
11 changed files with 617 additions and 62 deletions

View File

@ -73,7 +73,7 @@ dependencies {
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
implementation 'com.nononsenseapps:filepicker:3.0.1'
implementation 'com.google.android.exoplayer:exoplayer:2.6.0'
implementation 'com.google.android.exoplayer:exoplayer:2.7.0'
debugImplementation 'com.facebook.stetho:stetho:1.5.0'
debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'

View File

@ -527,23 +527,26 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
}
private void showDeleteSuggestionDialog(final SuggestionItem item) {
final Disposable onDelete = historyRecordManager.deleteSearchHistory(item.query)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.SOMETHING_ELSE, "none",
"Deleting item failed", R.string.general_error)
);
if (activity == null || historyRecordManager == null || suggestionPublisher == null ||
searchEditText == null || disposables == null) return;
final String query = item.query;
new AlertDialog.Builder(activity)
.setTitle(item.query)
.setTitle(query)
.setMessage(R.string.delete_item_search_history)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete))
.setPositiveButton(R.string.delete, (dialog, which) -> {
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.SOMETHING_ELSE, "none",
"Deleting item failed", R.string.general_error)
);
disposables.add(onDelete);
})
.show();
}
@ -701,19 +704,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<SearchResult>() {
@Override
public void accept(@NonNull SearchResult result) throws Exception {
isLoading.set(false);
handleResult(result);
}
}, new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
isLoading.set(false);
onError(throwable);
}
});
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
.subscribe(this::handleResult, this::onError);
}
@Override
@ -725,19 +717,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<ListExtractor.InfoItemPage>() {
@Override
public void accept(@NonNull ListExtractor.InfoItemPage result) throws Exception {
isLoading.set(false);
handleNextItems(result);
}
}, new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
isLoading.set(false);
onError(throwable);
}
});
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
.subscribe(this::handleNextItems, this::onError);
}
@Override

View File

@ -46,6 +46,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
@ -67,7 +68,7 @@ import org.schabi.newpipe.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.CacheFactory;
import org.schabi.newpipe.player.helper.LoadController;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.MediaSourceManagerAlt;
import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueAdapter;
@ -124,7 +125,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f};
protected MediaSourceManager playbackManager;
protected MediaSourceManagerAlt playbackManager;
protected PlayQueue playQueue;
protected StreamInfo currentInfo;
@ -150,6 +151,12 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
protected DataSource.Factory cacheDataSourceFactory;
protected DefaultExtractorsFactory extractorsFactory;
protected SsMediaSource.Factory ssMediaSourceFactory;
protected HlsMediaSource.Factory hlsMediaSourceFactory;
protected DashMediaSource.Factory dashMediaSourceFactory;
protected ExtractorMediaSource.Factory extractorMediaSourceFactory;
protected SingleSampleMediaSource.Factory sampleMediaSourceFactory;
protected Disposable progressUpdateReactor;
protected CompositeDisposable databaseUpdateReactor;
@ -192,6 +199,14 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
extractorsFactory = new DefaultExtractorsFactory();
cacheDataSourceFactory = new CacheFactory(context);
ssMediaSourceFactory = new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory);
hlsMediaSourceFactory = new HlsMediaSource.Factory(cacheDataSourceFactory);
dashMediaSourceFactory = new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory);
extractorMediaSourceFactory = new ExtractorMediaSource.Factory(cacheDataSourceFactory);
sampleMediaSourceFactory = new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
audioReactor = new AudioReactor(context, simpleExoPlayer);
@ -247,7 +262,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
protected void initPlayback(final PlayQueue queue) {
playQueue = queue;
playQueue.init();
playbackManager = new MediaSourceManager(this, playQueue);
playbackManager = new MediaSourceManagerAlt(this, playQueue);
if (playQueueAdapter != null) playQueueAdapter.dispose();
playQueueAdapter = new PlayQueueAdapter(context, playQueue);
@ -310,16 +325,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
MediaSource mediaSource;
switch (type) {
case C.TYPE_SS:
mediaSource = new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null);
mediaSource = ssMediaSourceFactory.createMediaSource(uri);
break;
case C.TYPE_DASH:
mediaSource = new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null);
mediaSource = dashMediaSourceFactory.createMediaSource(uri);
break;
case C.TYPE_HLS:
mediaSource = new HlsMediaSource(uri, cacheDataSourceFactory, null, null);
mediaSource = hlsMediaSourceFactory.createMediaSource(uri);
break;
case C.TYPE_OTHER:
mediaSource = new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null);
mediaSource = extractorMediaSourceFactory.createMediaSource(uri);
break;
default: {
throw new IllegalStateException("Unsupported type: " + type);
@ -489,7 +504,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
if (playbackManager != null) {

View File

@ -305,8 +305,8 @@ public abstract class VideoPlayer extends BasePlayer
captionItem.setOnMenuItemClickListener(menuItem -> {
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setParameters(trackSelector.getParameters()
.withPreferredTextLanguage(captionLanguage));
trackSelector.setParameters(trackSelector.getParameters().buildUpon()
.setPreferredTextLanguage(captionLanguage).build());
trackSelector.setRendererDisabled(textRendererIndex, false);
}
return true;
@ -395,8 +395,8 @@ public abstract class VideoPlayer extends BasePlayer
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
final MediaSource textSource = new SingleSampleMediaSource(
Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET);
final MediaSource textSource = sampleMediaSourceFactory.createMediaSource(
Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
mediaSources.add(textSource);
}

View File

@ -12,6 +12,8 @@ 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;
public class LoadController implements LoadControl {
@ -29,14 +31,14 @@ public class LoadController implements LoadControl {
PlayerHelper.getBufferForPlaybackMs(context));
}
public LoadController(final int minBufferMs,
final int maxBufferMs,
final int bufferForPlaybackMs) {
private LoadController(final int minBufferMs, final int maxBufferMs,
final int bufferForPlaybackMs) {
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);
bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
}
/*//////////////////////////////////////////////////////////////////////////
@ -49,7 +51,8 @@ public class LoadController implements LoadControl {
}
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, TrackSelectionArray trackSelectionArray) {
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray,
TrackSelectionArray trackSelectionArray) {
internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray);
}
@ -69,12 +72,24 @@ public class LoadController implements LoadControl {
}
@Override
public boolean shouldStartPlayback(long l, boolean b) {
return internalLoadControl.shouldStartPlayback(l, b);
public long getBackBufferDurationUs() {
return internalLoadControl.getBackBufferDurationUs();
}
@Override
public boolean shouldContinueLoading(long l) {
return internalLoadControl.shouldContinueLoading(l);
public boolean retainBackBufferFromKeyframe() {
return internalLoadControl.retainBackBufferFromKeyframe();
}
@Override
public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
}
@Override
public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed,
boolean rebuffering) {
return internalLoadControl.shouldStartPlayback(bufferedDurationUs, playbackSpeed,
rebuffering);
}
}

View File

@ -0,0 +1,72 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException;
public class FailedMediaSource implements ManagedMediaSource {
private final PlayQueueItem playQueueItem;
private final Throwable error;
private final long retryTimestamp;
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final Throwable error,
final long retryTimestamp) {
this.playQueueItem = playQueueItem;
this.error = error;
this.retryTimestamp = retryTimestamp;
}
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final Throwable error) {
this.playQueueItem = playQueueItem;
this.error = error;
this.retryTimestamp = Long.MAX_VALUE;
}
public PlayQueueItem getPlayQueueItem() {
return playQueueItem;
}
public Throwable getError() {
return error;
}
public boolean canRetry() {
return System.currentTimeMillis() >= retryTimestamp;
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
throw new IOException(error);
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
return null;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {}
@Override
public void releaseSource() {}
@Override
public boolean canReplace() {
return canRetry();
}
}

View File

@ -0,0 +1,75 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException;
public class LoadedMediaSource implements ManagedMediaSource {
private final PlayQueueItem playQueueItem;
private final StreamInfo streamInfo;
private final MediaSource source;
private final long expireTimestamp;
public LoadedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final StreamInfo streamInfo,
@NonNull final MediaSource source,
final long expireTimestamp) {
this.playQueueItem = playQueueItem;
this.streamInfo = streamInfo;
this.source = source;
this.expireTimestamp = expireTimestamp;
}
public PlayQueueItem getPlayQueueItem() {
return playQueueItem;
}
public StreamInfo getStreamInfo() {
return streamInfo;
}
public boolean isExpired() {
return System.currentTimeMillis() >= expireTimestamp;
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
source.prepareSource(player, isTopLevelSource, listener);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
source.maybeThrowSourceInfoRefreshError();
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
return source.createPeriod(id, allocator);
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
source.releasePeriod(mediaPeriod);
}
@Override
public void releaseSource() {
source.releaseSource();
}
@Override
public boolean canReplace() {
return isExpired();
}
}

View File

@ -0,0 +1,7 @@
package org.schabi.newpipe.player.mediasource;
import com.google.android.exoplayer2.source.MediaSource;
public interface ManagedMediaSource extends MediaSource {
boolean canReplace();
}

View File

@ -0,0 +1,22 @@
package org.schabi.newpipe.player.mediasource;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
public class PlaceholderMediaSource implements ManagedMediaSource {
// Do nothing, so this will stall the playback
@Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {}
@Override public void maybeThrowSourceInfoRefreshError() throws IOException {}
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; }
@Override public void releasePeriod(MediaPeriod mediaPeriod) {}
@Override public void releaseSource() {}
@Override
public boolean canReplace() {
return true;
}
}

View File

@ -20,7 +20,6 @@ import java.util.concurrent.TimeUnit;
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;
import io.reactivex.functions.Consumer;

View File

@ -0,0 +1,369 @@
package org.schabi.newpipe.player.playback;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import java.util.ArrayList;
import java.util.List;
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.subjects.PublishSubject;
public class MediaSourceManagerAlt {
// One-side rolling window size for default loading
// Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0
private final int windowSize;
private final PlaybackListener playbackListener;
private final PlayQueue playQueue;
// 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;
private DynamicConcatenatingMediaSource sources;
private Subscription playQueueReactor;
private CompositeDisposable loaderReactor;
private PlayQueueItem syncedItem;
private boolean isBlocked;
public MediaSourceManagerAlt(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
this(listener, playQueue, 1, 400L);
}
private MediaSourceManagerAlt(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue,
final int windowSize,
final long loadDebounceMillis) {
if (windowSize <= 0) {
throw new UnsupportedOperationException("MediaSourceManager window size must be greater than 0");
}
this.playbackListener = listener;
this.playQueue = playQueue;
this.windowSize = windowSize;
this.loadDebounceMillis = loadDebounceMillis;
this.loaderReactor = new CompositeDisposable();
this.debouncedLoadSignal = PublishSubject.create();
this.debouncedLoader = getDebouncedLoader();
this.sources = new DynamicConcatenatingMediaSource();
playQueue.getBroadcastReceiver()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getReactor());
}
/*//////////////////////////////////////////////////////////////////////////
// Exposed Methods
//////////////////////////////////////////////////////////////////////////*/
/**
* Dispose the manager and releases all message buses and loaders.
* */
public void dispose() {
if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete();
if (debouncedLoader != null) debouncedLoader.dispose();
if (playQueueReactor != null) playQueueReactor.cancel();
if (loaderReactor != null) loaderReactor.dispose();
if (sources != null) sources.releaseSource();
playQueueReactor = null;
loaderReactor = null;
syncedItem = null;
sources = null;
}
/**
* Loads the current playing stream and the streams within its windowSize bound.
*
* Unblocks the player once the item at the current index is loaded.
* */
public void load() {
loadDebounced();
}
/**
* Blocks the player and repopulate the sources.
*
* Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}.
* */
public void reset() {
tryBlock();
syncedItem = null;
populateSources();
}
/*//////////////////////////////////////////////////////////////////////////
// Event Reactor
//////////////////////////////////////////////////////////////////////////*/
private Subscriber<PlayQueueEvent> getReactor() {
return new Subscriber<PlayQueueEvent>() {
@Override
public void onSubscribe(@NonNull Subscription d) {
if (playQueueReactor != null) playQueueReactor.cancel();
playQueueReactor = d;
playQueueReactor.request(1);
}
@Override
public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage);
}
@Override
public void onError(@NonNull Throwable e) {}
@Override
public void onComplete() {}
};
}
private void onPlayQueueChanged(final PlayQueueEvent event) {
if (playQueue.isEmpty() && playQueue.isComplete()) {
playbackListener.shutdown();
return;
}
// Event specific action
switch (event.type()) {
case INIT:
case REORDER:
case ERROR:
reset();
break;
case APPEND:
populateSources();
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex());
break;
case MOVE:
final MoveEvent moveEvent = (MoveEvent) event;
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case SELECT:
case RECOVERY:
default:
break;
}
// Loading and Syncing
switch (event.type()) {
case INIT:
case REORDER:
case ERROR:
loadImmediate(); // low frequency, critical events
break;
case APPEND:
case REMOVE:
case SELECT:
case MOVE:
case RECOVERY:
default:
loadDebounced(); // high frequency or noncritical events
break;
}
if (!isPlayQueueReady()) {
tryBlock();
playQueue.fetch();
}
if (playQueueReactor != null) playQueueReactor.request(1);
}
/*//////////////////////////////////////////////////////////////////////////
// Internal Helpers
//////////////////////////////////////////////////////////////////////////*/
private boolean isPlayQueueReady() {
final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > windowSize;
return playQueue.isComplete() || isWindowLoaded;
}
private boolean tryBlock() {
if (!isBlocked) {
playbackListener.block();
resetSources();
isBlocked = true;
return true;
}
return false;
}
private boolean tryUnblock() {
if (isPlayQueueReady() && isBlocked && sources != null) {
isBlocked = false;
playbackListener.unblock(sources);
return true;
}
return false;
}
private void sync(final PlayQueueItem item, final StreamInfo info) {
final PlayQueueItem currentItem = playQueue.getItem();
if (currentItem != item || syncedItem == item || playbackListener == null) return;
syncedItem = currentItem;
// Ensure the current item is up to date with the play queue
if (playQueue.getItem() == currentItem && playQueue.getItem() == syncedItem) {
playbackListener.sync(syncedItem, info);
}
}
private void loadDebounced() {
debouncedLoadSignal.onNext(System.currentTimeMillis());
}
private void loadImmediate() {
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return;
loadItem(currentItem);
// The rest are just for seamless playback
final int leftBound = Math.max(0, currentIndex - windowSize);
final int rightLimit = currentIndex + windowSize + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
// Do a round robin
final int excess = rightLimit - playQueue.size();
if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
for (final PlayQueueItem item: items) loadItem(item);
}
private void loadItem(@Nullable final PlayQueueItem item) {
if (sources == null || item == null) return;
final int index = playQueue.indexOf(item);
if (index > sources.getSize() - 1) return;
if (((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) {
final Disposable loader = getMediaSource(item)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(mediaSource -> update(playQueue.indexOf(item), mediaSource));
loaderReactor.add(loader);
}
tryUnblock();
if (!isBlocked) {
final MediaSource mediaSource = sources.getMediaSource(playQueue.indexOf(item));
final StreamInfo info = mediaSource instanceof LoadedMediaSource ?
((LoadedMediaSource) mediaSource).getStreamInfo() : null;
sync(item, info);
}
}
private void resetSources() {
if (this.sources != null) this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource();
}
private void populateSources() {
if (sources == null) return;
for (final PlayQueueItem item : playQueue.getStreams()) {
insert(playQueue.indexOf(item), new PlaceholderMediaSource());
}
}
private Disposable getDebouncedLoader() {
return debouncedLoadSignal
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate());
}
private Single<ManagedMediaSource> getMediaSource(@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) {
return new FailedMediaSource(stream, new IllegalStateException(
"MediaSource resolution is null"));
}
return new LoadedMediaSource(stream, streamInfo, source,
TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS));
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
}
/*//////////////////////////////////////////////////////////////////////////
// Media Source List Manipulation
//////////////////////////////////////////////////////////////////////////*/
private void update(final int queueIndex, final MediaSource source) {
if (sources == null) return;
if (queueIndex < 0 || queueIndex < sources.getSize()) return;
sources.addMediaSource(queueIndex + 1, source);
sources.removeMediaSource(queueIndex);
}
/**
* Inserts a source into {@link DynamicConcatenatingMediaSource} with position
* in respect to the play queue.
*
* If the play queue index already exists, then the insert is ignored.
* */
private void insert(final int queueIndex, final PlaceholderMediaSource source) {
if (sources == null) return;
if (queueIndex < 0 || queueIndex < sources.getSize()) return;
sources.addMediaSource(queueIndex, source);
}
/**
* Removes a source from {@link DynamicConcatenatingMediaSource} with the given play queue index.
*
* If the play queue index does not exist, the removal is ignored.
* */
private void remove(final int queueIndex) {
if (sources == null) return;
if (queueIndex < 0 || queueIndex > sources.getSize()) return;
sources.removeMediaSource(queueIndex);
}
private void move(final int source, final int target) {
if (sources == null) return;
if (source < 0 || target < 0) return;
if (source >= sources.getSize() || target >= sources.getSize()) return;
sources.moveMediaSource(source, target);
}
}