From 8e3be3826ff6732b67023b7addb676cd314c08d3 Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Sat, 23 Sep 2017 17:02:05 -0700 Subject: [PATCH] -Fixed Deferred Media Source not working on non-extractor (e.g. dash) sources. -Fixed NPE when extracting streams with no audio. --- .../org/schabi/newpipe/player/BasePlayer.java | 29 ++--- .../schabi/newpipe/player/VideoPlayer.java | 8 +- .../mediasource/DeferredMediaSource.java | 115 +++++++++++++----- .../player/playback/MediaSourceManager.java | 71 +++++++---- .../schabi/newpipe/playlist/PlayQueue.java | 7 +- 5 files changed, 156 insertions(+), 74 deletions(-) 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 1fb09d027..180fc3d57 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -551,6 +551,8 @@ public abstract class BasePlayer implements Player.EventListener, //////////////////////////////////////////////////////////////////////////*/ private void refreshTimeline() { + playbackManager.load(); + final int currentSourceIndex = playbackManager.getCurrentSourceIndex(); // Sanity checks @@ -558,15 +560,6 @@ public abstract class BasePlayer implements Player.EventListener, // Check if already playing correct window final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex; - if (isCurrentWindowCorrect && getCurrentState() == STATE_PLAYING) return; - - // Check timeline is up-to-date and has window - if (playbackManager.expectedTimelineSize() != simpleExoPlayer.getCurrentTimeline().getWindowCount()) return; - - // Check if window is ready - Timeline.Window window = new Timeline.Window(); - simpleExoPlayer.getCurrentTimeline().getWindow(currentSourceIndex, window); - if (window.isDynamic) return; // Check if on wrong window if (!isCurrentWindowCorrect) { @@ -576,14 +569,16 @@ public abstract class BasePlayer implements Player.EventListener, } // Check if recovering - if (isRecovery && queuePos == playQueue.getIndex() && isCurrentWindowCorrect) { - if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + " at: " + getTimeString((int)videoPos)); - simpleExoPlayer.seekTo(videoPos); + if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) { + // todo: figure out exactly why this is the case + /* Rounding time to nearest second as certain media cannot guarantee a sub-second seek + will complete and the player might get stuck in buffering state forever */ + final long roundedPos = (videoPos / 1000) * 1000; + + if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + " at: " + getTimeString((int)roundedPos)); + simpleExoPlayer.seekTo(roundedPos); isRecovery = false; } - - // Good to go... - simpleExoPlayer.setPlayWhenReady(true); } /*////////////////////////////////////////////////////////////////////////// @@ -628,7 +623,9 @@ public abstract class BasePlayer implements Player.EventListener, isPrepared = false; break; case Player.STATE_BUFFERING: // 2 - if (isPrepared) changeState(STATE_BUFFERING); + if (isPrepared) { + changeState(STATE_BUFFERING); + } break; case Player.STATE_READY: //3 if (!isPrepared) { 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 4b468650c..7d7fa3bdf 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -265,9 +265,11 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. } final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); - final Uri audioUri = Uri.parse(audio.url); - final MediaSource audioSource = new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null); - sources.add(audioSource); + if (audio != null) { + final Uri audioUri = Uri.parse(audio.url); + final MediaSource audioSource = new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null); + sources.add(audioSource); + } return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()])); } 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 index 581fb6683..c85161e60 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java @@ -1,53 +1,107 @@ package org.schabi.newpipe.player.mediasource; -import android.os.Looper; +import android.support.annotation.NonNull; +import android.util.Log; -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; + +import io.reactivex.SingleObserver; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; public final class DeferredMediaSource implements MediaSource { + private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode()); + + private int state = -1; + + public final static int STATE_INIT = 0; + public final static int STATE_PREPARED = 1; + public final static int STATE_LOADED = 2; + public final static int STATE_DISPOSED = 3; public interface Callback { MediaSource sourceOf(final StreamInfo info); } - final private PlayQueueItem stream; - final private Callback callback; + private PlayQueueItem stream; + private Callback callback; - private StreamInfo info; private MediaSource mediaSource; - private ExoPlayer exoPlayer; - private boolean isTopLevel; - private Listener listener; + private Disposable loader; - public DeferredMediaSource(final PlayQueueItem stream, final Callback callback) { + private ExoPlayer exoPlayer; + private Listener listener; + private Throwable error; + + public DeferredMediaSource(@NonNull final PlayQueueItem stream, + @NonNull final Callback callback) { this.stream = stream; this.callback = callback; + this.state = STATE_INIT; } @Override public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) { this.exoPlayer = exoPlayer; - this.isTopLevel = isTopLevelSource; this.listener = listener; + this.state = STATE_PREPARED; + } - listener.onSourceInfoRefreshed(new SinglePeriodTimeline(C.TIME_UNSET, false), null); + public int state() { + return state; + } + + public synchronized void load() { + if (state != STATE_PREPARED || stream == null || loader != null) return; + Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); + + final Consumer onSuccess = new Consumer() { + @Override + public void accept(StreamInfo streamInfo) throws Exception { + if (exoPlayer == null && listener == null) { + error = new Throwable("Stream info loading failed. URL: " + stream.getUrl()); + } else { + Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl()); + + mediaSource = callback.sourceOf(streamInfo); + mediaSource.prepareSource(exoPlayer, false, listener); + state = STATE_LOADED; + } + } + }; + + final Consumer onError = new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + Log.e(TAG, "Loading error:", throwable); + error = throwable; + state = STATE_LOADED; + } + }; + + loader = stream.getStream() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(onSuccess, onError); } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { + if (error != null) { + throw new IOException(error); + } + if (mediaSource != null) { mediaSource.maybeThrowSourceInfoRefreshError(); } @@ -55,28 +109,33 @@ public final class DeferredMediaSource implements MediaSource { @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); + if (mediaSource == null) { + Log.e(TAG, "releasePeriod() called when media source is null, memory leak may have occurred."); + } else { + mediaSource.releasePeriod(mediaPeriod); + } } @Override public void releaseSource() { - if (mediaSource != null) mediaSource.releaseSource(); - info = null; - mediaSource = null; + state = STATE_DISPOSED; + + if (mediaSource != null) { + mediaSource.releaseSource(); + } + if (loader != null) { + loader.dispose(); + } + + /* Do not set mediaSource as null here as it may be called through releasePeriod */ + stream = null; + callback = null; + exoPlayer = null; + listener = null; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index b3b2a028b..3752136b5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.playback; import android.support.annotation.Nullable; +import android.util.Log; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -13,16 +14,13 @@ import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.RemoveEvent; -import org.schabi.newpipe.playlist.events.UpdateEvent; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import io.reactivex.SingleObserver; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; @@ -44,7 +42,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { private Subscription playQueueReactor; private Disposable syncReactor; - private CompositeDisposable disposables; private boolean isBlocked; @@ -53,8 +50,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { this.playbackListener = listener; this.playQueue = playQueue; - this.disposables = new CompositeDisposable(); - this.sources = new DynamicConcatenatingMediaSource(); this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList()); @@ -85,18 +80,35 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { public void dispose() { if (playQueueReactor != null) playQueueReactor.cancel(); - if (disposables != null) disposables.dispose(); if (syncReactor != null) syncReactor.dispose(); if (sources != null) sources.releaseSource(); if (sourceToQueueIndex != null) sourceToQueueIndex.clear(); playQueueReactor = null; - disposables = null; syncReactor = null; sources = null; sourceToQueueIndex = null; } + public 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); + + // The rest are just for seamless playback + final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); + final int rightLimit = currentIndex + WINDOW_SIZE + 1; + final int rightBound = Math.min(playQueue.size(), rightLimit); + final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); + + 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) load(item); + } + /*////////////////////////////////////////////////////////////////////////// // Event Reactor //////////////////////////////////////////////////////////////////////////*/ @@ -115,30 +127,26 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { // why no pattern matching in Java =( switch (event.type()) { case APPEND: + populateSources(); break; case SELECT: - if (isBlocked) break; if (isCurrentIndexLoaded()) { sync(); - } else { - tryBlock(); - resetSources(); } break; case REMOVE: final RemoveEvent removeEvent = (RemoveEvent) event; if (!removeEvent.isCurrent()) { remove(removeEvent.index()); - } else { - tryBlock(); - resetSources(); + break; } - break; case INIT: case UPDATE: case REORDER: tryBlock(); resetSources(); + populateSources(); + if (tryUnblock()) sync(); break; default: break; @@ -204,31 +212,46 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { } }; - currentItem.getStream().subscribe(syncPlayback); + final Consumer onError = new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + Log.e(TAG, "Sync error:", throwable); + } + }; + + currentItem.getStream().subscribe(syncPlayback, onError); } - private void load() { - for (final PlayQueueItem item : playQueue.getStreams()) { - insert(playQueue.indexOf(item), new DeferredMediaSource(item, this)); - if (tryUnblock()) sync(); - } + private void load(@Nullable final PlayQueueItem item) { + if (item == null) return; + + final int index = playQueue.indexOf(item); + if (index > sources.getSize() - 1) return; + + final DeferredMediaSource mediaSource = (DeferredMediaSource) sources.getMediaSource(playQueue.indexOf(item)); + if (mediaSource.state() == DeferredMediaSource.STATE_PREPARED) mediaSource.load(); } 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(); } + private void populateSources() { + for (final PlayQueueItem item : playQueue.getStreams()) { + insert(playQueue.indexOf(item), new DeferredMediaSource(item, this)); + } + } + /*////////////////////////////////////////////////////////////////////////// // Media Source List Manipulation //////////////////////////////////////////////////////////////////////////*/ // 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) { + private void insert(final int queueIndex, final DeferredMediaSource source) { if (queueIndex < 0) return; int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex); diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java index 2b9f7a7ee..badc1cf86 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -15,6 +15,7 @@ import org.schabi.newpipe.playlist.events.UpdateEvent; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -147,9 +148,9 @@ public abstract class PlayQueue implements Serializable { broadcast(new UpdateEvent(index)); } - protected synchronized void append(final PlayQueueItem item) { - streams.add(item); - broadcast(new AppendEvent(1)); + protected synchronized void append(final PlayQueueItem... items) { + streams.addAll(Arrays.asList(items)); + broadcast(new AppendEvent(items.length)); } protected synchronized void append(final Collection items) {