From 9bc95f030cf178f9cf45eac8e35d7f3210cb412a Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Tue, 19 Sep 2017 21:46:16 -0700 Subject: [PATCH] -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. --- .../newpipe/player/BackgroundPlayer.java | 15 ++- .../org/schabi/newpipe/player/BasePlayer.java | 40 +++---- .../newpipe/player/PopupVideoPlayer.java | 4 +- .../schabi/newpipe/player/VideoPlayer.java | 23 ++-- .../mediasource/DeferredMediaSource.java | 82 ++++++++++++++ ...ckManager.java => MediaSourceManager.java} | 100 +++++------------- .../player/playback/PlaybackListener.java | 16 +-- 7 files changed, 151 insertions(+), 129 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java rename app/src/main/java/org/schabi/newpipe/player/playback/{PlaybackManager.java => MediaSourceManager.java} (74%) diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 5e9f505bb..71691b8b1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -37,6 +37,7 @@ import android.widget.RemoteViews; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MergingMediaSource; import org.schabi.newpipe.BuildConfig; 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.ThemeHelper; +import java.util.ArrayList; +import java.util.List; + /** * Base players joining the common properties @@ -390,9 +394,14 @@ public final class BackgroundPlayer extends Service { } @Override - public MediaSource sourceOf(final StreamInfo info, final int sortedStreamsIndex) { - final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); - return buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format)); + public MediaSource sourceOf(final StreamInfo info) { + List sources = new ArrayList<>(); + 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 diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index d54022aa7..1fb09d027 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -72,8 +72,8 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfo; 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.PlaybackManager; import org.schabi.newpipe.playlist.ExternalPlayQueue; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; @@ -139,7 +139,7 @@ public abstract class BasePlayer implements Player.EventListener, // Playback //////////////////////////////////////////////////////////////////////////*/ - protected PlaybackManager playbackManager; + protected MediaSourceManager playbackManager; protected PlayQueue playQueue; private boolean isRecovery = false; @@ -158,7 +158,6 @@ public abstract class BasePlayer implements Player.EventListener, protected SimpleExoPlayer simpleExoPlayer; protected boolean isPrepared = false; - protected boolean wasPlaying = false; protected CacheDataSourceFactory cacheDataSourceFactory; protected final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); @@ -297,7 +296,7 @@ public abstract class BasePlayer implements Player.EventListener, playQueue = queue; playQueue.init(); - playbackManager = new PlaybackManager(this, playQueue); + playbackManager = new MediaSourceManager(this, playQueue); } public void initThumbnail(final String url) { @@ -442,14 +441,12 @@ public abstract class BasePlayer implements Player.EventListener, if (isResumeAfterAudioFocusGain()) { simpleExoPlayer.setPlayWhenReady(true); - wasPlaying = true; } } protected void onAudioFocusLoss() { if (DEBUG) Log.d(TAG, "onAudioFocusLoss() called"); simpleExoPlayer.setPlayWhenReady(false); - wasPlaying = false; } protected void onAudioFocusLossCanDuck() { @@ -586,7 +583,7 @@ public abstract class BasePlayer implements Player.EventListener, } // 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: " + "window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]"); - if (newQueueIndex == -1) { - playQueue.offsetIndex(+1); - } else { - playQueue.setIndex(newQueueIndex); - } + // If the user selects a new track, then the discontinuity occurs after the index is changed. + // Therefore, the only source that causes a discrepancy would be autoplay, + // which can only offset the current track by +1. + if (newQueueIndex != playQueue.getIndex()) playQueue.offsetIndex(+1); } @Override @@ -690,31 +686,20 @@ public abstract class BasePlayer implements Player.EventListener, if (simpleExoPlayer == null) return; 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(); isPrepared = false; - simpleExoPlayer.prepare(mediaSource); + changeState(STATE_BLOCKED); } @Override - public void unblock() { + public void unblock(final MediaSource mediaSource) { if (simpleExoPlayer == null) return; if (DEBUG) Log.d(TAG, "Unblocking..."); if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); - simpleExoPlayer.addListener(this); + + simpleExoPlayer.prepare(mediaSource); } @Override @@ -762,7 +747,6 @@ public abstract class BasePlayer implements Player.EventListener, else playQueue.setIndex(0); } simpleExoPlayer.setPlayWhenReady(!isPlaying()); - wasPlaying = simpleExoPlayer.getPlayWhenReady(); } public void onFastRewind() { diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index e138ee3fa..427d109da 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -63,7 +63,7 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; 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.SinglePlayQueue; import org.schabi.newpipe.report.ErrorActivity; @@ -743,7 +743,7 @@ public final class PopupVideoPlayer extends Service { public void run() { playerImpl.playQueue = new SinglePlayQueue(info, PlayQueueItem.DEFAULT_QUALITY); playerImpl.playQueue.init(); - playerImpl.playbackManager = new PlaybackManager(playerImpl, playerImpl.playQueue); + playerImpl.playbackManager = new MediaSourceManager(playerImpl, playerImpl.playQueue); } }); } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 1bb1e830b..4b468650c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -57,7 +57,6 @@ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; 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.PlayQueueItem; 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 boolean startedFromNewPipe = true; + protected boolean wasPlaying = false; /*////////////////////////////////////////////////////////////////////////// // Views @@ -255,24 +255,21 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. buildPlaybackSpeedMenu(playbackSpeedPopupMenu); } - @Override - public MediaSource sourceOf(final StreamInfo info, final int sortedStreamsIndex) { + public MediaSource sourceOf(final StreamInfo info) { final List videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); + List sources = new ArrayList<>(); - final VideoStream video; - if (sortedStreamsIndex == PlayQueueItem.DEFAULT_QUALITY) { - final int index = ListHelper.getDefaultResolutionIndex(context, videos); - video = videos.get(index); - } else { - video = videos.get(sortedStreamsIndex); + for (final VideoStream video : videos) { + final MediaSource mediaSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format)); + sources.add(mediaSource); } - final MediaSource mediaSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format)); - if (!video.isVideoOnly) return mediaSource; - final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); 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) { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java new file mode 100644 index 000000000..581fb6683 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java similarity index 74% rename from app/src/main/java/org/schabi/newpipe/player/playback/PlaybackManager.java rename to app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 5ffa90b74..b3b2a028b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -8,6 +8,7 @@ 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.DeferredMediaSource; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.events.PlayQueueMessage; @@ -25,12 +26,12 @@ import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; -public class PlaybackManager { - private final String TAG = "PlaybackManager@" + Integer.toHexString(hashCode()); +public class MediaSourceManager implements DeferredMediaSource.Callback { + private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode()); // 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 - private static final int WINDOW_SIZE = 3; + private static final int WINDOW_SIZE = 1; private final PlaybackListener playbackListener; private final PlayQueue playQueue; @@ -46,10 +47,9 @@ public class PlaybackManager { private CompositeDisposable disposables; private boolean isBlocked; - private boolean hasReset; - public PlaybackManager(@NonNull final PlaybackListener listener, - @NonNull final PlayQueue playQueue) { + public MediaSourceManager(@NonNull final PlaybackListener listener, + @NonNull final PlayQueue playQueue) { this.playbackListener = listener; this.playQueue = playQueue; @@ -114,22 +114,27 @@ public class PlaybackManager { public void onNext(@NonNull PlayQueueMessage event) { // why no pattern matching in Java =( switch (event.type()) { - case INIT: - tryBlock(); - resetSources(); - break; case APPEND: break; case SELECT: if (isBlocked) break; - if (isCurrentIndexLoaded()) sync(); else tryBlock(); + if (isCurrentIndexLoaded()) { + sync(); + } else { + tryBlock(); + resetSources(); + } break; case REMOVE: final RemoveEvent removeEvent = (RemoveEvent) event; if (!removeEvent.isCurrent()) { remove(removeEvent.index()); - break; + } else { + tryBlock(); + resetSources(); } + break; + case INIT: case UPDATE: case REORDER: tryBlock(); @@ -182,13 +187,8 @@ public class PlaybackManager { private boolean tryUnblock() { if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) { - if (hasReset) { - playbackListener.prepare(sources); - hasReset = false; - } - isBlocked = false; - playbackListener.unblock(); + playbackListener.unblock(sources); return true; } return false; @@ -208,53 +208,10 @@ public class PlaybackManager { } private void load() { - // The current item has higher priority - final int currentIndex = playQueue.getIndex(); - final PlayQueueItem currentItem = playQueue.get(currentIndex); - 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 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() { - @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(); - } - }); + for (final PlayQueueItem item : playQueue.getStreams()) { + insert(playQueue.indexOf(item), new DeferredMediaSource(item, this)); + if (tryUnblock()) sync(); + } } private void resetSources() { @@ -263,7 +220,6 @@ public class PlaybackManager { if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear(); 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 // 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; int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex); @@ -280,9 +236,6 @@ public class PlaybackManager { final int sourceIndex = -pos-1; sourceToQueueIndex.add(sourceIndex, queueIndex); 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); } } + + @Override + public MediaSource sourceOf(StreamInfo info) { + return playbackListener.sourceOf(info); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index c56c7fe93..32bed87ae 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -4,6 +4,8 @@ import com.google.android.exoplayer2.source.MediaSource; import org.schabi.newpipe.extractor.stream.StreamInfo; +import java.util.List; + public interface PlaybackListener { /* * Called when the stream at the current queue index is not ready yet. @@ -13,23 +15,13 @@ public interface PlaybackListener { * */ 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. * Signals to the listener to resume the player. * * May be called only when the player is blocked. * */ - void unblock(); + void unblock(final MediaSource mediaSource); /* * Called when the queue index is refreshed. @@ -46,7 +38,7 @@ public interface PlaybackListener { * * 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.