-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:
parent
9576d5bd89
commit
9bc95f030c
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue