2017-09-02 20:06:36 +02:00
|
|
|
package org.schabi.newpipe.player;
|
|
|
|
|
2017-09-05 00:38:58 +02:00
|
|
|
import android.support.annotation.Nullable;
|
2017-09-03 04:30:34 +02:00
|
|
|
|
2017-09-02 20:06:36 +02:00
|
|
|
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
|
|
|
import com.google.android.exoplayer2.source.MediaSource;
|
|
|
|
|
|
|
|
import org.reactivestreams.Subscriber;
|
|
|
|
import org.reactivestreams.Subscription;
|
2017-09-04 19:23:56 +02:00
|
|
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
2017-09-02 20:06:36 +02:00
|
|
|
import org.schabi.newpipe.playlist.PlayQueue;
|
2017-09-03 04:30:34 +02:00
|
|
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
2017-09-02 20:06:36 +02:00
|
|
|
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
|
2017-09-03 04:30:34 +02:00
|
|
|
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
|
|
|
import org.schabi.newpipe.playlist.events.SwapEvent;
|
2017-09-02 20:06:36 +02:00
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.List;
|
|
|
|
|
2017-09-04 19:23:56 +02:00
|
|
|
import io.reactivex.SingleObserver;
|
2017-09-03 04:30:34 +02:00
|
|
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
2017-09-02 20:06:36 +02:00
|
|
|
import io.reactivex.annotations.NonNull;
|
2017-09-03 04:30:34 +02:00
|
|
|
import io.reactivex.disposables.CompositeDisposable;
|
|
|
|
import io.reactivex.disposables.Disposable;
|
|
|
|
import io.reactivex.functions.Consumer;
|
|
|
|
|
|
|
|
class MediaSourceManager {
|
|
|
|
private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode());
|
2017-09-04 04:15:11 +02:00
|
|
|
// One-side rolling window size for default loading
|
|
|
|
// Effectively loads WINDOW_SIZE * 2 streams
|
2017-09-03 04:30:34 +02:00
|
|
|
private static final int WINDOW_SIZE = 3;
|
2017-09-02 20:06:36 +02:00
|
|
|
|
2017-09-04 19:23:56 +02:00
|
|
|
private final PlaybackListener playbackListener;
|
|
|
|
private final PlayQueue playQueue;
|
|
|
|
|
|
|
|
private DynamicConcatenatingMediaSource sources;
|
2017-09-03 04:30:34 +02:00
|
|
|
// sourceToQueueIndex maps media source index to play queue index
|
|
|
|
// Invariant 1: this list is sorted in ascending order
|
|
|
|
// Invariant 2: this list contains no duplicates
|
2017-09-04 19:23:56 +02:00
|
|
|
private List<Integer> sourceToQueueIndex;
|
2017-09-02 20:06:36 +02:00
|
|
|
|
|
|
|
private Subscription playQueueReactor;
|
2017-09-03 04:30:34 +02:00
|
|
|
private Subscription loadingReactor;
|
|
|
|
private CompositeDisposable disposables;
|
2017-09-02 20:06:36 +02:00
|
|
|
|
2017-09-04 04:15:11 +02:00
|
|
|
private boolean isBlocked;
|
|
|
|
|
2017-09-02 20:06:36 +02:00
|
|
|
interface PlaybackListener {
|
2017-09-04 04:15:11 +02:00
|
|
|
/*
|
|
|
|
* Called when the stream at the current queue index is not ready yet.
|
|
|
|
* Signals to the listener to block the player from playing anything.
|
|
|
|
* */
|
2017-09-02 20:06:36 +02:00
|
|
|
void block();
|
2017-09-04 04:15:11 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Called when the stream at the current queue index is ready.
|
|
|
|
* Signals to the listener to resume the player.
|
|
|
|
* May be called at any time, even when the player is unblocked.
|
|
|
|
* */
|
2017-09-02 20:06:36 +02:00
|
|
|
void unblock();
|
|
|
|
|
2017-09-04 04:15:11 +02:00
|
|
|
/*
|
|
|
|
* Called when the queue index is refreshed.
|
|
|
|
* Signals to the listener to synchronize the player's window to the manager's
|
|
|
|
* window.
|
|
|
|
* */
|
2017-09-05 21:27:12 +02:00
|
|
|
void sync(final StreamInfo info, final int sortedStreamsIndex);
|
2017-09-04 04:15:11 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Requests the listener to resolve a stream info into a media source respective
|
|
|
|
* of the listener's implementation (background, popup or main video player),
|
|
|
|
* */
|
2017-09-05 21:27:12 +02:00
|
|
|
MediaSource sourceOf(final StreamInfo info, final int sortedStreamsIndex);
|
|
|
|
|
|
|
|
void shutdown();
|
2017-09-02 20:06:36 +02:00
|
|
|
}
|
|
|
|
|
2017-09-03 04:30:34 +02:00
|
|
|
MediaSourceManager(@NonNull final MediaSourceManager.PlaybackListener listener,
|
|
|
|
@NonNull final PlayQueue playQueue) {
|
2017-09-02 20:06:36 +02:00
|
|
|
this.playbackListener = listener;
|
|
|
|
this.playQueue = playQueue;
|
|
|
|
|
2017-09-04 19:23:56 +02:00
|
|
|
this.disposables = new CompositeDisposable();
|
|
|
|
|
|
|
|
this.sources = new DynamicConcatenatingMediaSource();
|
|
|
|
this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>());
|
2017-09-03 04:30:34 +02:00
|
|
|
|
|
|
|
playQueue.getBroadcastReceiver()
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.subscribe(getReactor());
|
|
|
|
}
|
|
|
|
|
2017-09-04 04:15:11 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Exposed Methods
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Returns the media source index of the currently playing stream.
|
|
|
|
* */
|
2017-09-03 04:30:34 +02:00
|
|
|
int getCurrentSourceIndex() {
|
|
|
|
return sourceToQueueIndex.indexOf(playQueue.getIndex());
|
|
|
|
}
|
|
|
|
|
|
|
|
@NonNull
|
|
|
|
DynamicConcatenatingMediaSource getMediaSource() {
|
|
|
|
return sources;
|
|
|
|
}
|
|
|
|
|
2017-09-04 04:15:11 +02:00
|
|
|
/*
|
2017-09-04 19:23:56 +02:00
|
|
|
* Called when the player has transitioned to another stream.
|
2017-09-04 04:15:11 +02:00
|
|
|
* */
|
2017-09-03 04:30:34 +02:00
|
|
|
void refresh(final int newSourceIndex) {
|
2017-09-04 19:23:56 +02:00
|
|
|
if (sourceToQueueIndex.indexOf(newSourceIndex) != -1) {
|
|
|
|
playQueue.setIndex(sourceToQueueIndex.indexOf(newSourceIndex));
|
2017-09-03 04:30:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-04 14:42:34 +02:00
|
|
|
void report(final Exception error) {
|
|
|
|
// ignore error checking for now, just remove the current index
|
2017-09-05 21:27:12 +02:00
|
|
|
if (error == null || !tryBlock()) return;
|
2017-09-04 14:42:34 +02:00
|
|
|
|
|
|
|
final int index = playQueue.getIndex();
|
|
|
|
playQueue.remove(index);
|
2017-09-04 19:23:56 +02:00
|
|
|
|
|
|
|
resetSources();
|
2017-09-05 00:38:58 +02:00
|
|
|
load();
|
2017-09-04 14:42:34 +02:00
|
|
|
}
|
|
|
|
|
2017-09-05 21:27:12 +02:00
|
|
|
int queueIndexOf(final int sourceIndex) {
|
|
|
|
return sourceIndex < sourceToQueueIndex.size() ? sourceToQueueIndex.get(sourceIndex) : -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
void updateCurrent(final int newSortedStreamsIndex) {
|
|
|
|
if (!tryBlock()) return;
|
|
|
|
|
|
|
|
PlayQueueItem item = playQueue.getCurrent();
|
|
|
|
item.setSortedQualityIndex(newSortedStreamsIndex);
|
|
|
|
resetSources();
|
|
|
|
load();
|
|
|
|
}
|
|
|
|
|
2017-09-04 04:15:11 +02:00
|
|
|
void dispose() {
|
|
|
|
if (loadingReactor != null) loadingReactor.cancel();
|
|
|
|
if (playQueueReactor != null) playQueueReactor.cancel();
|
|
|
|
if (disposables != null) disposables.dispose();
|
|
|
|
|
|
|
|
loadingReactor = null;
|
|
|
|
playQueueReactor = null;
|
|
|
|
disposables = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Event Reactor
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
private Subscriber<PlayQueueMessage> getReactor() {
|
|
|
|
return new Subscriber<PlayQueueMessage>() {
|
|
|
|
@Override
|
|
|
|
public void onSubscribe(@NonNull Subscription d) {
|
|
|
|
if (playQueueReactor != null) playQueueReactor.cancel();
|
|
|
|
playQueueReactor = d;
|
|
|
|
playQueueReactor.request(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onNext(@NonNull PlayQueueMessage event) {
|
|
|
|
// why no pattern matching in Java =(
|
|
|
|
switch (event.type()) {
|
|
|
|
case INIT:
|
2017-09-05 00:38:58 +02:00
|
|
|
isBlocked = true;
|
2017-09-04 04:15:11 +02:00
|
|
|
case APPEND:
|
|
|
|
load();
|
|
|
|
break;
|
|
|
|
case SELECT:
|
|
|
|
onSelect();
|
|
|
|
break;
|
|
|
|
case REMOVE:
|
|
|
|
final RemoveEvent removeEvent = (RemoveEvent) event;
|
|
|
|
remove(removeEvent.index());
|
|
|
|
break;
|
|
|
|
case SWAP:
|
|
|
|
final SwapEvent swapEvent = (SwapEvent) event;
|
|
|
|
swap(swapEvent.getFrom(), swapEvent.getTo());
|
|
|
|
break;
|
|
|
|
case NEXT:
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2017-09-04 19:23:56 +02:00
|
|
|
if (!isPlayQueueReady()) {
|
|
|
|
tryBlock();
|
2017-09-04 04:15:11 +02:00
|
|
|
playQueue.fetch();
|
2017-09-05 21:27:12 +02:00
|
|
|
} else if (playQueue.isEmpty()) {
|
|
|
|
playbackListener.shutdown();
|
2017-09-04 04:15:11 +02:00
|
|
|
}
|
2017-09-05 21:27:12 +02:00
|
|
|
|
2017-09-04 04:15:11 +02:00
|
|
|
if (playQueueReactor != null) playQueueReactor.request(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onError(@NonNull Throwable e) {}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onComplete() {
|
|
|
|
dispose();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Internal Helpers
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
private boolean isPlayQueueReady() {
|
|
|
|
return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
|
|
|
|
}
|
|
|
|
|
|
|
|
private boolean isCurrentIndexLoaded() {
|
|
|
|
return getCurrentSourceIndex() != -1;
|
|
|
|
}
|
|
|
|
|
2017-09-05 00:38:58 +02:00
|
|
|
private boolean tryBlock() {
|
2017-09-04 19:23:56 +02:00
|
|
|
if (!isBlocked) {
|
|
|
|
playbackListener.block();
|
|
|
|
isBlocked = true;
|
2017-09-05 00:38:58 +02:00
|
|
|
return true;
|
2017-09-04 19:23:56 +02:00
|
|
|
}
|
2017-09-05 00:38:58 +02:00
|
|
|
return false;
|
2017-09-04 14:42:34 +02:00
|
|
|
}
|
|
|
|
|
2017-09-05 00:38:58 +02:00
|
|
|
private boolean tryUnblock() {
|
2017-09-04 04:15:11 +02:00
|
|
|
if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) {
|
|
|
|
isBlocked = false;
|
|
|
|
playbackListener.unblock();
|
2017-09-05 00:38:58 +02:00
|
|
|
return true;
|
2017-09-04 04:15:11 +02:00
|
|
|
}
|
2017-09-05 00:38:58 +02:00
|
|
|
return false;
|
2017-09-04 04:15:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Responds to a SELECT event.
|
|
|
|
* When a change occur, the manager prepares by loading more.
|
|
|
|
* If the current item has not been fully loaded,
|
|
|
|
* */
|
|
|
|
private void onSelect() {
|
|
|
|
if (isCurrentIndexLoaded()) {
|
2017-09-03 04:30:34 +02:00
|
|
|
sync();
|
2017-09-04 19:23:56 +02:00
|
|
|
} else {
|
|
|
|
tryBlock();
|
2017-09-03 04:30:34 +02:00
|
|
|
}
|
2017-09-04 04:15:11 +02:00
|
|
|
|
|
|
|
load();
|
2017-09-03 04:30:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private void sync() {
|
2017-09-05 21:27:12 +02:00
|
|
|
final PlayQueueItem currentItem = playQueue.getCurrent();
|
|
|
|
|
2017-09-03 04:30:34 +02:00
|
|
|
final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
|
|
|
|
@Override
|
|
|
|
public void accept(StreamInfo streamInfo) throws Exception {
|
2017-09-05 21:27:12 +02:00
|
|
|
playbackListener.sync(streamInfo, currentItem.getSortedQualityIndex());
|
2017-09-03 04:30:34 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-09-05 21:27:12 +02:00
|
|
|
currentItem.getStream().subscribe(onSuccess);
|
2017-09-03 04:30:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private void load() {
|
2017-09-05 00:38:58 +02:00
|
|
|
// The current item has higher priority
|
2017-09-03 04:30:34 +02:00
|
|
|
final int currentIndex = playQueue.getIndex();
|
2017-09-05 00:38:58 +02:00
|
|
|
final PlayQueueItem currentItem = playQueue.get(currentIndex);
|
|
|
|
if (currentItem != null) load(currentItem);
|
2017-09-03 04:30:34 +02:00
|
|
|
|
2017-09-05 00:38:58 +02:00
|
|
|
// The rest are just for seamless playback
|
2017-09-03 04:30:34 +02:00
|
|
|
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
|
|
|
|
final int rightBound = Math.min(playQueue.size(), currentIndex + WINDOW_SIZE);
|
|
|
|
final List<PlayQueueItem> items = playQueue.getStreams().subList(leftBound, rightBound);
|
|
|
|
for (final PlayQueueItem item: items) {
|
|
|
|
load(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-05 00:38:58 +02:00
|
|
|
private void load(@Nullable final PlayQueueItem item) {
|
|
|
|
if (item == null) return;
|
2017-09-04 04:15:11 +02:00
|
|
|
|
2017-09-04 19:23:56 +02:00
|
|
|
item.getStream().subscribe(new SingleObserver<StreamInfo>() {
|
2017-09-03 04:30:34 +02:00
|
|
|
@Override
|
|
|
|
public void onSubscribe(@NonNull Disposable d) {
|
|
|
|
if (disposables != null) {
|
|
|
|
disposables.add(d);
|
|
|
|
} else {
|
|
|
|
d.dispose();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onSuccess(@NonNull StreamInfo streamInfo) {
|
2017-09-05 21:27:12 +02:00
|
|
|
final MediaSource source = playbackListener.sourceOf(streamInfo, item.getSortedQualityIndex());
|
2017-09-03 04:30:34 +02:00
|
|
|
insert(playQueue.indexOf(item), source);
|
2017-09-05 00:38:58 +02:00
|
|
|
if (tryUnblock()) sync();
|
2017-09-03 04:30:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onError(@NonNull Throwable e) {
|
|
|
|
playQueue.remove(playQueue.indexOf(item));
|
2017-09-04 04:15:11 +02:00
|
|
|
load();
|
2017-09-03 04:30:34 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-09-04 19:23:56 +02:00
|
|
|
private void resetSources() {
|
|
|
|
if (this.disposables != null) this.disposables.clear();
|
|
|
|
if (this.sources != null) this.sources.releaseSource();
|
|
|
|
if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear();
|
|
|
|
|
|
|
|
this.sources = new DynamicConcatenatingMediaSource();
|
|
|
|
}
|
|
|
|
|
2017-09-04 04:15:11 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Media Source List Manipulation
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
2017-09-05 21:27:12 +02:00
|
|
|
private void reset(final int queueIndex) {
|
2017-09-04 04:15:11 +02:00
|
|
|
if (queueIndex < 0) return;
|
|
|
|
|
|
|
|
final int sourceIndex = sourceToQueueIndex.indexOf(queueIndex);
|
|
|
|
if (sourceIndex != -1) {
|
2017-09-05 21:27:12 +02:00
|
|
|
sourceToQueueIndex.remove(sourceIndex);
|
2017-09-04 04:15:11 +02:00
|
|
|
sources.removeMediaSource(sourceIndex);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-03 04:30:34 +02:00
|
|
|
// Insert source into playlist 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 MediaSource source) {
|
|
|
|
if (queueIndex < 0) return;
|
|
|
|
|
|
|
|
int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex);
|
|
|
|
if (pos < 0) {
|
|
|
|
final int sourceIndex = -pos-1;
|
|
|
|
sourceToQueueIndex.add(sourceIndex, queueIndex);
|
|
|
|
sources.addMediaSource(sourceIndex, source);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void remove(final int queueIndex) {
|
|
|
|
if (queueIndex < 0) return;
|
|
|
|
|
|
|
|
final int sourceIndex = sourceToQueueIndex.indexOf(queueIndex);
|
|
|
|
if (sourceIndex != -1) {
|
|
|
|
sourceToQueueIndex.remove(sourceIndex);
|
|
|
|
sources.removeMediaSource(sourceIndex);
|
|
|
|
// Will be slow on really large arrays, fast enough for typical use case
|
|
|
|
for (int i = sourceIndex; i < sourceToQueueIndex.size(); i++) {
|
|
|
|
sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void swap(final int source, final int target) {
|
|
|
|
final int sourceIndex = sourceToQueueIndex.indexOf(source);
|
|
|
|
final int targetIndex = sourceToQueueIndex.indexOf(target);
|
|
|
|
|
|
|
|
if (sourceIndex != -1 && targetIndex != -1) {
|
|
|
|
sources.moveMediaSource(sourceIndex, targetIndex);
|
|
|
|
} else if (sourceIndex != -1) {
|
|
|
|
remove(sourceIndex);
|
|
|
|
} else if (targetIndex != -1) {
|
|
|
|
remove(targetIndex);
|
|
|
|
}
|
2017-09-02 20:06:36 +02:00
|
|
|
}
|
|
|
|
}
|