-Baked stream info resolution into custom media source, allowing for simpler playlist control.

-Added track merging on different stream qualities, allowing for implementation of smooth transition on A/V quality and captions change.
This commit is contained in:
John Zhen M 2017-09-19 21:46:16 -07:00 committed by John Zhen Mo
parent 9576d5bd89
commit 9bc95f030c
7 changed files with 151 additions and 129 deletions

View File

@ -37,6 +37,7 @@ import android.widget.RemoteViews;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
@ -49,6 +50,9 @@ import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.List;
/** /**
* Base players joining the common properties * Base players joining the common properties
@ -390,9 +394,14 @@ public final class BackgroundPlayer extends Service {
} }
@Override @Override
public MediaSource sourceOf(final StreamInfo info, final int sortedStreamsIndex) { public MediaSource sourceOf(final StreamInfo info) {
final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); List<MediaSource> sources = new ArrayList<>();
return buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format)); for (final AudioStream audio : info.audio_streams) {
final MediaSource audioSource = buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format));
sources.add(audioSource);
}
return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()]));
} }
@Override @Override

View File

@ -72,8 +72,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.player.playback.PlaybackManager;
import org.schabi.newpipe.playlist.ExternalPlayQueue; import org.schabi.newpipe.playlist.ExternalPlayQueue;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
@ -139,7 +139,7 @@ public abstract class BasePlayer implements Player.EventListener,
// Playback // Playback
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected PlaybackManager playbackManager; protected MediaSourceManager playbackManager;
protected PlayQueue playQueue; protected PlayQueue playQueue;
private boolean isRecovery = false; private boolean isRecovery = false;
@ -158,7 +158,6 @@ public abstract class BasePlayer implements Player.EventListener,
protected SimpleExoPlayer simpleExoPlayer; protected SimpleExoPlayer simpleExoPlayer;
protected boolean isPrepared = false; protected boolean isPrepared = false;
protected boolean wasPlaying = false;
protected CacheDataSourceFactory cacheDataSourceFactory; protected CacheDataSourceFactory cacheDataSourceFactory;
protected final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); protected final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
@ -297,7 +296,7 @@ public abstract class BasePlayer implements Player.EventListener,
playQueue = queue; playQueue = queue;
playQueue.init(); playQueue.init();
playbackManager = new PlaybackManager(this, playQueue); playbackManager = new MediaSourceManager(this, playQueue);
} }
public void initThumbnail(final String url) { public void initThumbnail(final String url) {
@ -442,14 +441,12 @@ public abstract class BasePlayer implements Player.EventListener,
if (isResumeAfterAudioFocusGain()) { if (isResumeAfterAudioFocusGain()) {
simpleExoPlayer.setPlayWhenReady(true); simpleExoPlayer.setPlayWhenReady(true);
wasPlaying = true;
} }
} }
protected void onAudioFocusLoss() { protected void onAudioFocusLoss() {
if (DEBUG) Log.d(TAG, "onAudioFocusLoss() called"); if (DEBUG) Log.d(TAG, "onAudioFocusLoss() called");
simpleExoPlayer.setPlayWhenReady(false); simpleExoPlayer.setPlayWhenReady(false);
wasPlaying = false;
} }
protected void onAudioFocusLossCanDuck() { protected void onAudioFocusLossCanDuck() {
@ -586,7 +583,7 @@ public abstract class BasePlayer implements Player.EventListener,
} }
// Good to go... // Good to go...
simpleExoPlayer.setPlayWhenReady(wasPlaying); simpleExoPlayer.setPlayWhenReady(true);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -669,11 +666,10 @@ public abstract class BasePlayer implements Player.EventListener,
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with: " + if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with: " +
"window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]"); "window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]");
if (newQueueIndex == -1) { // If the user selects a new track, then the discontinuity occurs after the index is changed.
playQueue.offsetIndex(+1); // Therefore, the only source that causes a discrepancy would be autoplay,
} else { // which can only offset the current track by +1.
playQueue.setIndex(newQueueIndex); if (newQueueIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
}
} }
@Override @Override
@ -690,31 +686,20 @@ public abstract class BasePlayer implements Player.EventListener,
if (simpleExoPlayer == null) return; if (simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Blocking..."); if (DEBUG) Log.d(TAG, "Blocking...");
simpleExoPlayer.removeListener(this);
changeState(STATE_BLOCKED);
wasPlaying = simpleExoPlayer.getPlayWhenReady();
simpleExoPlayer.setPlayWhenReady(false);
}
@Override
public void prepare(final MediaSource mediaSource) {
if (simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Preparing...");
simpleExoPlayer.stop(); simpleExoPlayer.stop();
isPrepared = false; isPrepared = false;
simpleExoPlayer.prepare(mediaSource); changeState(STATE_BLOCKED);
} }
@Override @Override
public void unblock() { public void unblock(final MediaSource mediaSource) {
if (simpleExoPlayer == null) return; if (simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Unblocking..."); if (DEBUG) Log.d(TAG, "Unblocking...");
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
simpleExoPlayer.addListener(this);
simpleExoPlayer.prepare(mediaSource);
} }
@Override @Override
@ -762,7 +747,6 @@ public abstract class BasePlayer implements Player.EventListener,
else playQueue.setIndex(0); else playQueue.setIndex(0);
} }
simpleExoPlayer.setPlayWhenReady(!isPlaying()); simpleExoPlayer.setPlayWhenReady(!isPlaying());
wasPlaying = simpleExoPlayer.getPlayWhenReady();
} }
public void onFastRewind() { public void onFastRewind() {

View File

@ -63,7 +63,7 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.old.PlayVideoActivity; import org.schabi.newpipe.player.old.PlayVideoActivity;
import org.schabi.newpipe.player.playback.PlaybackManager; import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
@ -743,7 +743,7 @@ public final class PopupVideoPlayer extends Service {
public void run() { public void run() {
playerImpl.playQueue = new SinglePlayQueue(info, PlayQueueItem.DEFAULT_QUALITY); playerImpl.playQueue = new SinglePlayQueue(info, PlayQueueItem.DEFAULT_QUALITY);
playerImpl.playQueue.init(); playerImpl.playQueue.init();
playerImpl.playbackManager = new PlaybackManager(playerImpl, playerImpl.playQueue); playerImpl.playbackManager = new MediaSourceManager(playerImpl, playerImpl.playQueue);
} }
}); });
} }

View File

@ -57,7 +57,6 @@ import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.playback.PlaybackManager;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue;
@ -104,6 +103,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
private boolean startedFromNewPipe = true; private boolean startedFromNewPipe = true;
protected boolean wasPlaying = false;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Views // Views
@ -255,24 +255,21 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
buildPlaybackSpeedMenu(playbackSpeedPopupMenu); buildPlaybackSpeedMenu(playbackSpeedPopupMenu);
} }
@Override public MediaSource sourceOf(final StreamInfo info) {
public MediaSource sourceOf(final StreamInfo info, final int sortedStreamsIndex) {
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
List<MediaSource> sources = new ArrayList<>();
final VideoStream video; for (final VideoStream video : videos) {
if (sortedStreamsIndex == PlayQueueItem.DEFAULT_QUALITY) { final MediaSource mediaSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format));
final int index = ListHelper.getDefaultResolutionIndex(context, videos); sources.add(mediaSource);
video = videos.get(index);
} else {
video = videos.get(sortedStreamsIndex);
} }
final MediaSource mediaSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format));
if (!video.isVideoOnly) return mediaSource;
final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams);
final Uri audioUri = Uri.parse(audio.url); final Uri audioUri = Uri.parse(audio.url);
return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null)); final MediaSource audioSource = new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null);
sources.add(audioSource);
return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()]));
} }
public void buildQualityMenu(PopupMenu popupMenu) { public void buildQualityMenu(PopupMenu popupMenu) {

View File

@ -0,0 +1,82 @@
package org.schabi.newpipe.player.mediasource;
import android.os.Looper;
import com.google.android.exoplayer2.C;
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.source.MergingMediaSource;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException;
import java.util.List;
public final class DeferredMediaSource implements MediaSource {
public interface Callback {
MediaSource sourceOf(final StreamInfo info);
}
final private PlayQueueItem stream;
final private Callback callback;
private StreamInfo info;
private MediaSource mediaSource;
private ExoPlayer exoPlayer;
private boolean isTopLevel;
private Listener listener;
public DeferredMediaSource(final PlayQueueItem stream, final Callback callback) {
this.stream = stream;
this.callback = callback;
}
@Override
public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) {
this.exoPlayer = exoPlayer;
this.isTopLevel = isTopLevelSource;
this.listener = listener;
listener.onSourceInfoRefreshed(new SinglePeriodTimeline(C.TIME_UNSET, false), null);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (mediaSource != null) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public MediaPeriod createPeriod(MediaPeriodId mediaPeriodId, Allocator allocator) {
// This must be called on a non-main thread
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new UnsupportedOperationException("Source preparation is blocking, it must be run on non-UI thread.");
}
info = stream.getStream().blockingGet();
mediaSource = callback.sourceOf(info);
mediaSource.prepareSource(exoPlayer, isTopLevel, listener);
return mediaSource.createPeriod(mediaPeriodId, allocator);
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
mediaSource.releasePeriod(mediaPeriod);
}
@Override
public void releaseSource() {
if (mediaSource != null) mediaSource.releaseSource();
info = null;
mediaSource = null;
}
}

View File

@ -8,6 +8,7 @@ import com.google.android.exoplayer2.source.MediaSource;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.DeferredMediaSource;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueMessage;
@ -25,12 +26,12 @@ import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
public class PlaybackManager { public class MediaSourceManager implements DeferredMediaSource.Callback {
private final String TAG = "PlaybackManager@" + Integer.toHexString(hashCode()); private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode());
// One-side rolling window size for default loading // One-side rolling window size for default loading
// Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 // Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback
// todo: inject this parameter, allow user settings perhaps // todo: inject this parameter, allow user settings perhaps
private static final int WINDOW_SIZE = 3; private static final int WINDOW_SIZE = 1;
private final PlaybackListener playbackListener; private final PlaybackListener playbackListener;
private final PlayQueue playQueue; private final PlayQueue playQueue;
@ -46,10 +47,9 @@ public class PlaybackManager {
private CompositeDisposable disposables; private CompositeDisposable disposables;
private boolean isBlocked; private boolean isBlocked;
private boolean hasReset;
public PlaybackManager(@NonNull final PlaybackListener listener, public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) { @NonNull final PlayQueue playQueue) {
this.playbackListener = listener; this.playbackListener = listener;
this.playQueue = playQueue; this.playQueue = playQueue;
@ -114,22 +114,27 @@ public class PlaybackManager {
public void onNext(@NonNull PlayQueueMessage event) { public void onNext(@NonNull PlayQueueMessage event) {
// why no pattern matching in Java =( // why no pattern matching in Java =(
switch (event.type()) { switch (event.type()) {
case INIT:
tryBlock();
resetSources();
break;
case APPEND: case APPEND:
break; break;
case SELECT: case SELECT:
if (isBlocked) break; if (isBlocked) break;
if (isCurrentIndexLoaded()) sync(); else tryBlock(); if (isCurrentIndexLoaded()) {
sync();
} else {
tryBlock();
resetSources();
}
break; break;
case REMOVE: case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event; final RemoveEvent removeEvent = (RemoveEvent) event;
if (!removeEvent.isCurrent()) { if (!removeEvent.isCurrent()) {
remove(removeEvent.index()); remove(removeEvent.index());
break; } else {
tryBlock();
resetSources();
} }
break;
case INIT:
case UPDATE: case UPDATE:
case REORDER: case REORDER:
tryBlock(); tryBlock();
@ -182,13 +187,8 @@ public class PlaybackManager {
private boolean tryUnblock() { private boolean tryUnblock() {
if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) { if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) {
if (hasReset) {
playbackListener.prepare(sources);
hasReset = false;
}
isBlocked = false; isBlocked = false;
playbackListener.unblock(); playbackListener.unblock(sources);
return true; return true;
} }
return false; return false;
@ -208,53 +208,10 @@ public class PlaybackManager {
} }
private void load() { private void load() {
// The current item has higher priority for (final PlayQueueItem item : playQueue.getStreams()) {
final int currentIndex = playQueue.getIndex(); insert(playQueue.indexOf(item), new DeferredMediaSource(item, this));
final PlayQueueItem currentItem = playQueue.get(currentIndex); if (tryUnblock()) sync();
if (currentItem == null) return; }
load(currentItem);
// Load boundaries to ensure correct looping
if (sourceToQueueIndex.indexOf(0) == -1) load(playQueue.get(0));
if (sourceToQueueIndex.indexOf(playQueue.size() - 1) == -1) load(playQueue.get(playQueue.size() - 1));
// The rest are just for seamless playback
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightBound = Math.min(playQueue.size(), currentIndex + WINDOW_SIZE + 1);
final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
for (final PlayQueueItem item: items) load(item);
}
private void load(@Nullable final PlayQueueItem item) {
if (item == null) return;
item.getStream().subscribe(new SingleObserver<StreamInfo>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (disposables == null) {
d.dispose();
return;
}
disposables.add(d);
}
@Override
public void onSuccess(@NonNull StreamInfo streamInfo) {
final MediaSource source = playbackListener.sourceOf(streamInfo, item.getSortedQualityIndex());
final int itemIndex = playQueue.indexOf(item);
// replace all except the currently playing
insert(itemIndex, source, itemIndex != playQueue.getIndex());
if (tryUnblock()) sync();
}
@Override
public void onError(@NonNull Throwable e) {
playQueue.remove(playQueue.indexOf(item));
load();
}
});
} }
private void resetSources() { private void resetSources() {
@ -263,7 +220,6 @@ public class PlaybackManager {
if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear(); if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear();
this.sources = new DynamicConcatenatingMediaSource(); this.sources = new DynamicConcatenatingMediaSource();
this.hasReset = true;
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -272,7 +228,7 @@ public class PlaybackManager {
// Insert source into playlist with position in respect to the play queue // Insert source into playlist with position in respect to the play queue
// If the play queue index already exists, then the insert is ignored // If the play queue index already exists, then the insert is ignored
private void insert(final int queueIndex, final MediaSource source, final boolean replace) { private void insert(final int queueIndex, final MediaSource source) {
if (queueIndex < 0) return; if (queueIndex < 0) return;
int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex); int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex);
@ -280,9 +236,6 @@ public class PlaybackManager {
final int sourceIndex = -pos-1; final int sourceIndex = -pos-1;
sourceToQueueIndex.add(sourceIndex, queueIndex); sourceToQueueIndex.add(sourceIndex, queueIndex);
sources.addMediaSource(sourceIndex, source); sources.addMediaSource(sourceIndex, source);
} else if (replace) {
sources.addMediaSource(pos + 1, source);
sources.removeMediaSource(pos);
} }
} }
@ -300,4 +253,9 @@ public class PlaybackManager {
sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1); sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1);
} }
} }
@Override
public MediaSource sourceOf(StreamInfo info) {
return playbackListener.sourceOf(info);
}
} }

View File

@ -4,6 +4,8 @@ import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.List;
public interface PlaybackListener { public interface PlaybackListener {
/* /*
* Called when the stream at the current queue index is not ready yet. * Called when the stream at the current queue index is not ready yet.
@ -13,23 +15,13 @@ public interface PlaybackListener {
* */ * */
void block(); void block();
/*
* Called when the media source is rebuilt.
* Signals to the listener to prepare the media source again.
* The provided media source is always non-empty.
*
* May be called only after blocking and before unblocking.
* */
void prepare(final MediaSource mediaSource);
/* /*
* Called when the stream at the current queue index is ready. * Called when the stream at the current queue index is ready.
* Signals to the listener to resume the player. * Signals to the listener to resume the player.
* *
* May be called only when the player is blocked. * May be called only when the player is blocked.
* */ * */
void unblock(); void unblock(final MediaSource mediaSource);
/* /*
* Called when the queue index is refreshed. * Called when the queue index is refreshed.
@ -46,7 +38,7 @@ public interface PlaybackListener {
* *
* May be called at any time. * May be called at any time.
* */ * */
MediaSource sourceOf(final StreamInfo info, final int sortedStreamsIndex); MediaSource sourceOf(final StreamInfo info);
/* /*
* Called when the play queue can no longer to played or used. * Called when the play queue can no longer to played or used.