From 8803b60b28e0cc1ef292610bead89a115d76f1ea Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 22 Feb 2018 13:30:48 -0800 Subject: [PATCH 01/19] -Updated Exoplayer to 2.7.0. -PoC for new seamless stream loading mechanism. --- app/build.gradle | 2 +- .../fragments/list/search/SearchFragment.java | 59 +-- .../org/schabi/newpipe/player/BasePlayer.java | 31 +- .../schabi/newpipe/player/VideoPlayer.java | 8 +- .../newpipe/player/helper/LoadController.java | 33 +- .../player/mediasource/FailedMediaSource.java | 72 ++++ .../player/mediasource/LoadedMediaSource.java | 75 ++++ .../mediasource/ManagedMediaSource.java | 7 + .../mediasource/PlaceholderMediaSource.java | 22 ++ .../player/playback/MediaSourceManager.java | 1 - .../playback/MediaSourceManagerAlt.java | 369 ++++++++++++++++++ 11 files changed, 617 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java diff --git a/app/build.gradle b/app/build.gradle index c5887faed..c9bd8d003 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.nononsenseapps:filepicker:3.0.1' - implementation 'com.google.android.exoplayer:exoplayer:2.6.0' + implementation 'com.google.android.exoplayer:exoplayer:2.7.0' debugImplementation 'com.facebook.stetho:stetho:1.5.0' debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 9c9aeb080..1ad31d06c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -527,23 +527,26 @@ public class SearchFragment extends BaseListFragment suggestionPublisher - .onNext(searchEditText.getText().toString()), - - throwable -> showSnackBarError(throwable, - UserAction.SOMETHING_ELSE, "none", - "Deleting item failed", R.string.general_error) - ); - + if (activity == null || historyRecordManager == null || suggestionPublisher == null || + searchEditText == null || disposables == null) return; + final String query = item.query; new AlertDialog.Builder(activity) - .setTitle(item.query) + .setTitle(query) .setMessage(R.string.delete_item_search_history) .setCancelable(true) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete)) + .setPositiveButton(R.string.delete, (dialog, which) -> { + final Disposable onDelete = historyRecordManager.deleteSearchHistory(query) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + howManyDeleted -> suggestionPublisher + .onNext(searchEditText.getText().toString()), + throwable -> showSnackBarError(throwable, + UserAction.SOMETHING_ELSE, "none", + "Deleting item failed", R.string.general_error) + ); + disposables.add(onDelete); + }) .show(); } @@ -701,19 +704,8 @@ public class SearchFragment extends BaseListFragment() { - @Override - public void accept(@NonNull SearchResult result) throws Exception { - isLoading.set(false); - handleResult(result); - } - }, new Consumer() { - @Override - public void accept(@NonNull Throwable throwable) throws Exception { - isLoading.set(false); - onError(throwable); - } - }); + .doOnEvent((searchResult, throwable) -> isLoading.set(false)) + .subscribe(this::handleResult, this::onError); } @Override @@ -725,19 +717,8 @@ public class SearchFragment extends BaseListFragment() { - @Override - public void accept(@NonNull ListExtractor.InfoItemPage result) throws Exception { - isLoading.set(false); - handleNextItems(result); - } - }, new Consumer() { - @Override - public void accept(@NonNull Throwable throwable) throws Exception { - isLoading.set(false); - onError(throwable); - } - }); + .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) + .subscribe(this::handleNextItems, this::onError); } @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 222f0fad8..9ee83427d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -46,6 +46,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; @@ -67,7 +68,7 @@ import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; -import org.schabi.newpipe.player.playback.MediaSourceManager; +import org.schabi.newpipe.player.playback.MediaSourceManagerAlt; import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueAdapter; @@ -124,7 +125,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f}; - protected MediaSourceManager playbackManager; + protected MediaSourceManagerAlt playbackManager; protected PlayQueue playQueue; protected StreamInfo currentInfo; @@ -150,6 +151,12 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected DataSource.Factory cacheDataSourceFactory; protected DefaultExtractorsFactory extractorsFactory; + protected SsMediaSource.Factory ssMediaSourceFactory; + protected HlsMediaSource.Factory hlsMediaSourceFactory; + protected DashMediaSource.Factory dashMediaSourceFactory; + protected ExtractorMediaSource.Factory extractorMediaSourceFactory; + protected SingleSampleMediaSource.Factory sampleMediaSourceFactory; + protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; @@ -192,6 +199,14 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen extractorsFactory = new DefaultExtractorsFactory(); cacheDataSourceFactory = new CacheFactory(context); + ssMediaSourceFactory = new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory); + hlsMediaSourceFactory = new HlsMediaSource.Factory(cacheDataSourceFactory); + dashMediaSourceFactory = new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory); + extractorMediaSourceFactory = new ExtractorMediaSource.Factory(cacheDataSourceFactory); + sampleMediaSourceFactory = new SingleSampleMediaSource.Factory(cacheDataSourceFactory); + simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); audioReactor = new AudioReactor(context, simpleExoPlayer); @@ -247,7 +262,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected void initPlayback(final PlayQueue queue) { playQueue = queue; playQueue.init(); - playbackManager = new MediaSourceManager(this, playQueue); + playbackManager = new MediaSourceManagerAlt(this, playQueue); if (playQueueAdapter != null) playQueueAdapter.dispose(); playQueueAdapter = new PlayQueueAdapter(context, playQueue); @@ -310,16 +325,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen MediaSource mediaSource; switch (type) { case C.TYPE_SS: - mediaSource = new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null); + mediaSource = ssMediaSourceFactory.createMediaSource(uri); break; case C.TYPE_DASH: - mediaSource = new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null); + mediaSource = dashMediaSourceFactory.createMediaSource(uri); break; case C.TYPE_HLS: - mediaSource = new HlsMediaSource(uri, cacheDataSourceFactory, null, null); + mediaSource = hlsMediaSourceFactory.createMediaSource(uri); break; case C.TYPE_OTHER: - mediaSource = new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null); + mediaSource = extractorMediaSourceFactory.createMediaSource(uri); break; default: { throw new IllegalStateException("Unsupported type: " + type); @@ -489,7 +504,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); if (playbackManager != null) { 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 40b7df2dc..f8844c15e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -305,8 +305,8 @@ public abstract class VideoPlayer extends BasePlayer captionItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setParameters(trackSelector.getParameters() - .withPreferredTextLanguage(captionLanguage)); + trackSelector.setParameters(trackSelector.getParameters().buildUpon() + .setPreferredTextLanguage(captionLanguage).build()); trackSelector.setRendererDisabled(textRendererIndex, false); } return true; @@ -395,8 +395,8 @@ public abstract class VideoPlayer extends BasePlayer final Format textFormat = Format.createTextSampleFormat(null, mimeType, SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); - final MediaSource textSource = new SingleSampleMediaSource( - Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET); + final MediaSource textSource = sampleMediaSourceFactory.createMediaSource( + Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); mediaSources.add(textSource); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index be7b8efde..15668be90 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -12,6 +12,8 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; +import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS; +import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES; public class LoadController implements LoadControl { @@ -29,14 +31,14 @@ public class LoadController implements LoadControl { PlayerHelper.getBufferForPlaybackMs(context)); } - public LoadController(final int minBufferMs, - final int maxBufferMs, - final int bufferForPlaybackMs) { + private LoadController(final int minBufferMs, final int maxBufferMs, + final int bufferForPlaybackMs) { final DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, - bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); + bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } /*////////////////////////////////////////////////////////////////////////// @@ -49,7 +51,8 @@ public class LoadController implements LoadControl { } @Override - public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, TrackSelectionArray trackSelectionArray) { + public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, + TrackSelectionArray trackSelectionArray) { internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray); } @@ -69,12 +72,24 @@ public class LoadController implements LoadControl { } @Override - public boolean shouldStartPlayback(long l, boolean b) { - return internalLoadControl.shouldStartPlayback(l, b); + public long getBackBufferDurationUs() { + return internalLoadControl.getBackBufferDurationUs(); } @Override - public boolean shouldContinueLoading(long l) { - return internalLoadControl.shouldContinueLoading(l); + public boolean retainBackBufferFromKeyframe() { + return internalLoadControl.retainBackBufferFromKeyframe(); + } + + @Override + public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { + return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); + } + + @Override + public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, + boolean rebuffering) { + return internalLoadControl.shouldStartPlayback(bufferedDurationUs, playbackSpeed, + rebuffering); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java new file mode 100644 index 000000000..c4a44f503 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -0,0 +1,72 @@ +package org.schabi.newpipe.player.mediasource; + +import android.support.annotation.NonNull; + +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.upstream.Allocator; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.playlist.PlayQueueItem; + +import java.io.IOException; + +public class FailedMediaSource implements ManagedMediaSource { + + private final PlayQueueItem playQueueItem; + private final Throwable error; + + private final long retryTimestamp; + + public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, + @NonNull final Throwable error, + final long retryTimestamp) { + this.playQueueItem = playQueueItem; + this.error = error; + this.retryTimestamp = retryTimestamp; + } + + public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, + @NonNull final Throwable error) { + this.playQueueItem = playQueueItem; + this.error = error; + this.retryTimestamp = Long.MAX_VALUE; + } + + public PlayQueueItem getPlayQueueItem() { + return playQueueItem; + } + + public Throwable getError() { + return error; + } + + public boolean canRetry() { + return System.currentTimeMillis() >= retryTimestamp; + } + + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + throw new IOException(error); + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + return null; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) {} + + @Override + public void releaseSource() {} + + @Override + public boolean canReplace() { + return canRetry(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java new file mode 100644 index 000000000..45a079d2b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -0,0 +1,75 @@ +package org.schabi.newpipe.player.mediasource; + +import android.support.annotation.NonNull; + +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.upstream.Allocator; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.playlist.PlayQueueItem; + +import java.io.IOException; + +public class LoadedMediaSource implements ManagedMediaSource { + + private final PlayQueueItem playQueueItem; + private final StreamInfo streamInfo; + private final MediaSource source; + + private final long expireTimestamp; + + public LoadedMediaSource(@NonNull final PlayQueueItem playQueueItem, + @NonNull final StreamInfo streamInfo, + @NonNull final MediaSource source, + final long expireTimestamp) { + this.playQueueItem = playQueueItem; + this.streamInfo = streamInfo; + this.source = source; + + this.expireTimestamp = expireTimestamp; + } + + public PlayQueueItem getPlayQueueItem() { + return playQueueItem; + } + + public StreamInfo getStreamInfo() { + return streamInfo; + } + + public boolean isExpired() { + return System.currentTimeMillis() >= expireTimestamp; + } + + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + source.prepareSource(player, isTopLevelSource, listener); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + source.maybeThrowSourceInfoRefreshError(); + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + return source.createPeriod(id, allocator); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + source.releasePeriod(mediaPeriod); + } + + @Override + public void releaseSource() { + source.releaseSource(); + } + + @Override + public boolean canReplace() { + return isExpired(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java new file mode 100644 index 000000000..5ac07c9f0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.player.mediasource; + +import com.google.android.exoplayer2.source.MediaSource; + +public interface ManagedMediaSource extends MediaSource { + boolean canReplace(); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java new file mode 100644 index 000000000..0a389a9d9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -0,0 +1,22 @@ +package org.schabi.newpipe.player.mediasource; + +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.upstream.Allocator; + +import java.io.IOException; + +public class PlaceholderMediaSource implements ManagedMediaSource { + // Do nothing, so this will stall the playback + @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} + @Override public void maybeThrowSourceInfoRefreshError() throws IOException {} + @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; } + @Override public void releasePeriod(MediaPeriod mediaPeriod) {} + @Override public void releaseSource() {} + + @Override + public boolean canReplace() { + return true; + } +} 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 54eb4078a..9dea4fdce 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 @@ -20,7 +20,6 @@ import java.util.concurrent.TimeUnit; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; import io.reactivex.functions.Consumer; diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java new file mode 100644 index 000000000..a306ff859 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java @@ -0,0 +1,369 @@ +package org.schabi.newpipe.player.playback; + +import android.support.annotation.Nullable; + +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +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.FailedMediaSource; +import org.schabi.newpipe.player.mediasource.LoadedMediaSource; +import org.schabi.newpipe.player.mediasource.ManagedMediaSource; +import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.playlist.events.MoveEvent; +import org.schabi.newpipe.playlist.events.PlayQueueEvent; +import org.schabi.newpipe.playlist.events.RemoveEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.subjects.PublishSubject; + +public class MediaSourceManagerAlt { + // One-side rolling window size for default loading + // Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0 + private final int windowSize; + private final PlaybackListener playbackListener; + private final PlayQueue playQueue; + + // Process only the last load order when receiving a stream of load orders (lessens I/O) + // The higher it is, the less loading occurs during rapid noncritical timeline changes + // Not recommended to go below 100ms + private final long loadDebounceMillis; + private final PublishSubject debouncedLoadSignal; + private final Disposable debouncedLoader; + + private DynamicConcatenatingMediaSource sources; + + private Subscription playQueueReactor; + private CompositeDisposable loaderReactor; + + private PlayQueueItem syncedItem; + + private boolean isBlocked; + + public MediaSourceManagerAlt(@NonNull final PlaybackListener listener, + @NonNull final PlayQueue playQueue) { + this(listener, playQueue, 1, 400L); + } + + private MediaSourceManagerAlt(@NonNull final PlaybackListener listener, + @NonNull final PlayQueue playQueue, + final int windowSize, + final long loadDebounceMillis) { + if (windowSize <= 0) { + throw new UnsupportedOperationException("MediaSourceManager window size must be greater than 0"); + } + + this.playbackListener = listener; + this.playQueue = playQueue; + this.windowSize = windowSize; + this.loadDebounceMillis = loadDebounceMillis; + + this.loaderReactor = new CompositeDisposable(); + this.debouncedLoadSignal = PublishSubject.create(); + this.debouncedLoader = getDebouncedLoader(); + + this.sources = new DynamicConcatenatingMediaSource(); + + playQueue.getBroadcastReceiver() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getReactor()); + } + + /*////////////////////////////////////////////////////////////////////////// + // Exposed Methods + //////////////////////////////////////////////////////////////////////////*/ + /** + * Dispose the manager and releases all message buses and loaders. + * */ + public void dispose() { + if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete(); + if (debouncedLoader != null) debouncedLoader.dispose(); + if (playQueueReactor != null) playQueueReactor.cancel(); + if (loaderReactor != null) loaderReactor.dispose(); + if (sources != null) sources.releaseSource(); + + playQueueReactor = null; + loaderReactor = null; + syncedItem = null; + sources = null; + } + + /** + * Loads the current playing stream and the streams within its windowSize bound. + * + * Unblocks the player once the item at the current index is loaded. + * */ + public void load() { + loadDebounced(); + } + + /** + * Blocks the player and repopulate the sources. + * + * Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}. + * */ + public void reset() { + tryBlock(); + + syncedItem = null; + populateSources(); + } + /*////////////////////////////////////////////////////////////////////////// + // Event Reactor + //////////////////////////////////////////////////////////////////////////*/ + + private Subscriber getReactor() { + return new Subscriber() { + @Override + public void onSubscribe(@NonNull Subscription d) { + if (playQueueReactor != null) playQueueReactor.cancel(); + playQueueReactor = d; + playQueueReactor.request(1); + } + + @Override + public void onNext(@NonNull PlayQueueEvent playQueueMessage) { + if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage); + } + + @Override + public void onError(@NonNull Throwable e) {} + + @Override + public void onComplete() {} + }; + } + + private void onPlayQueueChanged(final PlayQueueEvent event) { + if (playQueue.isEmpty() && playQueue.isComplete()) { + playbackListener.shutdown(); + return; + } + + // Event specific action + switch (event.type()) { + case INIT: + case REORDER: + case ERROR: + reset(); + break; + case APPEND: + populateSources(); + break; + case REMOVE: + final RemoveEvent removeEvent = (RemoveEvent) event; + remove(removeEvent.getRemoveIndex()); + break; + case MOVE: + final MoveEvent moveEvent = (MoveEvent) event; + move(moveEvent.getFromIndex(), moveEvent.getToIndex()); + break; + case SELECT: + case RECOVERY: + default: + break; + } + + // Loading and Syncing + switch (event.type()) { + case INIT: + case REORDER: + case ERROR: + loadImmediate(); // low frequency, critical events + break; + case APPEND: + case REMOVE: + case SELECT: + case MOVE: + case RECOVERY: + default: + loadDebounced(); // high frequency or noncritical events + break; + } + + if (!isPlayQueueReady()) { + tryBlock(); + playQueue.fetch(); + } + if (playQueueReactor != null) playQueueReactor.request(1); + } + + /*////////////////////////////////////////////////////////////////////////// + // Internal Helpers + //////////////////////////////////////////////////////////////////////////*/ + + private boolean isPlayQueueReady() { + final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > windowSize; + return playQueue.isComplete() || isWindowLoaded; + } + + private boolean tryBlock() { + if (!isBlocked) { + playbackListener.block(); + resetSources(); + isBlocked = true; + return true; + } + return false; + } + + private boolean tryUnblock() { + if (isPlayQueueReady() && isBlocked && sources != null) { + isBlocked = false; + playbackListener.unblock(sources); + return true; + } + return false; + } + + private void sync(final PlayQueueItem item, final StreamInfo info) { + final PlayQueueItem currentItem = playQueue.getItem(); + if (currentItem != item || syncedItem == item || playbackListener == null) return; + + syncedItem = currentItem; + // Ensure the current item is up to date with the play queue + if (playQueue.getItem() == currentItem && playQueue.getItem() == syncedItem) { + playbackListener.sync(syncedItem, info); + } + } + + private void loadDebounced() { + debouncedLoadSignal.onNext(System.currentTimeMillis()); + } + + private void loadImmediate() { + // The current item has higher priority + final int currentIndex = playQueue.getIndex(); + final PlayQueueItem currentItem = playQueue.getItem(currentIndex); + if (currentItem == null) return; + loadItem(currentItem); + + // The rest are just for seamless playback + final int leftBound = Math.max(0, currentIndex - windowSize); + final int rightLimit = currentIndex + windowSize + 1; + final int rightBound = Math.min(playQueue.size(), rightLimit); + final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); + + // Do a round robin + 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) loadItem(item); + } + + private void loadItem(@Nullable final PlayQueueItem item) { + if (sources == null || item == null) return; + + final int index = playQueue.indexOf(item); + if (index > sources.getSize() - 1) return; + + if (((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) { + final Disposable loader = getMediaSource(item) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(mediaSource -> update(playQueue.indexOf(item), mediaSource)); + loaderReactor.add(loader); + } + + tryUnblock(); + if (!isBlocked) { + final MediaSource mediaSource = sources.getMediaSource(playQueue.indexOf(item)); + final StreamInfo info = mediaSource instanceof LoadedMediaSource ? + ((LoadedMediaSource) mediaSource).getStreamInfo() : null; + sync(item, info); + } + } + + private void resetSources() { + if (this.sources != null) this.sources.releaseSource(); + this.sources = new DynamicConcatenatingMediaSource(); + } + + private void populateSources() { + if (sources == null) return; + + for (final PlayQueueItem item : playQueue.getStreams()) { + insert(playQueue.indexOf(item), new PlaceholderMediaSource()); + } + } + + private Disposable getDebouncedLoader() { + return debouncedLoadSignal + .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(timestamp -> loadImmediate()); + } + + private Single getMediaSource(@NonNull final PlayQueueItem stream) { + return stream.getStream().map(streamInfo -> { + if (playbackListener == null) { + return new FailedMediaSource(stream, new IllegalStateException( + "MediaSourceManager playback listener unavailable")); + } + + final MediaSource source = playbackListener.sourceOf(stream, streamInfo); + if (source == null) { + return new FailedMediaSource(stream, new IllegalStateException( + "MediaSource resolution is null")); + } + + return new LoadedMediaSource(stream, streamInfo, source, + TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS)); + }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); + } + /*////////////////////////////////////////////////////////////////////////// + // Media Source List Manipulation + //////////////////////////////////////////////////////////////////////////*/ + + private void update(final int queueIndex, final MediaSource source) { + if (sources == null) return; + if (queueIndex < 0 || queueIndex < sources.getSize()) return; + + sources.addMediaSource(queueIndex + 1, source); + sources.removeMediaSource(queueIndex); + } + + /** + * Inserts a source into {@link DynamicConcatenatingMediaSource} 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 PlaceholderMediaSource source) { + if (sources == null) return; + if (queueIndex < 0 || queueIndex < sources.getSize()) return; + + sources.addMediaSource(queueIndex, source); + } + + /** + * Removes a source from {@link DynamicConcatenatingMediaSource} with the given play queue index. + * + * If the play queue index does not exist, the removal is ignored. + * */ + private void remove(final int queueIndex) { + if (sources == null) return; + if (queueIndex < 0 || queueIndex > sources.getSize()) return; + + sources.removeMediaSource(queueIndex); + } + + private void move(final int source, final int target) { + if (sources == null) return; + if (source < 0 || target < 0) return; + if (source >= sources.getSize() || target >= sources.getSize()) return; + + sources.moveMediaSource(source, target); + } +} From 19cbcd0c1d02e30a4011db7b169e1f32e2e2a321 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 23 Feb 2018 19:00:08 -0800 Subject: [PATCH 02/19] -Fixed media source update index check. -Fixed media source manager excessive loading. -Remove unneeded fields in loaded media source. --- .../player/mediasource/LoadedMediaSource.java | 23 +- .../playback/MediaSourceManagerAlt.java | 213 +++++++++++------- 2 files changed, 135 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index 45a079d2b..ddc78bd77 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -14,35 +14,16 @@ import java.io.IOException; public class LoadedMediaSource implements ManagedMediaSource { - private final PlayQueueItem playQueueItem; - private final StreamInfo streamInfo; private final MediaSource source; private final long expireTimestamp; - public LoadedMediaSource(@NonNull final PlayQueueItem playQueueItem, - @NonNull final StreamInfo streamInfo, - @NonNull final MediaSource source, - final long expireTimestamp) { - this.playQueueItem = playQueueItem; - this.streamInfo = streamInfo; + public LoadedMediaSource(@NonNull final MediaSource source, final long expireTimestamp) { this.source = source; this.expireTimestamp = expireTimestamp; } - public PlayQueueItem getPlayQueueItem() { - return playQueueItem; - } - - public StreamInfo getStreamInfo() { - return streamInfo; - } - - public boolean isExpired() { - return System.currentTimeMillis() >= expireTimestamp; - } - @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { source.prepareSource(player, isTopLevelSource, listener); @@ -70,6 +51,6 @@ public class LoadedMediaSource implements ManagedMediaSource { @Override public boolean canReplace() { - return isExpired(); + return System.currentTimeMillis() >= expireTimestamp; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java index a306ff859..03b583e07 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java @@ -19,7 +19,10 @@ import org.schabi.newpipe.playlist.events.PlayQueueEvent; import org.schabi.newpipe.playlist.events.RemoveEvent; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; import io.reactivex.Single; @@ -27,6 +30,8 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.SerialDisposable; +import io.reactivex.functions.Consumer; import io.reactivex.subjects.PublishSubject; public class MediaSourceManagerAlt { @@ -48,21 +53,24 @@ public class MediaSourceManagerAlt { private Subscription playQueueReactor; private CompositeDisposable loaderReactor; - private PlayQueueItem syncedItem; - private boolean isBlocked; + private SerialDisposable syncReactor; + private PlayQueueItem syncedItem; + private Set loadingItems; + public MediaSourceManagerAlt(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { - this(listener, playQueue, 1, 400L); + this(listener, playQueue, 0, 400L); } private MediaSourceManagerAlt(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue, final int windowSize, final long loadDebounceMillis) { - if (windowSize <= 0) { - throw new UnsupportedOperationException("MediaSourceManager window size must be greater than 0"); + if (windowSize < 0) { + throw new UnsupportedOperationException( + "MediaSourceManager window size must be greater than 0"); } this.playbackListener = listener; @@ -76,6 +84,9 @@ public class MediaSourceManagerAlt { this.sources = new DynamicConcatenatingMediaSource(); + this.syncReactor = new SerialDisposable(); + this.loadingItems = Collections.synchronizedSet(new HashSet<>()); + playQueue.getBroadcastReceiver() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getReactor()); @@ -92,10 +103,12 @@ public class MediaSourceManagerAlt { if (debouncedLoader != null) debouncedLoader.dispose(); if (playQueueReactor != null) playQueueReactor.cancel(); if (loaderReactor != null) loaderReactor.dispose(); + if (syncReactor != null) syncReactor.dispose(); if (sources != null) sources.releaseSource(); playQueueReactor = null; loaderReactor = null; + syncReactor = null; syncedItem = null; sources = null; } @@ -112,7 +125,8 @@ public class MediaSourceManagerAlt { /** * Blocks the player and repopulate the sources. * - * Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}. + * Does not ensure the player is unblocked and should be done explicitly + * through {@link #load() load}. * */ public void reset() { tryBlock(); @@ -201,7 +215,7 @@ public class MediaSourceManagerAlt { } /*////////////////////////////////////////////////////////////////////////// - // Internal Helpers + // Playback Locking //////////////////////////////////////////////////////////////////////////*/ private boolean isPlayQueueReady() { @@ -209,33 +223,74 @@ public class MediaSourceManagerAlt { return playQueue.isComplete() || isWindowLoaded; } - private boolean tryBlock() { - if (!isBlocked) { - playbackListener.block(); - resetSources(); - isBlocked = true; - return true; - } - return false; + private boolean isPlaybackReady() { + return sources.getSize() > 0 && + sources.getMediaSource(playQueue.getIndex()) instanceof LoadedMediaSource; } - private boolean tryUnblock() { - if (isPlayQueueReady() && isBlocked && sources != null) { + private void tryBlock() { + if (isBlocked) return; + + playbackListener.block(); + + if (this.sources != null) this.sources.releaseSource(); + this.sources = new DynamicConcatenatingMediaSource(); + + isBlocked = true; + } + + private void tryUnblock() { + if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) { isBlocked = false; playbackListener.unblock(sources); - return true; } - return false; } - private void sync(final PlayQueueItem item, final StreamInfo info) { - final PlayQueueItem currentItem = playQueue.getItem(); - if (currentItem != item || syncedItem == item || playbackListener == null) return; + /*////////////////////////////////////////////////////////////////////////// + // Metadata Synchronization TODO: maybe this should be a separate manager + //////////////////////////////////////////////////////////////////////////*/ - syncedItem = currentItem; + private void sync() { + final PlayQueueItem currentItem = playQueue.getItem(); + if (isBlocked || currentItem == null) return; + + final Consumer onSuccess = info -> syncInternal(currentItem, info); + final Consumer onError = throwable -> syncInternal(currentItem, null); + + if (syncedItem != currentItem) { + syncedItem = currentItem; + final Disposable sync = currentItem.getStream() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(onSuccess, onError); + syncReactor.set(sync); + } + } + + private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item, + @Nullable final StreamInfo info) { + if (playQueue == null || playbackListener == null) return; // Ensure the current item is up to date with the play queue - if (playQueue.getItem() == currentItem && playQueue.getItem() == syncedItem) { - playbackListener.sync(syncedItem, info); + if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { + playbackListener.sync(syncedItem,info); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // MediaSource Loading + //////////////////////////////////////////////////////////////////////////*/ + + private Disposable getDebouncedLoader() { + return debouncedLoadSignal + .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(timestamp -> loadImmediate()); + } + + private void populateSources() { + if (sources == null || sources.getSize() >= playQueue.size()) return; + + for (int index = sources.getSize() - 1; index < playQueue.size(); index++) { + emplace(index, new PlaceholderMediaSource()); } } @@ -254,11 +309,14 @@ public class MediaSourceManagerAlt { final int leftBound = Math.max(0, currentIndex - windowSize); final int rightLimit = currentIndex + windowSize + 1; final int rightBound = Math.min(playQueue.size(), rightLimit); - final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); + final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, + rightBound)); // Do a round robin final int excess = rightLimit - playQueue.size(); - if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); + if (excess >= 0) { + items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); + } for (final PlayQueueItem item: items) loadItem(item); } @@ -269,43 +327,28 @@ public class MediaSourceManagerAlt { final int index = playQueue.indexOf(item); if (index > sources.getSize() - 1) return; - if (((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) { - final Disposable loader = getMediaSource(item) + final Consumer onDone = mediaSource -> { + update(playQueue.indexOf(item), mediaSource); + loadingItems.remove(item); + tryUnblock(); + sync(); + }; + + if (!loadingItems.contains(item) && + ((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) { + + loadingItems.add(item); + final Disposable loader = getLoadedMediaSource(item) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(mediaSource -> update(playQueue.indexOf(item), mediaSource)); + .subscribe(onDone); loaderReactor.add(loader); } tryUnblock(); - if (!isBlocked) { - final MediaSource mediaSource = sources.getMediaSource(playQueue.indexOf(item)); - final StreamInfo info = mediaSource instanceof LoadedMediaSource ? - ((LoadedMediaSource) mediaSource).getStreamInfo() : null; - sync(item, info); - } + sync(); } - private void resetSources() { - if (this.sources != null) this.sources.releaseSource(); - this.sources = new DynamicConcatenatingMediaSource(); - } - - private void populateSources() { - if (sources == null) return; - - for (final PlayQueueItem item : playQueue.getStreams()) { - insert(playQueue.indexOf(item), new PlaceholderMediaSource()); - } - } - - private Disposable getDebouncedLoader() { - return debouncedLoadSignal - .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(timestamp -> loadImmediate()); - } - - private Single getMediaSource(@NonNull final PlayQueueItem stream) { + private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { return stream.getStream().map(streamInfo -> { if (playbackListener == null) { return new FailedMediaSource(stream, new IllegalStateException( @@ -318,47 +361,44 @@ public class MediaSourceManagerAlt { "MediaSource resolution is null")); } - return new LoadedMediaSource(stream, streamInfo, source, - TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS)); + final long expiration = System.currentTimeMillis() + + TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS); + return new LoadedMediaSource(source, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); } + /*////////////////////////////////////////////////////////////////////////// // Media Source List Manipulation //////////////////////////////////////////////////////////////////////////*/ - private void update(final int queueIndex, final MediaSource source) { + /** + * Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource} + * with position * in respect to the play queue only if no {@link MediaSource} + * already exists at the given index. + * */ + private void emplace(final int index, final MediaSource source) { if (sources == null) return; - if (queueIndex < 0 || queueIndex < sources.getSize()) return; + if (index < 0 || index < sources.getSize()) return; - sources.addMediaSource(queueIndex + 1, source); - sources.removeMediaSource(queueIndex); + sources.addMediaSource(index, source); } /** - * Inserts a source into {@link DynamicConcatenatingMediaSource} with position - * in respect to the play queue. - * - * If the play queue index already exists, then the insert is ignored. + * Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource} + * at the given index. If this index is out of bound, then the removal is ignored. * */ - private void insert(final int queueIndex, final PlaceholderMediaSource source) { + private void remove(final int index) { if (sources == null) return; - if (queueIndex < 0 || queueIndex < sources.getSize()) return; + if (index < 0 || index > sources.getSize()) return; - sources.addMediaSource(queueIndex, source); + sources.removeMediaSource(index); } /** - * Removes a source from {@link DynamicConcatenatingMediaSource} with the given play queue index. - * - * If the play queue index does not exist, the removal is ignored. + * Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource} + * from the given source index to the target index. If either index is out of bound, + * then the call is ignored. * */ - private void remove(final int queueIndex) { - if (sources == null) return; - if (queueIndex < 0 || queueIndex > sources.getSize()) return; - - sources.removeMediaSource(queueIndex); - } - private void move(final int source, final int target) { if (sources == null) return; if (source < 0 || target < 0) return; @@ -366,4 +406,17 @@ public class MediaSourceManagerAlt { sources.moveMediaSource(source, target); } + + /** + * Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource} + * at the given index with a given {@link MediaSource}. If the index is out of bound, + * then the replacement is ignored. + * */ + private void update(final int index, final MediaSource source) { + if (sources == null) return; + if (index < 0 || index >= sources.getSize()) return; + + sources.addMediaSource(index + 1, source); + sources.removeMediaSource(index); + } } From 563a4137bd9d21a1f2ee0efaefcd5dc0b33256bf Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 25 Feb 2018 15:10:11 -0800 Subject: [PATCH 03/19] -Fixed inconsistent audio focus state when audio becomes noisy (e.g. headset unplugged). -Fixed live media sources failing when using cached data source by introducing cacheless data sources. -Added custom track selector to circumvent ExoPlayer's language normalization NPE. -Updated Extractor to correctly load live streams. -Removed deprecated deferred media source and media source manager. -Removed Livestream exceptions. --- app/build.gradle | 2 +- .../fragments/detail/VideoDetailFragment.java | 22 +- .../newpipe/player/BackgroundPlayer.java | 3 + .../org/schabi/newpipe/player/BasePlayer.java | 53 ++- .../schabi/newpipe/player/VideoPlayer.java | 66 +-- .../newpipe/player/helper/CacheFactory.java | 22 +- .../player/playback/CustomTrackSelector.java | 114 +++++ .../player/playback/DeferredMediaSource.java | 216 --------- .../player/playback/MediaSourceManager.java | 207 ++++++--- .../playback/MediaSourceManagerAlt.java | 422 ------------------ .../schabi/newpipe/util/ExtractorHelper.java | 2 - 11 files changed, 356 insertions(+), 773 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java diff --git a/app/build.gradle b/app/build.gradle index c9bd8d003..ba6406d4b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:86db415b181' + implementation 'com.github.karyogamy:NewPipeExtractor:837dbd6b86' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 94a2f8ec0..b306721ba 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -56,6 +56,7 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; @@ -1192,11 +1193,20 @@ public class VideoDetailFragment 0); } - if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) { - detailControlsBackground.setVisibility(View.GONE); - detailControlsPopup.setVisibility(View.GONE); - spinnerToolbar.setVisibility(View.GONE); - thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp); + switch (info.getStreamType()) { + case LIVE_STREAM: + case AUDIO_LIVE_STREAM: + detailControlsDownload.setVisibility(View.GONE); + spinnerToolbar.setVisibility(View.GONE); + break; + default: + if (!info.video_streams.isEmpty() || !info.video_only_streams.isEmpty()) break; + + detailControlsBackground.setVisibility(View.GONE); + detailControlsPopup.setVisibility(View.GONE); + spinnerToolbar.setVisibility(View.GONE); + thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp); + break; } if (autoPlayEnabled) { @@ -1216,8 +1226,6 @@ public class VideoDetailFragment if (exception instanceof YoutubeStreamExtractor.GemaException) { onBlockedByGemaError(); - } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { - showError(getString(R.string.live_streams_not_supported), false); } else if (exception instanceof ContentNotAvailableException) { showError(getString(R.string.content_not_available), false); } else { 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 fd47a7167..2fc3252a7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -391,6 +391,9 @@ public final class BackgroundPlayer extends Service { @Override @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { + final MediaSource liveSource = super.sourceOf(item, info); + if (liveSource != null) return liveSource; + final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams); if (index < 0 || index >= info.audio_streams.size()) return null; 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 9ee83427d..de86ea587 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -43,7 +43,6 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; @@ -54,21 +53,23 @@ import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; -import org.schabi.newpipe.player.playback.MediaSourceManagerAlt; +import org.schabi.newpipe.player.playback.CustomTrackSelector; +import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueAdapter; @@ -125,7 +126,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f}; - protected MediaSourceManagerAlt playbackManager; + protected MediaSourceManager playbackManager; protected PlayQueue playQueue; protected StreamInfo currentInfo; @@ -147,9 +148,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected boolean isPrepared = false; - protected DefaultTrackSelector trackSelector; + protected CustomTrackSelector trackSelector; protected DataSource.Factory cacheDataSourceFactory; - protected DefaultExtractorsFactory extractorsFactory; + protected DataSource.Factory cachelessDataSourceFactory; protected SsMediaSource.Factory ssMediaSourceFactory; protected HlsMediaSource.Factory hlsMediaSourceFactory; @@ -190,23 +191,25 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); databaseUpdateReactor = new CompositeDisposable(); + final String userAgent = Downloader.USER_AGENT; final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); - final LoadControl loadControl = new LoadController(context); - final RenderersFactory renderFactory = new DefaultRenderersFactory(context); + final AdaptiveTrackSelection.Factory trackSelectionFactory = + new AdaptiveTrackSelection.Factory(bandwidthMeter); - trackSelector = new DefaultTrackSelector(trackSelectionFactory); - extractorsFactory = new DefaultExtractorsFactory(); - cacheDataSourceFactory = new CacheFactory(context); + trackSelector = new CustomTrackSelector(trackSelectionFactory); + cacheDataSourceFactory = new CacheFactory(context, userAgent, bandwidthMeter); + cachelessDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter); ssMediaSourceFactory = new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory); - hlsMediaSourceFactory = new HlsMediaSource.Factory(cacheDataSourceFactory); + new DefaultSsChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory); + hlsMediaSourceFactory = new HlsMediaSource.Factory(cachelessDataSourceFactory); dashMediaSourceFactory = new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory); + new DefaultDashChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory); extractorMediaSourceFactory = new ExtractorMediaSource.Factory(cacheDataSourceFactory); sampleMediaSourceFactory = new SingleSampleMediaSource.Factory(cacheDataSourceFactory); + final LoadControl loadControl = new LoadController(context); + final RenderersFactory renderFactory = new DefaultRenderersFactory(context); simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); audioReactor = new AudioReactor(context, simpleExoPlayer); @@ -262,7 +265,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected void initPlayback(final PlayQueue queue) { playQueue = queue; playQueue.init(); - playbackManager = new MediaSourceManagerAlt(this, playQueue); + playbackManager = new MediaSourceManager(this, playQueue); if (playQueueAdapter != null) playQueueAdapter.dispose(); playQueueAdapter = new PlayQueueAdapter(context, playQueue); @@ -316,6 +319,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen recordManager = null; } + public MediaSource buildMediaSource(String url) { + return buildMediaSource(url, ""); + } + public MediaSource buildMediaSource(String url, String overrideExtension) { if (DEBUG) { Log.d(TAG, "buildMediaSource() called with: url = [" + url + "], overrideExtension = [" + overrideExtension + "]"); @@ -360,7 +367,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (intent == null || intent.getAction() == null) return; switch (intent.getAction()) { case AudioManager.ACTION_AUDIO_BECOMING_NOISY: - if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); + if (isPlaying()) onVideoPlayPause(); break; } } @@ -721,6 +728,18 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } + @Nullable + @Override + public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { + if (!info.getHlsUrl().isEmpty()) { + return buildMediaSource(info.getHlsUrl()); + } else if (!info.getDashMpdUrl().isEmpty()) { + return buildMediaSource(info.getDashMpdUrl()); + } + + return null; + } + @Override public void shutdown() { if (DEBUG) Log.d(TAG, "Shutting down..."); 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 f8844c15e..3e03bc207 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -53,7 +53,6 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; -import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -65,6 +64,7 @@ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.Subtitles; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.playlist.PlayQueueItem; @@ -305,8 +305,7 @@ public abstract class VideoPlayer extends BasePlayer captionItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setParameters(trackSelector.getParameters().buildUpon() - .setPreferredTextLanguage(captionLanguage).build()); + trackSelector.setPreferredTextLanguage(captionLanguage); trackSelector.setRendererDisabled(textRendererIndex, false); } return true; @@ -328,21 +327,32 @@ public abstract class VideoPlayer extends BasePlayer qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); - if (info != null && info.video_streams.size() + info.video_only_streams.size() > 0) { - final List videos = ListHelper.getSortedStreamVideosList(context, - info.video_streams, info.video_only_streams, false); - availableStreams = new ArrayList<>(videos); - if (playbackQuality == null) { - selectedStreamIndex = getDefaultResolutionIndex(videos); - } else { - selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); - } + final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); - buildQualityMenu(); - qualityTextView.setVisibility(View.VISIBLE); - surfaceView.setVisibility(View.VISIBLE); - } else { - surfaceView.setVisibility(View.GONE); + switch (streamType) { + case VIDEO_STREAM: + if (info.video_streams.size() + info.video_only_streams.size() == 0) break; + + final List videos = ListHelper.getSortedStreamVideosList(context, + info.video_streams, info.video_only_streams, false); + availableStreams = new ArrayList<>(videos); + if (playbackQuality == null) { + selectedStreamIndex = getDefaultResolutionIndex(videos); + } else { + selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); + } + + buildQualityMenu(); + qualityTextView.setVisibility(View.VISIBLE); + surfaceView.setVisibility(View.VISIBLE); + break; + + case AUDIO_STREAM: + case AUDIO_LIVE_STREAM: + surfaceView.setVisibility(View.GONE); + break; + default: + break; } buildPlaybackSpeedMenu(); @@ -352,6 +362,9 @@ public abstract class VideoPlayer extends BasePlayer @Override @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { + final MediaSource liveSource = super.sourceOf(item, info); + if (liveSource != null) return liveSource; + List mediaSources = new ArrayList<>(); // Create video stream source @@ -529,26 +542,15 @@ public abstract class VideoPlayer extends BasePlayer } // Normalize mismatching language strings - final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; - // Because ExoPlayer normalizes the preferred language string but not the text track - // language strings, some preferred language string will have the language name in lowercase - String formattedPreferredLanguage = null; - if (preferredLanguage != null) { - for (final String language : availableLanguages) { - if (language.compareToIgnoreCase(preferredLanguage) == 0) { - formattedPreferredLanguage = language; - break; - } - } - } + final String preferredLanguage = trackSelector.getPreferredTextLanguage(); // Build UI buildCaptionMenu(availableLanguages); - if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null || - !availableLanguages.contains(formattedPreferredLanguage)) { + if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null || + !availableLanguages.contains(preferredLanguage)) { captionTextView.setText(R.string.caption_none); } else { - captionTextView.setText(formattedPreferredLanguage); + captionTextView.setText(preferredLanguage); } captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index dce74ffb5..900f13ae9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -4,11 +4,14 @@ import android.content.Context; import android.support.annotation.NonNull; import android.util.Log; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.FileDataSource; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.cache.CacheDataSink; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; @@ -33,18 +36,21 @@ public class CacheFactory implements DataSource.Factory { // todo: make this a singleton? private static SimpleCache cache; - public CacheFactory(@NonNull final Context context) { - this(context, PlayerHelper.getPreferredCacheSize(context), PlayerHelper.getPreferredFileSize(context)); + public CacheFactory(@NonNull final Context context, + @NonNull final String userAgent, + @NonNull final TransferListener transferListener) { + this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context), + PlayerHelper.getPreferredFileSize(context)); } - CacheFactory(@NonNull final Context context, final long maxCacheSize, final long maxFileSize) { - super(); + private CacheFactory(@NonNull final Context context, + @NonNull final String userAgent, + @NonNull final TransferListener transferListener, + final long maxCacheSize, + final long maxFileSize) { this.maxFileSize = maxFileSize; - final String userAgent = Downloader.USER_AGENT; - final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, bandwidthMeter); - + dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); if (!cacheDir.exists()) { //noinspection ResultOfMethodCallIgnored diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java new file mode 100644 index 000000000..d80ea5bae --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java @@ -0,0 +1,114 @@ +package org.schabi.newpipe.player.playback; + +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * This class allows irregular text language labels for use when selecting text captions and + * is mostly a copy-paste from {@link DefaultTrackSelector}. + * + * This is a hack and should be removed once ExoPlayer fixes language normalization to accept + * a broader set of languages. + * */ +public class CustomTrackSelector extends DefaultTrackSelector { + private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; + + private String preferredTextLanguage; + + public CustomTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { + super(adaptiveTrackSelectionFactory); + } + + public String getPreferredTextLanguage() { + return preferredTextLanguage; + } + + public void setPreferredTextLanguage(@NonNull final String label) { + Assertions.checkNotNull(label); + if (!label.equals(preferredTextLanguage)) { + preferredTextLanguage = label; + invalidate(); + } + } + + /** @see DefaultTrackSelector#formatHasLanguage(Format, String)*/ + protected static boolean formatHasLanguage(Format format, String language) { + return language != null && TextUtils.equals(language, format.language); + } + + /** @see DefaultTrackSelector#formatHasNoLanguage(Format)*/ + protected static boolean formatHasNoLanguage(Format format) { + return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED); + } + + /** @see DefaultTrackSelector#selectTextTrack(TrackGroupArray, int[][], Parameters) */ + @Override + protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, + Parameters params) throws ExoPlaybackException { + TrackGroup selectedGroup = null; + int selectedTrackIndex = 0; + int selectedTrackScore = 0; + for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { + TrackGroup trackGroup = groups.get(groupIndex); + int[] trackFormatSupport = formatSupport[groupIndex]; + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { + Format format = trackGroup.getFormat(trackIndex); + int maskedSelectionFlags = + format.selectionFlags & ~params.disabledTextTrackSelectionFlags; + boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + int trackScore; + boolean preferredLanguageFound = formatHasLanguage(format, preferredTextLanguage); + if (preferredLanguageFound + || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { + if (isDefault) { + trackScore = 8; + } else if (!isForced) { + // Prefer non-forced to forced if a preferred text language has been specified. Where + // both are provided the non-forced track will usually contain the forced subtitles as + // a subset. + trackScore = 6; + } else { + trackScore = 4; + } + trackScore += preferredLanguageFound ? 1 : 0; + } else if (isDefault) { + trackScore = 3; + } else if (isForced) { + if (formatHasLanguage(format, params.preferredAudioLanguage)) { + trackScore = 2; + } else { + trackScore = 1; + } + } else { + // Track should not be selected. + continue; + } + if (isSupported(trackFormatSupport[trackIndex], false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; + } + if (trackScore > selectedTrackScore) { + selectedGroup = trackGroup; + selectedTrackIndex = trackIndex; + selectedTrackScore = trackScore; + } + } + } + } + return selectedGroup == null ? null + : new FixedTrackSelection(selectedGroup, selectedTrackIndex); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java deleted file mode 100644 index 3ae744d18..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java +++ /dev/null @@ -1,216 +0,0 @@ -package org.schabi.newpipe.player.playback; - -import android.support.annotation.NonNull; -import android.util.Log; - -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.upstream.Allocator; - -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.playlist.PlayQueueItem; - -import java.io.IOException; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; -import io.reactivex.schedulers.Schedulers; - -/** - * DeferredMediaSource is specifically designed to allow external control over when - * the source metadata are loaded while being compatible with ExoPlayer's playlists. - * - * This media source follows the structure of how NewPipeExtractor's - * {@link org.schabi.newpipe.extractor.stream.StreamInfoItem} is converted into - * {@link org.schabi.newpipe.extractor.stream.StreamInfo}. Once conversion is complete, - * this media source behaves identically as any other native media sources. - * */ -public final class DeferredMediaSource implements MediaSource { - private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode()); - - /** - * This state indicates the {@link DeferredMediaSource} has just been initialized or reset. - * The source must be prepared and loaded again before playback. - * */ - public final static int STATE_INIT = 0; - /** - * This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load. - * */ - public final static int STATE_PREPARED = 1; - /** - * This state indicates the {@link DeferredMediaSource} has been loaded without errors and - * is ready for playback. - * */ - public final static int STATE_LOADED = 2; - - public interface Callback { - /** - * Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution - * from a given StreamInfo. - * */ - MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info); - } - - private PlayQueueItem stream; - private Callback callback; - private int state; - - private MediaSource mediaSource; - - /* Custom internal objects */ - private Disposable loader; - 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; - } - - /** - * Returns the current state of the {@link DeferredMediaSource}. - * - * @see DeferredMediaSource#STATE_INIT - * @see DeferredMediaSource#STATE_PREPARED - * @see DeferredMediaSource#STATE_LOADED - * */ - public int state() { - return state; - } - - /** - * Parameters are kept in the class for delayed preparation. - * */ - @Override - public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) { - this.exoPlayer = exoPlayer; - this.listener = listener; - this.state = STATE_PREPARED; - } - - /** - * Externally controlled loading. This method fully prepares the source to be used - * like any other native {@link com.google.android.exoplayer2.source.MediaSource}. - * - * Ideally, this should be called after this source has entered PREPARED state and - * called once only. - * - * If loading fails here, an error will be propagated out and result in an - * {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException}, - * which is delegated to the player. - * */ - public synchronized void load() { - if (stream == null) { - Log.e(TAG, "Stream Info missing, media source loading terminated."); - return; - } - if (state != STATE_PREPARED || loader != null) return; - - Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); - - loader = stream.getStream() - .map(streamInfo -> onStreamInfoReceived(stream, streamInfo)) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onMediaSourceReceived, this::onStreamInfoError); - } - - private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item, - @NonNull final StreamInfo info) throws Exception { - if (callback == null) { - throw new Exception("No available callback for resolving stream info."); - } - - final MediaSource mediaSource = callback.sourceOf(item, info); - - if (mediaSource == null) { - throw new Exception("Unable to resolve source from stream info. URL: " + stream.getUrl() + - ", audio count: " + info.audio_streams.size() + - ", video count: " + info.video_only_streams.size() + info.video_streams.size()); - } - - return mediaSource; - } - - private void onMediaSourceReceived(final MediaSource mediaSource) throws Exception { - if (exoPlayer == null || listener == null || mediaSource == null) { - throw new Exception("MediaSource loading failed. URL: " + stream.getUrl()); - } - - Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl()); - state = STATE_LOADED; - - this.mediaSource = mediaSource; - this.mediaSource.prepareSource(exoPlayer, false, listener); - } - - private void onStreamInfoError(final Throwable throwable) { - Log.e(TAG, "Loading error:", throwable); - error = throwable; - state = STATE_LOADED; - } - - /** - * Delegate all errors to the player after {@link #load() load} is complete. - * - * Specifically, this method is called after an exception has occurred during loading or - * {@link com.google.android.exoplayer2.source.MediaSource#prepareSource(ExoPlayer, boolean, Listener) prepareSource}. - * */ - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - if (error != null) { - throw new IOException(error); - } - - if (mediaSource != null) { - mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId mediaPeriodId, Allocator allocator) { - return mediaSource.createPeriod(mediaPeriodId, allocator); - } - - /** - * Releases the media period (buffers). - * - * This may be called after {@link #releaseSource releaseSource}. - * */ - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - mediaSource.releasePeriod(mediaPeriod); - } - - /** - * Cleans up all internal custom objects creating during loading. - * - * This method is called when the parent {@link com.google.android.exoplayer2.source.MediaSource} - * is released or when the player is stopped. - * - * This method should not release or set null the resources passed in through the constructor. - * This method should not set null the internal {@link com.google.android.exoplayer2.source.MediaSource}. - * */ - @Override - public void releaseSource() { - if (mediaSource != null) { - mediaSource.releaseSource(); - } - if (loader != null) { - loader.dispose(); - } - - /* Do not set mediaSource as null here as it may be called through releasePeriod */ - loader = null; - exoPlayer = null; - listener = null; - error = null; - - state = STATE_INIT; - } -} 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 9dea4fdce..8d822fa54 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,13 +1,17 @@ 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; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.mediasource.FailedMediaSource; +import org.schabi.newpipe.player.mediasource.LoadedMediaSource; +import org.schabi.newpipe.player.mediasource.ManagedMediaSource; +import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.events.MoveEvent; @@ -15,18 +19,22 @@ import org.schabi.newpipe.playlist.events.PlayQueueEvent; import org.schabi.newpipe.playlist.events.RemoveEvent; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; import io.reactivex.functions.Consumer; import io.reactivex.subjects.PublishSubject; public class MediaSourceManager { - private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode()); // One-side rolling window size for default loading // Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0 private final int windowSize; @@ -40,17 +48,17 @@ public class MediaSourceManager { private final PublishSubject debouncedLoadSignal; private final Disposable debouncedLoader; - private final DeferredMediaSource.Callback sourceBuilder; - private DynamicConcatenatingMediaSource sources; private Subscription playQueueReactor; - private SerialDisposable syncReactor; - - private PlayQueueItem syncedItem; + private CompositeDisposable loaderReactor; private boolean isBlocked; + private SerialDisposable syncReactor; + private PlayQueueItem syncedItem; + private Set loadingItems; + public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { this(listener, playQueue, 1, 400L); @@ -61,7 +69,8 @@ public class MediaSourceManager { final int windowSize, final long loadDebounceMillis) { if (windowSize <= 0) { - throw new UnsupportedOperationException("MediaSourceManager window size must be greater than 0"); + throw new UnsupportedOperationException( + "MediaSourceManager window size must be greater than 0"); } this.playbackListener = listener; @@ -69,27 +78,20 @@ public class MediaSourceManager { this.windowSize = windowSize; this.loadDebounceMillis = loadDebounceMillis; - this.syncReactor = new SerialDisposable(); + this.loaderReactor = new CompositeDisposable(); this.debouncedLoadSignal = PublishSubject.create(); this.debouncedLoader = getDebouncedLoader(); - this.sourceBuilder = getSourceBuilder(); - this.sources = new DynamicConcatenatingMediaSource(); + this.syncReactor = new SerialDisposable(); + this.loadingItems = Collections.synchronizedSet(new HashSet<>()); + playQueue.getBroadcastReceiver() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getReactor()); } - /*////////////////////////////////////////////////////////////////////////// - // DeferredMediaSource listener - //////////////////////////////////////////////////////////////////////////*/ - - private DeferredMediaSource.Callback getSourceBuilder() { - return playbackListener::sourceOf; - } - /*////////////////////////////////////////////////////////////////////////// // Exposed Methods //////////////////////////////////////////////////////////////////////////*/ @@ -100,10 +102,12 @@ public class MediaSourceManager { if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete(); if (debouncedLoader != null) debouncedLoader.dispose(); if (playQueueReactor != null) playQueueReactor.cancel(); + if (loaderReactor != null) loaderReactor.dispose(); if (syncReactor != null) syncReactor.dispose(); if (sources != null) sources.releaseSource(); playQueueReactor = null; + loaderReactor = null; syncReactor = null; syncedItem = null; sources = null; @@ -121,7 +125,8 @@ public class MediaSourceManager { /** * Blocks the player and repopulate the sources. * - * Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}. + * Does not ensure the player is unblocked and should be done explicitly + * through {@link #load() load}. * */ public void reset() { tryBlock(); @@ -210,41 +215,45 @@ public class MediaSourceManager { } /*////////////////////////////////////////////////////////////////////////// - // Internal Helpers + // Playback Locking //////////////////////////////////////////////////////////////////////////*/ private boolean isPlayQueueReady() { - return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > windowSize; + final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > windowSize; + return playQueue.isComplete() || isWindowLoaded; } - private boolean tryBlock() { - if (!isBlocked) { - playbackListener.block(); - resetSources(); - isBlocked = true; - return true; - } - return false; + private boolean isPlaybackReady() { + return sources.getSize() > 0 && + sources.getMediaSource(playQueue.getIndex()) instanceof LoadedMediaSource; } - private boolean tryUnblock() { - if (isPlayQueueReady() && isBlocked && sources != null) { + private void tryBlock() { + if (isBlocked) return; + + playbackListener.block(); + resetSources(); + + isBlocked = true; + } + + private void tryUnblock() { + if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) { isBlocked = false; playbackListener.unblock(sources); - return true; } - return false; } + /*////////////////////////////////////////////////////////////////////////// + // Metadata Synchronization TODO: maybe this should be a separate manager + //////////////////////////////////////////////////////////////////////////*/ + private void sync() { final PlayQueueItem currentItem = playQueue.getItem(); - if (currentItem == null) return; + if (isBlocked || currentItem == null) return; final Consumer onSuccess = info -> syncInternal(currentItem, info); - final Consumer onError = throwable -> { - Log.e(TAG, "Sync error:", throwable); - syncInternal(currentItem, null); - }; + final Consumer onError = throwable -> syncInternal(currentItem, null); if (syncedItem != currentItem) { syncedItem = currentItem; @@ -264,6 +273,17 @@ public class MediaSourceManager { } } + /*////////////////////////////////////////////////////////////////////////// + // MediaSource Loading + //////////////////////////////////////////////////////////////////////////*/ + + private Disposable getDebouncedLoader() { + return debouncedLoadSignal + .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(timestamp -> loadImmediate()); + } + private void loadDebounced() { debouncedLoadSignal.onNext(System.currentTimeMillis()); } @@ -279,76 +299,113 @@ public class MediaSourceManager { final int leftBound = Math.max(0, currentIndex - windowSize); final int rightLimit = currentIndex + windowSize + 1; final int rightBound = Math.min(playQueue.size(), rightLimit); - final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); + final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, + rightBound)); // Do a round robin final int excess = rightLimit - playQueue.size(); - if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); + if (excess >= 0) { + items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); + } for (final PlayQueueItem item: items) loadItem(item); } private void loadItem(@Nullable final PlayQueueItem item) { - if (item == null) return; + if (sources == null || 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(); + final Consumer onDone = mediaSource -> { + update(playQueue.indexOf(item), mediaSource); + loadingItems.remove(item); + tryUnblock(); + sync(); + }; + + if (!loadingItems.contains(item) && + ((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) { + + loadingItems.add(item); + final Disposable loader = getLoadedMediaSource(item) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(onDone); + loaderReactor.add(loader); + } tryUnblock(); - if (!isBlocked) sync(); + sync(); } + private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { + return stream.getStream().map(streamInfo -> { + if (playbackListener == null) { + return new FailedMediaSource(stream, new IllegalStateException( + "MediaSourceManager playback listener unavailable")); + } + + final MediaSource source = playbackListener.sourceOf(stream, streamInfo); + if (source == null) { + return new FailedMediaSource(stream, new IllegalStateException( + "MediaSource resolution is null")); + } + + final long expiration = System.currentTimeMillis() + + TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS); + return new LoadedMediaSource(source, expiration); + }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); + } + + /*////////////////////////////////////////////////////////////////////////// + // MediaSource Playlist Helpers + //////////////////////////////////////////////////////////////////////////*/ + private void resetSources() { if (this.sources != null) this.sources.releaseSource(); this.sources = new DynamicConcatenatingMediaSource(); } private void populateSources() { - if (sources == null) return; + if (sources == null || sources.getSize() >= playQueue.size()) return; - for (final PlayQueueItem item : playQueue.getStreams()) { - insert(playQueue.indexOf(item), new DeferredMediaSource(item, sourceBuilder)); + for (int index = sources.getSize() - 1; index < playQueue.size(); index++) { + emplace(index, new PlaceholderMediaSource()); } } - private Disposable getDebouncedLoader() { - return debouncedLoadSignal - .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(timestamp -> loadImmediate()); - } /*////////////////////////////////////////////////////////////////////////// - // Media Source List Manipulation + // MediaSource Playlist Manipulation //////////////////////////////////////////////////////////////////////////*/ /** - * Inserts a source into {@link DynamicConcatenatingMediaSource} with position - * in respect to the play queue. - * - * If the play queue index already exists, then the insert is ignored. + * Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource} + * with position * in respect to the play queue only if no {@link MediaSource} + * already exists at the given index. * */ - private void insert(final int queueIndex, final DeferredMediaSource source) { + private void emplace(final int index, final MediaSource source) { if (sources == null) return; - if (queueIndex < 0 || queueIndex < sources.getSize()) return; + if (index < 0 || index < sources.getSize()) return; - sources.addMediaSource(queueIndex, source); + sources.addMediaSource(index, source); } /** - * Removes a source from {@link DynamicConcatenatingMediaSource} with the given play queue index. - * - * If the play queue index does not exist, the removal is ignored. + * Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource} + * at the given index. If this index is out of bound, then the removal is ignored. * */ - private void remove(final int queueIndex) { + private void remove(final int index) { if (sources == null) return; - if (queueIndex < 0 || queueIndex > sources.getSize()) return; + if (index < 0 || index > sources.getSize()) return; - sources.removeMediaSource(queueIndex); + sources.removeMediaSource(index); } + /** + * Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource} + * from the given source index to the target index. If either index is out of bound, + * then the call is ignored. + * */ private void move(final int source, final int target) { if (sources == null) return; if (source < 0 || target < 0) return; @@ -356,4 +413,18 @@ public class MediaSourceManager { sources.moveMediaSource(source, target); } + + /** + * Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource} + * at the given index with a given {@link MediaSource}. If the index is out of bound, + * then the replacement is ignored. + * */ + private void update(final int index, final MediaSource source) { + if (sources == null) return; + if (index < 0 || index >= sources.getSize()) return; + + sources.addMediaSource(index + 1, source, () -> { + if (sources != null) sources.removeMediaSource(index); + }); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java deleted file mode 100644 index 03b583e07..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java +++ /dev/null @@ -1,422 +0,0 @@ -package org.schabi.newpipe.player.playback; - -import android.support.annotation.Nullable; - -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; -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.FailedMediaSource; -import org.schabi.newpipe.player.mediasource.LoadedMediaSource; -import org.schabi.newpipe.player.mediasource.ManagedMediaSource; -import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource; -import org.schabi.newpipe.playlist.PlayQueue; -import org.schabi.newpipe.playlist.PlayQueueItem; -import org.schabi.newpipe.playlist.events.MoveEvent; -import org.schabi.newpipe.playlist.events.PlayQueueEvent; -import org.schabi.newpipe.playlist.events.RemoveEvent; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; -import io.reactivex.disposables.SerialDisposable; -import io.reactivex.functions.Consumer; -import io.reactivex.subjects.PublishSubject; - -public class MediaSourceManagerAlt { - // One-side rolling window size for default loading - // Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0 - private final int windowSize; - private final PlaybackListener playbackListener; - private final PlayQueue playQueue; - - // Process only the last load order when receiving a stream of load orders (lessens I/O) - // The higher it is, the less loading occurs during rapid noncritical timeline changes - // Not recommended to go below 100ms - private final long loadDebounceMillis; - private final PublishSubject debouncedLoadSignal; - private final Disposable debouncedLoader; - - private DynamicConcatenatingMediaSource sources; - - private Subscription playQueueReactor; - private CompositeDisposable loaderReactor; - - private boolean isBlocked; - - private SerialDisposable syncReactor; - private PlayQueueItem syncedItem; - private Set loadingItems; - - public MediaSourceManagerAlt(@NonNull final PlaybackListener listener, - @NonNull final PlayQueue playQueue) { - this(listener, playQueue, 0, 400L); - } - - private MediaSourceManagerAlt(@NonNull final PlaybackListener listener, - @NonNull final PlayQueue playQueue, - final int windowSize, - final long loadDebounceMillis) { - if (windowSize < 0) { - throw new UnsupportedOperationException( - "MediaSourceManager window size must be greater than 0"); - } - - this.playbackListener = listener; - this.playQueue = playQueue; - this.windowSize = windowSize; - this.loadDebounceMillis = loadDebounceMillis; - - this.loaderReactor = new CompositeDisposable(); - this.debouncedLoadSignal = PublishSubject.create(); - this.debouncedLoader = getDebouncedLoader(); - - this.sources = new DynamicConcatenatingMediaSource(); - - this.syncReactor = new SerialDisposable(); - this.loadingItems = Collections.synchronizedSet(new HashSet<>()); - - playQueue.getBroadcastReceiver() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getReactor()); - } - - /*////////////////////////////////////////////////////////////////////////// - // Exposed Methods - //////////////////////////////////////////////////////////////////////////*/ - /** - * Dispose the manager and releases all message buses and loaders. - * */ - public void dispose() { - if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete(); - if (debouncedLoader != null) debouncedLoader.dispose(); - if (playQueueReactor != null) playQueueReactor.cancel(); - if (loaderReactor != null) loaderReactor.dispose(); - if (syncReactor != null) syncReactor.dispose(); - if (sources != null) sources.releaseSource(); - - playQueueReactor = null; - loaderReactor = null; - syncReactor = null; - syncedItem = null; - sources = null; - } - - /** - * Loads the current playing stream and the streams within its windowSize bound. - * - * Unblocks the player once the item at the current index is loaded. - * */ - public void load() { - loadDebounced(); - } - - /** - * Blocks the player and repopulate the sources. - * - * Does not ensure the player is unblocked and should be done explicitly - * through {@link #load() load}. - * */ - public void reset() { - tryBlock(); - - syncedItem = null; - populateSources(); - } - /*////////////////////////////////////////////////////////////////////////// - // Event Reactor - //////////////////////////////////////////////////////////////////////////*/ - - private Subscriber getReactor() { - return new Subscriber() { - @Override - public void onSubscribe(@NonNull Subscription d) { - if (playQueueReactor != null) playQueueReactor.cancel(); - playQueueReactor = d; - playQueueReactor.request(1); - } - - @Override - public void onNext(@NonNull PlayQueueEvent playQueueMessage) { - if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage); - } - - @Override - public void onError(@NonNull Throwable e) {} - - @Override - public void onComplete() {} - }; - } - - private void onPlayQueueChanged(final PlayQueueEvent event) { - if (playQueue.isEmpty() && playQueue.isComplete()) { - playbackListener.shutdown(); - return; - } - - // Event specific action - switch (event.type()) { - case INIT: - case REORDER: - case ERROR: - reset(); - break; - case APPEND: - populateSources(); - break; - case REMOVE: - final RemoveEvent removeEvent = (RemoveEvent) event; - remove(removeEvent.getRemoveIndex()); - break; - case MOVE: - final MoveEvent moveEvent = (MoveEvent) event; - move(moveEvent.getFromIndex(), moveEvent.getToIndex()); - break; - case SELECT: - case RECOVERY: - default: - break; - } - - // Loading and Syncing - switch (event.type()) { - case INIT: - case REORDER: - case ERROR: - loadImmediate(); // low frequency, critical events - break; - case APPEND: - case REMOVE: - case SELECT: - case MOVE: - case RECOVERY: - default: - loadDebounced(); // high frequency or noncritical events - break; - } - - if (!isPlayQueueReady()) { - tryBlock(); - playQueue.fetch(); - } - if (playQueueReactor != null) playQueueReactor.request(1); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Locking - //////////////////////////////////////////////////////////////////////////*/ - - private boolean isPlayQueueReady() { - final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > windowSize; - return playQueue.isComplete() || isWindowLoaded; - } - - private boolean isPlaybackReady() { - return sources.getSize() > 0 && - sources.getMediaSource(playQueue.getIndex()) instanceof LoadedMediaSource; - } - - private void tryBlock() { - if (isBlocked) return; - - playbackListener.block(); - - if (this.sources != null) this.sources.releaseSource(); - this.sources = new DynamicConcatenatingMediaSource(); - - isBlocked = true; - } - - private void tryUnblock() { - if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) { - isBlocked = false; - playbackListener.unblock(sources); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Metadata Synchronization TODO: maybe this should be a separate manager - //////////////////////////////////////////////////////////////////////////*/ - - private void sync() { - final PlayQueueItem currentItem = playQueue.getItem(); - if (isBlocked || currentItem == null) return; - - final Consumer onSuccess = info -> syncInternal(currentItem, info); - final Consumer onError = throwable -> syncInternal(currentItem, null); - - if (syncedItem != currentItem) { - syncedItem = currentItem; - final Disposable sync = currentItem.getStream() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onSuccess, onError); - syncReactor.set(sync); - } - } - - private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item, - @Nullable final StreamInfo info) { - if (playQueue == null || playbackListener == null) return; - // Ensure the current item is up to date with the play queue - if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { - playbackListener.sync(syncedItem,info); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // MediaSource Loading - //////////////////////////////////////////////////////////////////////////*/ - - private Disposable getDebouncedLoader() { - return debouncedLoadSignal - .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(timestamp -> loadImmediate()); - } - - private void populateSources() { - if (sources == null || sources.getSize() >= playQueue.size()) return; - - for (int index = sources.getSize() - 1; index < playQueue.size(); index++) { - emplace(index, new PlaceholderMediaSource()); - } - } - - private void loadDebounced() { - debouncedLoadSignal.onNext(System.currentTimeMillis()); - } - - private void loadImmediate() { - // The current item has higher priority - final int currentIndex = playQueue.getIndex(); - final PlayQueueItem currentItem = playQueue.getItem(currentIndex); - if (currentItem == null) return; - loadItem(currentItem); - - // The rest are just for seamless playback - final int leftBound = Math.max(0, currentIndex - windowSize); - final int rightLimit = currentIndex + windowSize + 1; - final int rightBound = Math.min(playQueue.size(), rightLimit); - final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, - rightBound)); - - // Do a round robin - 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) loadItem(item); - } - - private void loadItem(@Nullable final PlayQueueItem item) { - if (sources == null || item == null) return; - - final int index = playQueue.indexOf(item); - if (index > sources.getSize() - 1) return; - - final Consumer onDone = mediaSource -> { - update(playQueue.indexOf(item), mediaSource); - loadingItems.remove(item); - tryUnblock(); - sync(); - }; - - if (!loadingItems.contains(item) && - ((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) { - - loadingItems.add(item); - final Disposable loader = getLoadedMediaSource(item) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onDone); - loaderReactor.add(loader); - } - - tryUnblock(); - sync(); - } - - private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { - return stream.getStream().map(streamInfo -> { - if (playbackListener == null) { - return new FailedMediaSource(stream, new IllegalStateException( - "MediaSourceManager playback listener unavailable")); - } - - final MediaSource source = playbackListener.sourceOf(stream, streamInfo); - if (source == null) { - return new FailedMediaSource(stream, new IllegalStateException( - "MediaSource resolution is null")); - } - - final long expiration = System.currentTimeMillis() + - TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS); - return new LoadedMediaSource(source, expiration); - }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); - } - - /*////////////////////////////////////////////////////////////////////////// - // Media Source List Manipulation - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource} - * with position * in respect to the play queue only if no {@link MediaSource} - * already exists at the given index. - * */ - private void emplace(final int index, final MediaSource source) { - if (sources == null) return; - if (index < 0 || index < sources.getSize()) return; - - sources.addMediaSource(index, source); - } - - /** - * Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource} - * at the given index. If this index is out of bound, then the removal is ignored. - * */ - private void remove(final int index) { - if (sources == null) return; - if (index < 0 || index > sources.getSize()) return; - - sources.removeMediaSource(index); - } - - /** - * Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource} - * from the given source index to the target index. If either index is out of bound, - * then the call is ignored. - * */ - private void move(final int source, final int target) { - if (sources == null) return; - if (source < 0 || target < 0) return; - if (source >= sources.getSize() || target >= sources.getSize()) return; - - sources.moveMediaSource(source, target); - } - - /** - * Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource} - * at the given index with a given {@link MediaSource}. If the index is out of bound, - * then the replacement is ignored. - * */ - private void update(final int index, final MediaSource source) { - if (sources == null) return; - if (index < 0 || index >= sources.getSize()) return; - - sources.addMediaSource(index + 1, source); - sources.removeMediaSource(index); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 1148171d7..78d6a6318 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -224,8 +224,6 @@ public final class ExtractorHelper { Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); } else if (exception instanceof YoutubeStreamExtractor.GemaException) { Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); - } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { - Toast.makeText(context, R.string.live_streams_not_supported, Toast.LENGTH_LONG).show(); } else if (exception instanceof ContentNotAvailableException) { Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); } else { From ac431e3ece282bc941ce5330d7e83bd8963dd50c Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 25 Feb 2018 15:32:25 -0800 Subject: [PATCH 04/19] -Fixed failed media source not treated as ready. --- .../newpipe/player/mediasource/FailedMediaSource.java | 2 -- .../schabi/newpipe/player/playback/MediaSourceManager.java | 7 +++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index c4a44f503..2454c022c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -4,10 +4,8 @@ import android.support.annotation.NonNull; 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.upstream.Allocator; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.playlist.PlayQueueItem; import java.io.IOException; 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 8d822fa54..50b485cb5 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 @@ -219,13 +219,16 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private boolean isPlayQueueReady() { + if (playQueue == null) return false; + final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > windowSize; return playQueue.isComplete() || isWindowLoaded; } + // Checks if the current playback media source is a placeholder, if so, then it is not ready private boolean isPlaybackReady() { - return sources.getSize() > 0 && - sources.getMediaSource(playQueue.getIndex()) instanceof LoadedMediaSource; + return sources != null && playQueue != null && sources.getSize() > playQueue.getIndex() && + !(sources.getMediaSource(playQueue.getIndex()) instanceof PlaceholderMediaSource); } private void tryBlock() { From 1444fe5468e3e6a691b916cb0fa80aecd7c628cd Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 25 Feb 2018 20:12:20 -0800 Subject: [PATCH 05/19] -Fixed potential NPE when obtaining broadcast receiver. -Extracted expiration time in media source manager. -Re-enabled long click on live stream info items. -Fixed dash source building to use mpd instead of extractor. --- .../holder/StreamMiniInfoItemHolder.java | 26 +++++------- .../org/schabi/newpipe/player/BasePlayer.java | 40 +++++++++---------- .../player/mediasource/FailedMediaSource.java | 4 ++ .../player/playback/MediaSourceManager.java | 24 +++++++---- .../schabi/newpipe/playlist/PlayQueue.java | 3 +- .../newpipe/playlist/PlayQueueAdapter.java | 4 +- 6 files changed, 54 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 48dc470d0..594a85582 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -59,23 +59,20 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { itemBuilder.getImageLoader() .displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); - itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (itemBuilder.getOnStreamSelectedListener() != null) { - itemBuilder.getOnStreamSelectedListener().selected(item); - } + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnStreamSelectedListener() != null) { + itemBuilder.getOnStreamSelectedListener().selected(item); } }); switch (item.stream_type) { case AUDIO_STREAM: case VIDEO_STREAM: - case FILE: - enableLongClick(item); - break; case LIVE_STREAM: case AUDIO_LIVE_STREAM: + enableLongClick(item); + break; + case FILE: case NONE: default: disableLongClick(); @@ -85,14 +82,11 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { private void enableLongClick(final StreamInfoItem item) { itemView.setLongClickable(true); - itemView.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - if (itemBuilder.getOnStreamSelectedListener() != null) { - itemBuilder.getOnStreamSelectedListener().held(item); - } - return true; + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnStreamSelectedListener() != null) { + itemBuilder.getOnStreamSelectedListener().held(item); } + return true; }); } 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 de86ea587..3e136dbde 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -319,35 +319,27 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen recordManager = null; } - public MediaSource buildMediaSource(String url) { - return buildMediaSource(url, ""); - } - public MediaSource buildMediaSource(String url, String overrideExtension) { if (DEBUG) { - Log.d(TAG, "buildMediaSource() called with: url = [" + url + "], overrideExtension = [" + overrideExtension + "]"); + Log.d(TAG, "buildMediaSource() called with: url = [" + url + + "], overrideExtension = [" + overrideExtension + "]"); } Uri uri = Uri.parse(url); - int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); - MediaSource mediaSource; + int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : + Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: - mediaSource = ssMediaSourceFactory.createMediaSource(uri); - break; + return ssMediaSourceFactory.createMediaSource(uri); case C.TYPE_DASH: - mediaSource = dashMediaSourceFactory.createMediaSource(uri); - break; + return dashMediaSourceFactory.createMediaSource(uri); case C.TYPE_HLS: - mediaSource = hlsMediaSourceFactory.createMediaSource(uri); - break; + return hlsMediaSourceFactory.createMediaSource(uri); case C.TYPE_OTHER: - mediaSource = extractorMediaSourceFactory.createMediaSource(uri); - break; + return extractorMediaSourceFactory.createMediaSource(uri); default: { throw new IllegalStateException("Unsupported type: " + type); } } - return mediaSource; } /*////////////////////////////////////////////////////////////////////////// @@ -514,8 +506,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); - if (playbackManager != null) { - playbackManager.load(); + switch (reason) { + case Player.TIMELINE_CHANGE_REASON_PREPARED: + case Player.TIMELINE_CHANGE_REASON_RESET: + case Player.TIMELINE_CHANGE_REASON_DYNAMIC: + default: + if (playbackManager != null) playbackManager.load(); + break; } } @@ -526,7 +523,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - if (DEBUG) Log.d(TAG, "playbackParameters(), speed: " + playbackParameters.speed + ", pitch: " + playbackParameters.pitch); + if (DEBUG) Log.d(TAG, "playbackParameters(), speed: " + playbackParameters.speed + + ", pitch: " + playbackParameters.pitch); } @Override @@ -732,9 +730,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen @Override public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { if (!info.getHlsUrl().isEmpty()) { - return buildMediaSource(info.getHlsUrl()); + return buildMediaSource(info.getHlsUrl(), "m3u8"); } else if (!info.getDashMpdUrl().isEmpty()) { - return buildMediaSource(info.getDashMpdUrl()); + return buildMediaSource(info.getDashMpdUrl(), "mpd"); } return null; diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 2454c022c..a73c9975e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -25,6 +25,10 @@ public class FailedMediaSource implements ManagedMediaSource { this.retryTimestamp = retryTimestamp; } + /** + * Permanently fail the play queue item associated with this source, with no hope of retrying. + * The error will always be propagated to ExoPlayer. + * */ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, @NonNull final Throwable error) { this.playQueueItem = playQueueItem; 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 50b485cb5..3b068f730 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 @@ -40,6 +40,8 @@ public class MediaSourceManager { private final int windowSize; private final PlaybackListener playbackListener; private final PlayQueue playQueue; + private final long expirationTimeMillis; + private final TimeUnit expirationTimeUnit; // Process only the last load order when receiving a stream of load orders (lessens I/O) // The higher it is, the less loading occurs during rapid noncritical timeline changes @@ -61,13 +63,15 @@ public class MediaSourceManager { public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { - this(listener, playQueue, 1, 400L); + this(listener, playQueue, 1, 400L, 2, TimeUnit.HOURS); } private MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue, final int windowSize, - final long loadDebounceMillis) { + final long loadDebounceMillis, + final long expirationTimeMillis, + @NonNull final TimeUnit expirationTimeUnit) { if (windowSize <= 0) { throw new UnsupportedOperationException( "MediaSourceManager window size must be greater than 0"); @@ -77,6 +81,8 @@ public class MediaSourceManager { this.playQueue = playQueue; this.windowSize = windowSize; this.loadDebounceMillis = loadDebounceMillis; + this.expirationTimeMillis = expirationTimeMillis; + this.expirationTimeUnit = expirationTimeUnit; this.loaderReactor = new CompositeDisposable(); this.debouncedLoadSignal = PublishSubject.create(); @@ -87,9 +93,11 @@ public class MediaSourceManager { this.syncReactor = new SerialDisposable(); this.loadingItems = Collections.synchronizedSet(new HashSet<>()); - playQueue.getBroadcastReceiver() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getReactor()); + if (playQueue.getBroadcastReceiver() != null) { + playQueue.getBroadcastReceiver() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getReactor()); + } } /*////////////////////////////////////////////////////////////////////////// @@ -225,7 +233,7 @@ public class MediaSourceManager { return playQueue.isComplete() || isWindowLoaded; } - // Checks if the current playback media source is a placeholder, if so, then it is not ready + // Checks if the current playback media source is a placeholder, if so, then it is not ready. private boolean isPlaybackReady() { return sources != null && playQueue != null && sources.getSize() > playQueue.getIndex() && !(sources.getMediaSource(playQueue.getIndex()) instanceof PlaceholderMediaSource); @@ -351,11 +359,11 @@ public class MediaSourceManager { final MediaSource source = playbackListener.sourceOf(stream, streamInfo); if (source == null) { return new FailedMediaSource(stream, new IllegalStateException( - "MediaSource resolution is null")); + "MediaSource cannot be resolved")); } final long expiration = System.currentTimeMillis() + - TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS); + TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit); return new LoadedMediaSource(source, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); } 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 3daa58bb7..cd507c2bf 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.playlist; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import org.reactivestreams.Subscriber; @@ -170,7 +171,7 @@ public abstract class PlayQueue implements Serializable { * Returns the play queue's update broadcast. * May be null if the play queue message bus is not initialized. * */ - @NonNull + @Nullable public Flowable getBroadcastReceiver() { return broadcastReceiver; } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java index cd833c1ab..7c701a637 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java @@ -99,7 +99,9 @@ public class PlayQueueAdapter extends RecyclerView.Adapter Date: Mon, 26 Feb 2018 19:57:59 -0800 Subject: [PATCH 06/19] -Improved player queue stability by using more aggressive synchronization policy. -Added sync buttons on live streams to allow seeking to live edge. -Added custom cache key for extractor sources to allow more persistent reuse. -Refactored player data source factories into own class and separating live and non-live data sources. --- .../newpipe/player/BackgroundPlayer.java | 4 +- .../org/schabi/newpipe/player/BasePlayer.java | 108 ++++++++++-------- .../newpipe/player/ServicePlayerActivity.java | 19 +++ .../schabi/newpipe/player/VideoPlayer.java | 39 +++++-- .../newpipe/player/helper/CacheFactory.java | 2 +- .../player/helper/PlayerDataSource.java | 76 ++++++++++++ .../newpipe/player/helper/PlayerHelper.java | 14 ++- .../player/mediasource/FailedMediaSource.java | 14 ++- .../player/mediasource/LoadedMediaSource.java | 21 +++- .../mediasource/ManagedMediaSource.java | 6 +- .../mediasource/PlaceholderMediaSource.java | 7 +- .../player/playback/MediaSourceManager.java | 52 +++++++-- .../activity_player_queue_control.xml | 10 ++ .../main/res/layout/activity_main_player.xml | 11 ++ .../layout/activity_player_queue_control.xml | 10 ++ app/src/main/res/layout/player_popup.xml | 11 ++ app/src/main/res/values/strings.xml | 2 + 17 files changed, 320 insertions(+), 86 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java 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 2fc3252a7..7e5e612d6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -46,6 +46,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.LockManager; +import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -398,7 +399,8 @@ public final class BackgroundPlayer extends Service { if (index < 0 || index >= info.audio_streams.size()) return null; final AudioStream audio = info.audio_streams.get(index); - return buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.getFormatId())); + return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), + MediaFormat.getSuffixById(audio.getFormatId())); } @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 3e136dbde..1854e9a01 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -43,20 +43,11 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; @@ -66,8 +57,8 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; -import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; +import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.PlaybackListener; @@ -149,14 +140,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected boolean isPrepared = false; protected CustomTrackSelector trackSelector; - protected DataSource.Factory cacheDataSourceFactory; - protected DataSource.Factory cachelessDataSourceFactory; - protected SsMediaSource.Factory ssMediaSourceFactory; - protected HlsMediaSource.Factory hlsMediaSourceFactory; - protected DashMediaSource.Factory dashMediaSourceFactory; - protected ExtractorMediaSource.Factory extractorMediaSourceFactory; - protected SingleSampleMediaSource.Factory sampleMediaSourceFactory; + protected PlayerDataSource dataSource; protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; @@ -193,20 +178,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen final String userAgent = Downloader.USER_AGENT; final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); + final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); - trackSelector = new CustomTrackSelector(trackSelectionFactory); - cacheDataSourceFactory = new CacheFactory(context, userAgent, bandwidthMeter); - cachelessDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter); - - ssMediaSourceFactory = new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory); - hlsMediaSourceFactory = new HlsMediaSource.Factory(cachelessDataSourceFactory); - dashMediaSourceFactory = new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory); - extractorMediaSourceFactory = new ExtractorMediaSource.Factory(cacheDataSourceFactory); - sampleMediaSourceFactory = new SingleSampleMediaSource.Factory(cacheDataSourceFactory); final LoadControl loadControl = new LoadController(context); final RenderersFactory renderFactory = new DefaultRenderersFactory(context); @@ -319,26 +295,56 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen recordManager = null; } - public MediaSource buildMediaSource(String url, String overrideExtension) { + /*////////////////////////////////////////////////////////////////////////// + // MediaSource Building + //////////////////////////////////////////////////////////////////////////*/ + + public MediaSource buildLiveMediaSource(@NonNull final String sourceUrl, + @C.ContentType final int type) { if (DEBUG) { - Log.d(TAG, "buildMediaSource() called with: url = [" + url + - "], overrideExtension = [" + overrideExtension + "]"); + Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl + + "], content type = [" + type + "]"); } - Uri uri = Uri.parse(url); - int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : - Util.inferContentType("." + overrideExtension); + if (dataSource == null) return null; + + final Uri uri = Uri.parse(sourceUrl); switch (type) { case C.TYPE_SS: - return ssMediaSourceFactory.createMediaSource(uri); + return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); case C.TYPE_DASH: - return dashMediaSourceFactory.createMediaSource(uri); + return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri); case C.TYPE_HLS: - return hlsMediaSourceFactory.createMediaSource(uri); - case C.TYPE_OTHER: - return extractorMediaSourceFactory.createMediaSource(uri); - default: { + return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + public MediaSource buildMediaSource(@NonNull final String sourceUrl, + @NonNull final String cacheKey, + @NonNull final String overrideExtension) { + if (DEBUG) { + Log.d(TAG, "buildMediaSource() called with: url = [" + sourceUrl + + "], cacheKey = [" + cacheKey + "]" + + "], overrideExtension = [" + overrideExtension + "]"); + } + if (dataSource == null) return null; + + final Uri uri = Uri.parse(sourceUrl); + @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? + Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); + + switch (type) { + case C.TYPE_SS: + return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); + case C.TYPE_DASH: + return dataSource.getDashMediaSourceFactory().createMediaSource(uri); + case C.TYPE_HLS: + return dataSource.getHlsMediaSourceFactory().createMediaSource(uri); + case C.TYPE_OTHER: + return dataSource.getExtractorMediaSourceFactory(cacheKey).createMediaSource(uri); + default: throw new IllegalStateException("Unsupported type: " + type); - } } } @@ -478,7 +484,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // ExoPlayer Listener //////////////////////////////////////////////////////////////////////////*/ - private void recover() { + private void maybeRecover() { final int currentSourceIndex = playQueue.getIndex(); final PlayQueueItem currentSourceItem = playQueue.getItem(); @@ -554,7 +560,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } break; case Player.STATE_READY: //3 - recover(); + maybeRecover(); if (!isPrepared) { isPrepared = true; onPrepared(playWhenReady); @@ -566,7 +572,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen case Player.STATE_ENDED: // 4 // Ensure the current window has actually ended // since single windows that are still loading may produce an ended state - if (isCurrentWindowValid() && simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { + if (isCurrentWindowValid() && + simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { changeState(STATE_COMPLETED); isPrepared = false; } @@ -730,9 +737,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen @Override public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { if (!info.getHlsUrl().isEmpty()) { - return buildMediaSource(info.getHlsUrl(), "m3u8"); + return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); } else if (!info.getDashMpdUrl().isEmpty()) { - return buildMediaSource(info.getDashMpdUrl(), "mpd"); + return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH); } return null; @@ -852,8 +859,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void seekBy(int milliSeconds) { if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); - if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) + if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || + ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) { return; + } + int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); if (progress < 0) progress = 0; simpleExoPlayer.seekTo(progress); @@ -864,6 +874,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen && simpleExoPlayer.getCurrentPosition() >= 0; } + public void seekToDefault() { + if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition(); + } + /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 1378d9a80..d9c04b796 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -76,6 +76,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private SeekBar progressSeekBar; private TextView progressCurrentTime; private TextView progressEndTime; + private TextView progressLiveSync; private TextView seekDisplay; private ImageButton repeatButton; @@ -294,9 +295,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity progressCurrentTime = rootView.findViewById(R.id.current_time); progressSeekBar = rootView.findViewById(R.id.seek_bar); progressEndTime = rootView.findViewById(R.id.end_time); + progressLiveSync = rootView.findViewById(R.id.live_sync); seekDisplay = rootView.findViewById(R.id.seek_display); progressSeekBar.setOnSeekBarChangeListener(this); + progressLiveSync.setOnClickListener(this); } private void buildControls() { @@ -513,6 +516,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } else if (view.getId() == metadata.getId()) { scrollToSelected(); + } else if (view.getId() == progressLiveSync.getId()) { + player.seekToDefault(); + } } @@ -574,6 +580,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (info != null) { metadataTitle.setText(info.getName()); metadataArtist.setText(info.uploader_name); + + progressEndTime.setVisibility(View.GONE); + progressLiveSync.setVisibility(View.GONE); + switch (info.getStreamType()) { + case LIVE_STREAM: + case AUDIO_LIVE_STREAM: + progressLiveSync.setVisibility(View.VISIBLE); + break; + default: + progressEndTime.setVisibility(View.VISIBLE); + break; + } + scrollToSelected(); } } 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 3e03bc207..eca6415f6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -50,7 +50,6 @@ import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.TrackGroup; @@ -58,6 +57,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; +import com.google.android.exoplayer2.video.VideoListener; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; @@ -87,7 +87,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; */ @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class VideoPlayer extends BasePlayer - implements SimpleExoPlayer.VideoListener, + implements VideoListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, Player.EventListener, @@ -131,6 +131,7 @@ public abstract class VideoPlayer extends BasePlayer private SeekBar playbackSeekBar; private TextView playbackCurrentTime; private TextView playbackEndTime; + private TextView playbackLiveSync; private TextView playbackSpeedTextView; private View topControlsRoot; @@ -180,6 +181,7 @@ public abstract class VideoPlayer extends BasePlayer this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar); this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime); this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime); + this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync); this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed); this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.topControlsRoot = rootView.findViewById(R.id.topControls); @@ -221,6 +223,7 @@ public abstract class VideoPlayer extends BasePlayer qualityTextView.setOnClickListener(this); captionTextView.setOnClickListener(this); resizeView.setOnClickListener(this); + playbackLiveSync.setOnClickListener(this); } @Override @@ -261,7 +264,8 @@ public abstract class VideoPlayer extends BasePlayer qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); for (int i = 0; i < availableStreams.size(); i++) { VideoStream videoStream = availableStreams.get(i); - qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); + qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, + MediaFormat.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); } if (getSelectedVideoStream() != null) { qualityTextView.setText(getSelectedVideoStream().resolution); @@ -327,9 +331,22 @@ public abstract class VideoPlayer extends BasePlayer qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); + playbackEndTime.setVisibility(View.GONE); + playbackLiveSync.setVisibility(View.GONE); + final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); switch (streamType) { + case AUDIO_STREAM: + surfaceView.setVisibility(View.GONE); + break; + + case AUDIO_LIVE_STREAM: + surfaceView.setVisibility(View.GONE); + case LIVE_STREAM: + playbackLiveSync.setVisibility(View.VISIBLE); + break; + case VIDEO_STREAM: if (info.video_streams.size() + info.video_only_streams.size() == 0) break; @@ -344,14 +361,10 @@ public abstract class VideoPlayer extends BasePlayer buildQualityMenu(); qualityTextView.setVisibility(View.VISIBLE); - surfaceView.setVisibility(View.VISIBLE); - break; - case AUDIO_STREAM: - case AUDIO_LIVE_STREAM: - surfaceView.setVisibility(View.GONE); - break; + surfaceView.setVisibility(View.VISIBLE); default: + playbackEndTime.setVisibility(View.VISIBLE); break; } @@ -381,6 +394,7 @@ public abstract class VideoPlayer extends BasePlayer final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null; if (video != null) { final MediaSource streamSource = buildMediaSource(video.getUrl(), + PlayerHelper.cacheKeyOf(info, video), MediaFormat.getSuffixById(video.getFormatId())); mediaSources.add(streamSource); } @@ -393,6 +407,7 @@ public abstract class VideoPlayer extends BasePlayer // Merge with audio stream in case if video does not contain audio if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { final MediaSource audioSource = buildMediaSource(audio.getUrl(), + PlayerHelper.cacheKeyOf(info, audio), MediaFormat.getSuffixById(audio.getFormatId())); mediaSources.add(audioSource); } @@ -408,8 +423,8 @@ public abstract class VideoPlayer extends BasePlayer final Format textFormat = Format.createTextSampleFormat(null, mimeType, SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); - final MediaSource textSource = sampleMediaSourceFactory.createMediaSource( - Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); + final MediaSource textSource = dataSource.getSampleMediaSourceFactory() + .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); mediaSources.add(textSource); } @@ -635,6 +650,8 @@ public abstract class VideoPlayer extends BasePlayer onResizeClicked(); } else if (v.getId() == captionTextView.getId()) { onCaptionClicked(); + } else if (v.getId() == playbackLiveSync.getId()) { + seekToDefault(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index 900f13ae9..ec7813056 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -21,7 +21,7 @@ import org.schabi.newpipe.Downloader; import java.io.File; -public class CacheFactory implements DataSource.Factory { +/* package-private */ class CacheFactory implements DataSource.Factory { private static final String TAG = "CacheFactory"; private static final String CACHE_FOLDER_NAME = "exoplayer"; private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java new file mode 100644 index 000000000..40548aa6c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -0,0 +1,76 @@ +package org.schabi.newpipe.player.helper; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.SingleSampleMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.TransferListener; + +public class PlayerDataSource { + private static final int MANIFEST_MINIMUM_RETRY = 5; + private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000; + + private final DataSource.Factory cacheDataSourceFactory; + private final DataSource.Factory cachelessDataSourceFactory; + + public PlayerDataSource(@NonNull final Context context, + @NonNull final String userAgent, + @NonNull final TransferListener transferListener) { + cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); + cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); + } + + public SsMediaSource.Factory getLiveSsMediaSourceFactory() { + return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( + cachelessDataSourceFactory), cachelessDataSourceFactory) + .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY) + .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); + } + + public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { + return new HlsMediaSource.Factory(cachelessDataSourceFactory) + .setAllowChunklessPreparation(true) + .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY); + } + + public DashMediaSource.Factory getLiveDashMediaSourceFactory() { + return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( + cachelessDataSourceFactory), cachelessDataSourceFactory) + .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY) + .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); + } + + public SsMediaSource.Factory getSsMediaSourceFactory() { + return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( + cacheDataSourceFactory), cacheDataSourceFactory); + } + + public HlsMediaSource.Factory getHlsMediaSourceFactory() { + return new HlsMediaSource.Factory(cacheDataSourceFactory); + } + + public DashMediaSource.Factory getDashMediaSourceFactory() { + return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( + cacheDataSourceFactory), cacheDataSourceFactory); + } + + public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() { + return new ExtractorMediaSource.Factory(cacheDataSourceFactory); + } + + public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) { + return new ExtractorMediaSource.Factory(cacheDataSourceFactory).setCustomCacheKey(key); + } + + public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() { + return new SingleSampleMediaSource.Factory(cacheDataSourceFactory); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index ea3f73a17..6e2ff0ac9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -10,7 +10,10 @@ import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.Subtitles; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.SubtitlesFormat; +import org.schabi.newpipe.extractor.stream.VideoStream; import java.text.DecimalFormat; import java.text.NumberFormat; @@ -69,8 +72,7 @@ public class PlayerHelper { public static String captionLanguageOf(@NonNull final Context context, @NonNull final Subtitles subtitles) { final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); - return displayName + (subtitles.isAutoGenerated() ? - " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); + return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); } public static String resizeTypeOf(@NonNull final Context context, @@ -83,6 +85,14 @@ public class PlayerHelper { } } + public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) { + return info.getUrl() + video.getResolution() + video.getFormat().getName(); + } + + public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) { + return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName(); + } + public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { return isResumeAfterAudioFocusGain(context, false); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index a73c9975e..d88385d2d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.mediasource; import android.support.annotation.NonNull; +import android.util.Log; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.source.MediaPeriod; @@ -11,6 +12,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem; import java.io.IOException; public class FailedMediaSource implements ManagedMediaSource { + private final String TAG = "ManagedMediaSource@" + Integer.toHexString(hashCode()); private final PlayQueueItem playQueueItem; private final Throwable error; @@ -36,7 +38,7 @@ public class FailedMediaSource implements ManagedMediaSource { this.retryTimestamp = Long.MAX_VALUE; } - public PlayQueueItem getPlayQueueItem() { + public PlayQueueItem getStream() { return playQueueItem; } @@ -44,12 +46,14 @@ public class FailedMediaSource implements ManagedMediaSource { return error; } - public boolean canRetry() { + private boolean canRetry() { return System.currentTimeMillis() >= retryTimestamp; } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Log.e(TAG, "Loading failed source: ", error); + } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { @@ -68,7 +72,7 @@ public class FailedMediaSource implements ManagedMediaSource { public void releaseSource() {} @Override - public boolean canReplace() { - return canRetry(); + public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { + return newIdentity != playQueueItem || canRetry(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index ddc78bd77..f523667f9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -7,7 +7,6 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.Allocator; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.playlist.PlayQueueItem; import java.io.IOException; @@ -15,15 +14,25 @@ import java.io.IOException; public class LoadedMediaSource implements ManagedMediaSource { private final MediaSource source; - + private final PlayQueueItem stream; private final long expireTimestamp; - public LoadedMediaSource(@NonNull final MediaSource source, final long expireTimestamp) { + public LoadedMediaSource(@NonNull final MediaSource source, + @NonNull final PlayQueueItem stream, + final long expireTimestamp) { this.source = source; - + this.stream = stream; this.expireTimestamp = expireTimestamp; } + public PlayQueueItem getStream() { + return stream; + } + + private boolean isExpired() { + return System.currentTimeMillis() >= expireTimestamp; + } + @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { source.prepareSource(player, isTopLevelSource, listener); @@ -50,7 +59,7 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public boolean canReplace() { - return System.currentTimeMillis() >= expireTimestamp; + public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { + return newIdentity != stream || isExpired(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java index 5ac07c9f0..3bb7ca429 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java @@ -1,7 +1,11 @@ package org.schabi.newpipe.player.mediasource; +import android.support.annotation.NonNull; + import com.google.android.exoplayer2.source.MediaSource; +import org.schabi.newpipe.playlist.PlayQueueItem; + public interface ManagedMediaSource extends MediaSource { - boolean canReplace(); + boolean canReplace(@NonNull final PlayQueueItem newIdentity); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 0a389a9d9..0d3436a01 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -1,10 +1,13 @@ package org.schabi.newpipe.player.mediasource; +import android.support.annotation.NonNull; + 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.upstream.Allocator; +import org.schabi.newpipe.playlist.PlayQueueItem; + import java.io.IOException; public class PlaceholderMediaSource implements ManagedMediaSource { @@ -16,7 +19,7 @@ public class PlaceholderMediaSource implements ManagedMediaSource { @Override public void releaseSource() {} @Override - public boolean canReplace() { + public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { return true; } } 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 3b068f730..2a2a4ccb4 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; @@ -34,7 +35,11 @@ import io.reactivex.disposables.SerialDisposable; import io.reactivex.functions.Consumer; import io.reactivex.subjects.PublishSubject; +import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; + public class MediaSourceManager { + private final String TAG = "MediaSourceManager"; + // One-side rolling window size for default loading // Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0 private final int windowSize; @@ -233,10 +238,16 @@ public class MediaSourceManager { return playQueue.isComplete() || isWindowLoaded; } - // Checks if the current playback media source is a placeholder, if so, then it is not ready. private boolean isPlaybackReady() { - return sources != null && playQueue != null && sources.getSize() > playQueue.getIndex() && - !(sources.getMediaSource(playQueue.getIndex()) instanceof PlaceholderMediaSource); + if (sources == null || playQueue == null || sources.getSize() != playQueue.size()) { + return false; + } + + final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); + if (!(mediaSource instanceof LoadedMediaSource)) return false; + + final PlayQueueItem playQueueItem = playQueue.getItem(); + return playQueueItem == ((LoadedMediaSource) mediaSource).getStream(); } private void tryBlock() { @@ -280,7 +291,7 @@ public class MediaSourceManager { if (playQueue == null || playbackListener == null) return; // Ensure the current item is up to date with the play queue if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { - playbackListener.sync(syncedItem,info); + playbackListener.sync(syncedItem, info); } } @@ -329,14 +340,19 @@ public class MediaSourceManager { if (index > sources.getSize() - 1) return; final Consumer onDone = mediaSource -> { - update(playQueue.indexOf(item), mediaSource); + if (DEBUG) Log.d(TAG, " Loaded: [" + item.getTitle() + + "] with url: " + item.getUrl()); + + if (isCorrectionNeeded(item)) update(playQueue.indexOf(item), mediaSource); + loadingItems.remove(item); tryUnblock(); sync(); }; - if (!loadingItems.contains(item) && - ((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) { + if (!loadingItems.contains(item) && isCorrectionNeeded(item)) { + if (DEBUG) Log.d(TAG, "Loading: [" + item.getTitle() + + "] with url: " + item.getUrl()); loadingItems.add(item); final Disposable loader = getLoadedMediaSource(item) @@ -358,16 +374,32 @@ public class MediaSourceManager { final MediaSource source = playbackListener.sourceOf(stream, streamInfo); if (source == null) { - return new FailedMediaSource(stream, new IllegalStateException( - "MediaSource cannot be resolved")); + final Exception exception = new IllegalStateException( + "Unable to resolve source from stream info." + + " URL: " + stream.getUrl() + + ", audio count: " + streamInfo.audio_streams.size() + + ", video count: " + streamInfo.video_only_streams.size() + + streamInfo.video_streams.size()); + return new FailedMediaSource(stream, new IllegalStateException(exception)); } final long expiration = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit); - return new LoadedMediaSource(source, expiration); + return new LoadedMediaSource(source, stream, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); } + private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { + if (playQueue == null || sources == null) return false; + + final int index = playQueue.indexOf(item); + if (index == -1 || index >= sources.getSize()) return false; + + final MediaSource mediaSource = sources.getMediaSource(index); + return !(mediaSource instanceof ManagedMediaSource) || + ((ManagedMediaSource) mediaSource).canReplace(item); + } + /*////////////////////////////////////////////////////////////////////////// // MediaSource Playlist Helpers //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index a577b7fe0..c3480c547 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -296,5 +296,15 @@ android:textColor="?attr/colorAccent" tools:ignore="HardcodedText" tools:text="1:23:49"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 80c67974f..be778e7c8 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -397,6 +397,17 @@ android:textColor="@android:color/white" tools:ignore="HardcodedText" tools:text="1:23:49"/> + + diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml index a59e5ba2e..639a8037c 100644 --- a/app/src/main/res/layout/activity_player_queue_control.xml +++ b/app/src/main/res/layout/activity_player_queue_control.xml @@ -146,6 +146,16 @@ android:textColor="?attr/colorAccent" tools:ignore="HardcodedText" tools:text="1:23:49"/> + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 495842092..9dd5dcddb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -413,6 +413,8 @@ Normal Font Larger Font + SYNC + Enable LeakCanary Memory leak monitoring may cause app to become unresponsive when heap dumping From 77da40e507ecebc8c0b361d1ed846cf0fa3ca356 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 26 Feb 2018 22:37:19 -0800 Subject: [PATCH 07/19] -Added perpetual extractor source loading on network failures. -Fixed play queue playlist desynchronization caused by media source manager window loading expansion on sublist prior to current item. -Fixed failed media source not treated as ready for playback. --- .../player/helper/PlayerDataSource.java | 6 ++- .../player/playback/MediaSourceManager.java | 41 ++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java index 40548aa6c..133121269 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -16,6 +16,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; public class PlayerDataSource { private static final int MANIFEST_MINIMUM_RETRY = 5; + private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE; private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000; private final DataSource.Factory cacheDataSourceFactory; @@ -63,11 +64,12 @@ public class PlayerDataSource { } public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() { - return new ExtractorMediaSource.Factory(cacheDataSourceFactory); + return new ExtractorMediaSource.Factory(cacheDataSourceFactory) + .setMinLoadableRetryCount(EXTRACTOR_MINIMUM_RETRY); } public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) { - return new ExtractorMediaSource.Factory(cacheDataSourceFactory).setCustomCacheKey(key); + return getExtractorMediaSourceFactory().setCustomCacheKey(key); } public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() { 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 2a2a4ccb4..0f30169a7 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 @@ -38,11 +38,12 @@ import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; public class MediaSourceManager { - private final String TAG = "MediaSourceManager"; + private final static String TAG = "MediaSourceManager"; + + // WINDOW_SIZE determines how many streams AFTER the current stream should be loaded. + // The default value (1) ensures seamless playback under typical network settings. + private final static int WINDOW_SIZE = 1; - // One-side rolling window size for default loading - // Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0 - private final int windowSize; private final PlaybackListener playbackListener; private final PlayQueue playQueue; private final long expirationTimeMillis; @@ -68,23 +69,19 @@ public class MediaSourceManager { public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { - this(listener, playQueue, 1, 400L, 2, TimeUnit.HOURS); + this(listener, playQueue, + /*loadDebounceMillis=*/400L, + /*expirationTimeMillis=*/2, + /*expirationTimeUnit=*/TimeUnit.HOURS); } private MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue, - final int windowSize, final long loadDebounceMillis, final long expirationTimeMillis, @NonNull final TimeUnit expirationTimeUnit) { - if (windowSize <= 0) { - throw new UnsupportedOperationException( - "MediaSourceManager window size must be greater than 0"); - } - this.playbackListener = listener; this.playQueue = playQueue; - this.windowSize = windowSize; this.loadDebounceMillis = loadDebounceMillis; this.expirationTimeMillis = expirationTimeMillis; this.expirationTimeUnit = expirationTimeUnit; @@ -234,7 +231,7 @@ public class MediaSourceManager { private boolean isPlayQueueReady() { if (playQueue == null) return false; - final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > windowSize; + final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > WINDOW_SIZE; return playQueue.isComplete() || isWindowLoaded; } @@ -244,10 +241,14 @@ public class MediaSourceManager { } final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); - if (!(mediaSource instanceof LoadedMediaSource)) return false; - final PlayQueueItem playQueueItem = playQueue.getItem(); - return playQueueItem == ((LoadedMediaSource) mediaSource).getStream(); + + if (mediaSource instanceof LoadedMediaSource) { + return playQueueItem == ((LoadedMediaSource) mediaSource).getStream(); + } else if (mediaSource instanceof FailedMediaSource) { + return playQueueItem == ((FailedMediaSource) mediaSource).getStream(); + } + return false; } private void tryBlock() { @@ -318,11 +319,11 @@ public class MediaSourceManager { loadItem(currentItem); // The rest are just for seamless playback - final int leftBound = Math.max(0, currentIndex - windowSize); - final int rightLimit = currentIndex + windowSize + 1; + final int leftBound = currentIndex + 1; + final int rightLimit = leftBound + WINDOW_SIZE; final int rightBound = Math.min(playQueue.size(), rightLimit); - final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, - rightBound)); + final List items = new ArrayList<>( + playQueue.getStreams().subList(leftBound,rightBound)); // Do a round robin final int excess = rightLimit - playQueue.size(); From b4668367c6640986cd78f2ac06436520fddb9399 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 28 Feb 2018 17:45:05 -0800 Subject: [PATCH 08/19] -Added better assertions and documentations to new mechanism in MediaSourceManager. -Modified LoadController to allow fast playback start and increased buffer zigzag window. -Removed unnecessary loading on timeline changes. -Changed select message in MediaSourceManager to cause immediate load. -Reduced default expiration time in MediaSourceManager. -Fixed main video player not showing end time on audio-only streams. -Fixed live stream has player view disabled after transitioning from audio stream. -Fixed inconsistent progress bar height between live and non-live video on main player. --- app/build.gradle | 2 +- .../org/schabi/newpipe/player/BasePlayer.java | 20 ++-- .../schabi/newpipe/player/VideoPlayer.java | 5 + .../newpipe/player/helper/LoadController.java | 29 +++-- .../newpipe/player/helper/PlayerHelper.java | 23 +++- .../player/mediasource/FailedMediaSource.java | 2 +- .../player/playback/MediaSourceManager.java | 104 +++++++++++------- .../main/res/layout/activity_main_player.xml | 4 +- 8 files changed, 117 insertions(+), 72 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ba6406d4b..bfc22c76b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.karyogamy:NewPipeExtractor:837dbd6b86' + implementation 'com.github.karyogamy:NewPipeExtractor:4cf4ee394f' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' 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 1854e9a01..a449b4a36 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -511,15 +511,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen @Override public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); - - switch (reason) { - case Player.TIMELINE_CHANGE_REASON_PREPARED: - case Player.TIMELINE_CHANGE_REASON_RESET: - case Player.TIMELINE_CHANGE_REASON_DYNAMIC: - default: - if (playbackManager != null) playbackManager.load(); - break; - } } @Override @@ -654,6 +645,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } else { playQueue.offsetIndex(+1); } + playbackManager.load(); break; case DISCONTINUITY_REASON_SEEK: case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: @@ -661,7 +653,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen default: break; } - playbackManager.load(); } @Override @@ -724,8 +715,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen "], queue index=[" + playQueue.getIndex() + "]"); } else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) { final long startPos = info != null ? info.start_position : 0; - if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + - " at: " + getTimeString((int)startPos)); + if (DEBUG) Log.d(TAG, "Rewinding to correct window=[" + currentSourceIndex + "]," + + " at=[" + getTimeString((int)startPos) + "]," + + " from=[" + simpleExoPlayer.getCurrentPeriodIndex() + "]."); simpleExoPlayer.seekTo(currentSourceIndex, startPos); } @@ -974,7 +966,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } public boolean isPlaying() { - return simpleExoPlayer.getPlaybackState() == Player.STATE_READY && simpleExoPlayer.getPlayWhenReady(); + final int state = simpleExoPlayer.getPlaybackState(); + return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) + && simpleExoPlayer.getPlayWhenReady(); } public int getRepeatMode() { 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 eca6415f6..5a7a9a462 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -339,11 +339,16 @@ public abstract class VideoPlayer extends BasePlayer switch (streamType) { case AUDIO_STREAM: surfaceView.setVisibility(View.GONE); + playbackEndTime.setVisibility(View.VISIBLE); break; case AUDIO_LIVE_STREAM: surfaceView.setVisibility(View.GONE); + playbackLiveSync.setVisibility(View.VISIBLE); + break; + case LIVE_STREAM: + surfaceView.setVisibility(View.VISIBLE); playbackLiveSync.setVisibility(View.VISIBLE); break; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index 15668be90..7670deb98 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -11,7 +11,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; -import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS; import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES; @@ -19,6 +18,7 @@ public class LoadController implements LoadControl { public static final String TAG = "LoadController"; + private final long initialPlaybackBufferUs; private final LoadControl internalLoadControl; /*////////////////////////////////////////////////////////////////////////// @@ -26,18 +26,24 @@ public class LoadController implements LoadControl { //////////////////////////////////////////////////////////////////////////*/ public LoadController(final Context context) { - this(PlayerHelper.getMinBufferMs(context), - PlayerHelper.getMaxBufferMs(context), - PlayerHelper.getBufferForPlaybackMs(context)); + this(PlayerHelper.getPlaybackStartBufferMs(context), + PlayerHelper.getPlaybackMinimumBufferMs(context), + PlayerHelper.getPlaybackOptimalBufferMs(context)); } - private LoadController(final int minBufferMs, final int maxBufferMs, - final int bufferForPlaybackMs) { + private LoadController(final int initialPlaybackBufferMs, + final int minimumPlaybackbufferMs, + final int optimalPlaybackBufferMs) { + this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000; + final DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); - internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, - bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + internalLoadControl = new DefaultLoadControl(allocator, + /*minBufferMs=*/minimumPlaybackbufferMs, + /*maxBufferMs=*/optimalPlaybackBufferMs, + /*bufferForPlaybackMs=*/initialPlaybackBufferMs, + /*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs, DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } @@ -89,7 +95,10 @@ public class LoadController implements LoadControl { @Override public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { - return internalLoadControl.shouldStartPlayback(bufferedDurationUs, playbackSpeed, - rebuffering); + final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >= + this.initialPlaybackBufferUs * playbackSpeed; + final boolean isInternalStartingPlayback = internalLoadControl.shouldStartPlayback( + bufferedDurationUs, playbackSpeed, rebuffering); + return isInitialPlaybackBufferFilled || isInternalStartingPlayback; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 6e2ff0ac9..813c69c22 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -117,16 +117,27 @@ public class PlayerHelper { return 512 * 1024L; } - public static int getMinBufferMs(@NonNull final Context context) { - return 15000; + /** + * Returns the number of milliseconds the player buffers for before starting playback. + * */ + public static int getPlaybackStartBufferMs(@NonNull final Context context) { + return 500; } - public static int getMaxBufferMs(@NonNull final Context context) { - return 30000; + /** + * Returns the minimum number of milliseconds the player always buffers to after starting + * playback. + * */ + public static int getPlaybackMinimumBufferMs(@NonNull final Context context) { + return 25000; } - public static int getBufferForPlaybackMs(@NonNull final Context context) { - return 2500; + /** + * Returns the maximum/optimal number of milliseconds the player will buffer to once the buffer + * hits the point of {@link #getPlaybackMinimumBufferMs(Context)}. + * */ + public static int getPlaybackOptimalBufferMs(@NonNull final Context context) { + return 60000; } public static boolean isUsingDSP(@NonNull final Context context) { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index d88385d2d..d07baf2a7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -12,7 +12,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem; import java.io.IOException; public class FailedMediaSource implements ManagedMediaSource { - private final String TAG = "ManagedMediaSource@" + Integer.toHexString(hashCode()); + private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final PlayQueueItem playQueueItem; private final Throwable error; 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 0f30169a7..8f91e53c2 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,5 +1,6 @@ package org.schabi.newpipe.player.playback; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -28,7 +29,6 @@ import java.util.concurrent.TimeUnit; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.annotations.NonNull; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; @@ -38,23 +38,26 @@ import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; public class MediaSourceManager { - private final static String TAG = "MediaSourceManager"; + @NonNull private final static String TAG = "MediaSourceManager"; // WINDOW_SIZE determines how many streams AFTER the current stream should be loaded. // The default value (1) ensures seamless playback under typical network settings. private final static int WINDOW_SIZE = 1; - private final PlaybackListener playbackListener; - private final PlayQueue playQueue; + @NonNull private final PlaybackListener playbackListener; + @NonNull private final PlayQueue playQueue; + + // Once a MediaSource item has been detected to be expired, the manager will immediately + // trigger a reload on the associated PlayQueueItem, which may disrupt playback, + // if the item is being played private final long expirationTimeMillis; - private final TimeUnit expirationTimeUnit; // Process only the last load order when receiving a stream of load orders (lessens I/O) // The higher it is, the less loading occurs during rapid noncritical timeline changes // Not recommended to go below 100ms private final long loadDebounceMillis; - private final PublishSubject debouncedLoadSignal; - private final Disposable debouncedLoader; + @NonNull private final Disposable debouncedLoader; + @NonNull private final PublishSubject debouncedSignal; private DynamicConcatenatingMediaSource sources; @@ -71,23 +74,20 @@ public class MediaSourceManager { @NonNull final PlayQueue playQueue) { this(listener, playQueue, /*loadDebounceMillis=*/400L, - /*expirationTimeMillis=*/2, - /*expirationTimeUnit=*/TimeUnit.HOURS); + /*expirationTimeMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES)); } private MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue, final long loadDebounceMillis, - final long expirationTimeMillis, - @NonNull final TimeUnit expirationTimeUnit) { + final long expirationTimeMillis) { this.playbackListener = listener; this.playQueue = playQueue; this.loadDebounceMillis = loadDebounceMillis; this.expirationTimeMillis = expirationTimeMillis; - this.expirationTimeUnit = expirationTimeUnit; this.loaderReactor = new CompositeDisposable(); - this.debouncedLoadSignal = PublishSubject.create(); + this.debouncedSignal = PublishSubject.create(); this.debouncedLoader = getDebouncedLoader(); this.sources = new DynamicConcatenatingMediaSource(); @@ -109,8 +109,11 @@ public class MediaSourceManager { * Dispose the manager and releases all message buses and loaders. * */ public void dispose() { - if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete(); - if (debouncedLoader != null) debouncedLoader.dispose(); + if (DEBUG) Log.d(TAG, "dispose() called."); + + debouncedSignal.onComplete(); + debouncedLoader.dispose(); + if (playQueueReactor != null) playQueueReactor.cancel(); if (loaderReactor != null) loaderReactor.dispose(); if (syncReactor != null) syncReactor.dispose(); @@ -129,6 +132,7 @@ public class MediaSourceManager { * Unblocks the player once the item at the current index is loaded. * */ public void load() { + if (DEBUG) Log.d(TAG, "load() called."); loadDebounced(); } @@ -139,6 +143,8 @@ public class MediaSourceManager { * through {@link #load() load}. * */ public void reset() { + if (DEBUG) Log.d(TAG, "reset() called."); + tryBlock(); syncedItem = null; @@ -205,11 +211,11 @@ public class MediaSourceManager { case INIT: case REORDER: case ERROR: + case SELECT: loadImmediate(); // low frequency, critical events break; case APPEND: case REMOVE: - case SELECT: case MOVE: case RECOVERY: default: @@ -229,16 +235,12 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private boolean isPlayQueueReady() { - if (playQueue == null) return false; - final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > WINDOW_SIZE; return playQueue.isComplete() || isWindowLoaded; } private boolean isPlaybackReady() { - if (sources == null || playQueue == null || sources.getSize() != playQueue.size()) { - return false; - } + if (sources == null || sources.getSize() != playQueue.size()) return false; final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); final PlayQueueItem playQueueItem = playQueue.getItem(); @@ -252,6 +254,8 @@ public class MediaSourceManager { } private void tryBlock() { + if (DEBUG) Log.d(TAG, "tryBlock() called."); + if (isBlocked) return; playbackListener.block(); @@ -261,6 +265,8 @@ public class MediaSourceManager { } private void tryUnblock() { + if (DEBUG) Log.d(TAG, "tryUnblock() called."); + if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) { isBlocked = false; playbackListener.unblock(sources); @@ -272,6 +278,8 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private void sync() { + if (DEBUG) Log.d(TAG, "sync() called."); + final PlayQueueItem currentItem = playQueue.getItem(); if (isBlocked || currentItem == null) return; @@ -289,7 +297,6 @@ public class MediaSourceManager { private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { - if (playQueue == null || playbackListener == null) return; // Ensure the current item is up to date with the play queue if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { playbackListener.sync(syncedItem, info); @@ -301,14 +308,14 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private Disposable getDebouncedLoader() { - return debouncedLoadSignal + return debouncedSignal .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(timestamp -> loadImmediate()); } private void loadDebounced() { - debouncedLoadSignal.onNext(System.currentTimeMillis()); + debouncedSignal.onNext(System.currentTimeMillis()); } private void loadImmediate() { @@ -316,7 +323,7 @@ public class MediaSourceManager { final int currentIndex = playQueue.getIndex(); final PlayQueueItem currentItem = playQueue.getItem(currentIndex); if (currentItem == null) return; - loadItem(currentItem); + maybeLoadItem(currentItem); // The rest are just for seamless playback final int leftBound = currentIndex + 1; @@ -331,10 +338,14 @@ public class MediaSourceManager { items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); } - for (final PlayQueueItem item: items) loadItem(item); + for (final PlayQueueItem item : items) { + maybeLoadItem(item); + } } - private void loadItem(@Nullable final PlayQueueItem item) { + private void maybeLoadItem(@Nullable final PlayQueueItem item) { + if (DEBUG) Log.d(TAG, "maybeLoadItem() called."); + if (sources == null || item == null) return; final int index = playQueue.indexOf(item); @@ -368,11 +379,6 @@ public class MediaSourceManager { private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { return stream.getStream().map(streamInfo -> { - if (playbackListener == null) { - return new FailedMediaSource(stream, new IllegalStateException( - "MediaSourceManager playback listener unavailable")); - } - final MediaSource source = playbackListener.sourceOf(stream, streamInfo); if (source == null) { final Exception exception = new IllegalStateException( @@ -384,21 +390,34 @@ public class MediaSourceManager { return new FailedMediaSource(stream, new IllegalStateException(exception)); } - final long expiration = System.currentTimeMillis() + - TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit); + final long expiration = System.currentTimeMillis() + expirationTimeMillis; return new LoadedMediaSource(source, stream, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); } + /** + * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource} + * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback + * readiness or playlist desynchronization. + *

+ * If the given {@link PlayQueueItem} is currently being played and is already loaded, + * then correction is not only needed if the playlist is desynchronized. Otherwise, the + * check depends on the status (e.g. expiration or placeholder) of the + * {@link ManagedMediaSource}. + * */ private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { - if (playQueue == null || sources == null) return false; + if (sources == null) return false; final int index = playQueue.indexOf(item); if (index == -1 || index >= sources.getSize()) return false; - final MediaSource mediaSource = sources.getMediaSource(index); - return !(mediaSource instanceof ManagedMediaSource) || - ((ManagedMediaSource) mediaSource).canReplace(item); + final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index); + + if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) { + return item != ((LoadedMediaSource) mediaSource).getStream(); + } else { + return mediaSource.canReplace(item); + } } /*////////////////////////////////////////////////////////////////////////// @@ -406,11 +425,14 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private void resetSources() { + if (DEBUG) Log.d(TAG, "resetSources() called."); + if (this.sources != null) this.sources.releaseSource(); this.sources = new DynamicConcatenatingMediaSource(); } private void populateSources() { + if (DEBUG) Log.d(TAG, "populateSources() called."); if (sources == null || sources.getSize() >= playQueue.size()) return; for (int index = sources.getSize() - 1; index < playQueue.size(); index++) { @@ -462,8 +484,12 @@ public class MediaSourceManager { * Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource} * at the given index with a given {@link MediaSource}. If the index is out of bound, * then the replacement is ignored. + *

+ * Not recommended to use on indices LESS THAN the currently playing index, since + * this will modify the playback timeline prior to the index and cause desynchronization + * on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}. * */ - private void update(final int index, final MediaSource source) { + private synchronized void update(final int index, final MediaSource source) { if (sources == null) return; if (index < 0 || index >= sources.getSize()) return; diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index be778e7c8..9abe43715 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -401,8 +401,8 @@ Date: Wed, 28 Feb 2018 17:47:12 -0800 Subject: [PATCH 09/19] -Added serialized cache for transferring serializable objects too large for intent transactions. -Fixed potential transaction too large exceptions for player intents. --- .../org/schabi/newpipe/player/BasePlayer.java | 12 +- .../playlist/AbstractInfoPlayQueue.java | 1 + .../schabi/newpipe/playlist/PlayQueue.java | 1 + .../schabi/newpipe/util/NavigationHelper.java | 38 +++--- .../schabi/newpipe/util/SerializedCache.java | 112 ++++++++++++++++++ 5 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/SerializedCache.java 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 a449b4a36..6a867110a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -65,8 +65,8 @@ import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueAdapter; import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.util.SerializedCache; -import java.io.Serializable; import java.util.concurrent.TimeUnit; import io.reactivex.Observable; @@ -106,7 +106,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public static final String PLAYBACK_PITCH = "playback_pitch"; public static final String PLAYBACK_SPEED = "playback_speed"; public static final String PLAYBACK_QUALITY = "playback_quality"; - public static final String PLAY_QUEUE = "play_queue"; + public static final String PLAY_QUEUE_KEY = "play_queue_key"; public static final String APPEND_ONLY = "append_only"; public static final String SELECT_ON_APPEND = "select_on_append"; @@ -207,10 +207,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (intent == null) return; // Resolve play queue - if (!intent.hasExtra(PLAY_QUEUE)) return; - final Serializable playQueueCandidate = intent.getSerializableExtra(PLAY_QUEUE); - if (!(playQueueCandidate instanceof PlayQueue)) return; - final PlayQueue queue = (PlayQueue) playQueueCandidate; + if (!intent.hasExtra(PLAY_QUEUE_KEY)) return; + final String intentCacheKey = intent.getStringExtra(PLAY_QUEUE_KEY); + final PlayQueue queue = SerializedCache.getInstance().take(intentCacheKey, PlayQueue.class); + if (queue == null) return; // Resolve append intents if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { diff --git a/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java index fc1fabfc4..6e63a3aaa 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java @@ -118,6 +118,7 @@ abstract class AbstractInfoPlayQueue ext public void dispose() { super.dispose(); if (fetchReactor != null) fetchReactor.dispose(); + fetchReactor = null; } private static List extractListItems(final List infos) { 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 cd507c2bf..9005ef8d2 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -84,6 +84,7 @@ public abstract class PlayQueue implements Serializable { if (eventBroadcast != null) eventBroadcast.onComplete(); if (reportingReactor != null) reportingReactor.cancel(); + eventBroadcast = null; broadcastReceiver = null; reportingReactor = null; } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 2039dcc8e..da1019f99 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -7,6 +7,8 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v7.app.AlertDialog; @@ -33,9 +35,9 @@ import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; +import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; import org.schabi.newpipe.fragments.local.bookmark.LocalPlaylistFragment; import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; -import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; @@ -59,39 +61,41 @@ public class NavigationHelper { // Players //////////////////////////////////////////////////////////////////////////*/ - public static Intent getPlayerIntent(final Context context, - final Class targetClazz, - final PlayQueue playQueue, - final String quality) { - Intent intent = new Intent(context, targetClazz) - .putExtra(VideoPlayer.PLAY_QUEUE, playQueue); + public static Intent getPlayerIntent(@NonNull final Context context, + @NonNull final Class targetClazz, + @NonNull final PlayQueue playQueue, + @Nullable final String quality) { + Intent intent = new Intent(context, targetClazz); + + final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); + if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); return intent; } - public static Intent getPlayerIntent(final Context context, - final Class targetClazz, - final PlayQueue playQueue) { + public static Intent getPlayerIntent(@NonNull final Context context, + @NonNull final Class targetClazz, + @NonNull final PlayQueue playQueue) { return getPlayerIntent(context, targetClazz, playQueue, null); } - public static Intent getPlayerEnqueueIntent(final Context context, - final Class targetClazz, - final PlayQueue playQueue, + public static Intent getPlayerEnqueueIntent(@NonNull final Context context, + @NonNull final Class targetClazz, + @NonNull final PlayQueue playQueue, final boolean selectOnAppend) { return getPlayerIntent(context, targetClazz, playQueue) .putExtra(BasePlayer.APPEND_ONLY, true) .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); } - public static Intent getPlayerIntent(final Context context, - final Class targetClazz, - final PlayQueue playQueue, + public static Intent getPlayerIntent(@NonNull final Context context, + @NonNull final Class targetClazz, + @NonNull final PlayQueue playQueue, final int repeatMode, final float playbackSpeed, final float playbackPitch, - final String playbackQuality) { + @Nullable final String playbackQuality) { return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) diff --git a/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java b/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java new file mode 100644 index 000000000..02871aff5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java @@ -0,0 +1,112 @@ +package org.schabi.newpipe.util; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.util.LruCache; +import android.util.Log; + +import org.schabi.newpipe.MainActivity; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.UUID; + +public class SerializedCache { + private static final boolean DEBUG = MainActivity.DEBUG; + private final String TAG = getClass().getSimpleName(); + + private static final SerializedCache instance = new SerializedCache(); + private static final int MAX_ITEMS_ON_CACHE = 5; + + private static final LruCache lruCache = + new LruCache<>(MAX_ITEMS_ON_CACHE); + + private SerializedCache() { + //no instance + } + + public static SerializedCache getInstance() { + return instance; + } + + @Nullable + public T take(@NonNull final String key, @NonNull final Class type) { + if (DEBUG) Log.d(TAG, "take() called with: key = [" + key + "]"); + synchronized (lruCache) { + return lruCache.get(key) != null ? getItem(lruCache.remove(key), type) : null; + } + } + + @Nullable + public T get(@NonNull final String key, @NonNull final Class type) { + if (DEBUG) Log.d(TAG, "get() called with: key = [" + key + "]"); + synchronized (lruCache) { + final CacheData data = lruCache.get(key); + return data != null ? getItem(data, type) : null; + } + } + + @Nullable + public String put(@NonNull T item, @NonNull final Class type) { + final String key = UUID.randomUUID().toString(); + return put(key, item, type) ? key : null; + } + + public boolean put(@NonNull final String key, @NonNull T item, + @NonNull final Class type) { + if (DEBUG) Log.d(TAG, "put() called with: key = [" + key + "], item = [" + item + "]"); + synchronized (lruCache) { + try { + lruCache.put(key, new CacheData<>(clone(item, type), type)); + return true; + } catch (final Exception error) { + Log.e(TAG, "Serialization failed for: ", error); + } + } + return false; + } + + public void clear() { + if (DEBUG) Log.d(TAG, "clear() called"); + synchronized (lruCache) { + lruCache.evictAll(); + } + } + + public long size() { + synchronized (lruCache) { + return lruCache.size(); + } + } + + @Nullable + private T getItem(@NonNull final CacheData data, @NonNull final Class type) { + return type.isAssignableFrom(data.type) ? type.cast(data.item) : null; + } + + @NonNull + private T clone(@NonNull T item, + @NonNull final Class type) throws Exception { + final ByteArrayOutputStream bytesOutput = new ByteArrayOutputStream(); + try (final ObjectOutputStream objectOutput = new ObjectOutputStream(bytesOutput)) { + objectOutput.writeObject(item); + objectOutput.flush(); + } + final Object clone = new ObjectInputStream( + new ByteArrayInputStream(bytesOutput.toByteArray())).readObject(); + return type.cast(clone); + } + + final private static class CacheData { + private final T item; + private final Class type; + + private CacheData(@NonNull final T item, @NonNull Class type) { + this.item = item; + this.type = type; + } + } +} From 9ea08c8a4b1e02beb7b08db301aced50c9534e82 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 28 Feb 2018 23:25:45 -0800 Subject: [PATCH 10/19] -Re-added loading for items prior to current index in MediaSourceManager to allow faster access time. -Added some null checks annotation. --- .../player/playback/MediaSourceManager.java | 21 ++++++++++++------- .../schabi/newpipe/playlist/PlayQueue.java | 10 ++++----- .../schabi/newpipe/util/NavigationHelper.java | 4 ++++ 3 files changed, 22 insertions(+), 13 deletions(-) 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 8f91e53c2..439885e58 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 @@ -326,8 +326,10 @@ public class MediaSourceManager { maybeLoadItem(currentItem); // The rest are just for seamless playback - final int leftBound = currentIndex + 1; - final int rightLimit = leftBound + WINDOW_SIZE; + // Although timeline is not updated prior to the current index, these sources are still + // loaded into the cache for faster retrieval at a potentially later time. + 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)); @@ -343,10 +345,9 @@ public class MediaSourceManager { } } - private void maybeLoadItem(@Nullable final PlayQueueItem item) { + private void maybeLoadItem(@NonNull final PlayQueueItem item) { if (DEBUG) Log.d(TAG, "maybeLoadItem() called."); - - if (sources == null || item == null) return; + if (sources == null) return; final int index = playQueue.indexOf(item); if (index > sources.getSize() - 1) return; @@ -355,7 +356,11 @@ public class MediaSourceManager { if (DEBUG) Log.d(TAG, " Loaded: [" + item.getTitle() + "] with url: " + item.getUrl()); - if (isCorrectionNeeded(item)) update(playQueue.indexOf(item), mediaSource); + final int itemIndex = playQueue.indexOf(item); + // Only update the playlist timeline for items at the current index or after. + if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) { + update(itemIndex, mediaSource); + } loadingItems.remove(item); tryUnblock(); @@ -449,7 +454,7 @@ public class MediaSourceManager { * with position * in respect to the play queue only if no {@link MediaSource} * already exists at the given index. * */ - private void emplace(final int index, final MediaSource source) { + private void emplace(final int index, @NonNull final MediaSource source) { if (sources == null) return; if (index < 0 || index < sources.getSize()) return; @@ -489,7 +494,7 @@ public class MediaSourceManager { * this will modify the playback timeline prior to the index and cause desynchronization * on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}. * */ - private synchronized void update(final int index, final MediaSource source) { + private synchronized void update(final int index, @NonNull final MediaSource source) { if (sources == null) return; if (index < 0 || index >= sources.getSize()) return; 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 9005ef8d2..8f4c5913d 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -45,7 +45,7 @@ public abstract class PlayQueue implements Serializable { private ArrayList backup; private ArrayList streams; - private final AtomicInteger queueIndex; + @NonNull private final AtomicInteger queueIndex; private transient BehaviorSubject eventBroadcast; private transient Flowable broadcastReceiver; @@ -133,7 +133,7 @@ public abstract class PlayQueue implements Serializable { * Returns the index of the given item using referential equality. * May be null despite play queue contains identical item. * */ - public int indexOf(final PlayQueueItem item) { + public int indexOf(@NonNull final PlayQueueItem item) { // referential equality, can't think of a better way to do this // todo: better than this return streams.indexOf(item); @@ -213,7 +213,7 @@ public abstract class PlayQueue implements Serializable { * * @see #append(List items) * */ - public synchronized void append(final PlayQueueItem... items) { + public synchronized void append(@NonNull final PlayQueueItem... items) { append(Arrays.asList(items)); } @@ -225,7 +225,7 @@ public abstract class PlayQueue implements Serializable { * * Will emit a {@link AppendEvent} on any given context. * */ - public synchronized void append(final List items) { + public synchronized void append(@NonNull final List items) { List itemList = new ArrayList<>(items); if (isShuffled()) { @@ -393,7 +393,7 @@ public abstract class PlayQueue implements Serializable { // Rx Broadcast //////////////////////////////////////////////////////////////////////////*/ - private void broadcast(final PlayQueueEvent event) { + private void broadcast(@NonNull final PlayQueueEvent event) { if (eventBroadcast != null) { eventBroadcast.onNext(event); } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index da1019f99..4fc854416 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -61,6 +61,7 @@ public class NavigationHelper { // Players //////////////////////////////////////////////////////////////////////////*/ + @NonNull public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, @@ -74,12 +75,14 @@ public class NavigationHelper { return intent; } + @NonNull public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue) { return getPlayerIntent(context, targetClazz, playQueue, null); } + @NonNull public static Intent getPlayerEnqueueIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, @@ -89,6 +92,7 @@ public class NavigationHelper { .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); } + @NonNull public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, From fdfb53c05c963ea31e0e5d10c2fcbc0f616cd036 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Fri, 2 Mar 2018 10:10:15 +0100 Subject: [PATCH 11/19] Update README.md --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 030963a89..52c1159dd 100644 --- a/README.md +++ b/README.md @@ -59,18 +59,17 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only * Search/Watch Playlists * Watch as queues Playlists * Queuing videos +* Local playlists +* Subtitles +* Multi-service support (eg. SoundCloud in NewPipe Beta) ### Coming Features -* Multiservice support (eg. SoundCloud) -* Bookmarks -* Subtitles support -* livestream support +* Livestream support +* Cast to UPnP and Cast +* Show comments * ... and many more -### Multiservice support -Although NewPipe only supports YouTube at the moment, it's designed to support many more streaming services. The plan is, that NewPipe will get such support by the version 2.0. - ## Contribution Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome. The more is done the better it gets! From 0c17f0825b6f8b23ccba2c845fa5275754b29848 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 3 Mar 2018 11:42:23 -0800 Subject: [PATCH 12/19] -Added loader eviction to avoid spawning too many threads in MediaSourceManager. -Added nonnull and final constraints to variables in MediaSourceManager. -Added nonnull and final constraints on context related objects in BasePlayer. -Fixed Hls livestreams crashing player when behind live window for too long. -Fixed cache miss when InfoCache key mismatch between StreamInfo and StreamInfoItem. --- app/build.gradle | 2 +- .../fragments/detail/VideoDetailFragment.java | 2 +- .../newpipe/player/BackgroundPlayer.java | 11 +- .../org/schabi/newpipe/player/BasePlayer.java | 363 +++++++++++------- .../newpipe/player/PopupVideoPlayer.java | 10 +- .../schabi/newpipe/player/VideoPlayer.java | 7 +- .../player/playback/MediaSourceManager.java | 209 +++++----- .../newpipe/playlist/PlayQueueAdapter.java | 29 +- .../schabi/newpipe/util/ExtractorHelper.java | 2 +- .../org/schabi/newpipe/util/InfoCache.java | 37 +- 10 files changed, 395 insertions(+), 277 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bfc22c76b..74a005ce3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.karyogamy:NewPipeExtractor:4cf4ee394f' + implementation 'com.github.karyogamy:NewPipeExtractor:b4206479cb' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index b306721ba..6d505b00e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -322,7 +322,7 @@ public class VideoDetailFragment if (serializable instanceof StreamInfo) { //noinspection unchecked currentInfo = (StreamInfo) serializable; - InfoCache.getInstance().putInfo(currentInfo); + InfoCache.getInstance().putInfo(serviceId, url, currentInfo); } serializable = savedState.getSerializable(STACK_KEY); 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 7e5e612d6..f002115f8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -33,6 +33,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.util.Log; +import android.view.View; import android.widget.RemoteViews; import com.google.android.exoplayer2.PlaybackParameters; @@ -292,15 +293,15 @@ public final class BackgroundPlayer extends Service { } @Override - public void onThumbnailReceived(Bitmap thumbnail) { - super.onThumbnailReceived(thumbnail); + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + super.onLoadingComplete(imageUri, view, loadedImage); - if (thumbnail != null) { + if (loadedImage != null) { // rebuild notification here since remote view does not release bitmaps, causing memory leaks resetNotification(); - if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); - if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); + if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); + if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); updateNotification(-1); } 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 6a867110a..86a4d1234 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -43,6 +43,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; @@ -50,7 +51,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; @@ -67,6 +69,8 @@ import org.schabi.newpipe.playlist.PlayQueueAdapter; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.SerializedCache; +import java.io.IOException; +import java.net.UnknownHostException; import java.util.concurrent.TimeUnit; import io.reactivex.Observable; @@ -86,17 +90,18 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; * @author mauriciocolli */ @SuppressWarnings({"WeakerAccess"}) -public abstract class BasePlayer implements Player.EventListener, PlaybackListener { +public abstract class BasePlayer implements + Player.EventListener, PlaybackListener, ImageLoadingListener { public static final boolean DEBUG = true; - public static final String TAG = "BasePlayer"; + @NonNull public static final String TAG = "BasePlayer"; - protected Context context; + @NonNull final protected Context context; - protected BroadcastReceiver broadcastReceiver; - protected IntentFilter intentFilter; + @NonNull final protected BroadcastReceiver broadcastReceiver; + @NonNull final protected IntentFilter intentFilter; - protected PlayQueueAdapter playQueueAdapter; + @NonNull final protected HistoryRecordManager recordManager; /*////////////////////////////////////////////////////////////////////////// // Intent @@ -117,8 +122,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f}; - protected MediaSourceManager playbackManager; protected PlayQueue playQueue; + protected PlayQueueAdapter playQueueAdapter; + + protected MediaSourceManager playbackManager; protected StreamInfo currentInfo; protected PlayQueueItem currentItem; @@ -134,23 +141,20 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected final static int PROGRESS_LOOP_INTERVAL = 500; protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds + protected CustomTrackSelector trackSelector; + protected PlayerDataSource dataSource; + protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; protected boolean isPrepared = false; - protected CustomTrackSelector trackSelector; - - protected PlayerDataSource dataSource; - protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; - protected HistoryRecordManager recordManager; - //////////////////////////////////////////////////////////////////////////*/ - public BasePlayer(Context context) { + public BasePlayer(@NonNull final Context context) { this.context = context; this.broadcastReceiver = new BroadcastReceiver() { @@ -162,6 +166,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen this.intentFilter = new IntentFilter(); setupBroadcastReceiver(intentFilter); context.registerReceiver(broadcastReceiver, intentFilter); + + this.recordManager = new HistoryRecordManager(context); } public void setup() { @@ -172,7 +178,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void initPlayer() { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); - if (recordManager == null) recordManager = new HistoryRecordManager(context); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); databaseUpdateReactor = new CompositeDisposable(); @@ -195,13 +200,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void initListeners() {} - private Disposable getProgressReactor() { - return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .filter(ignored -> isProgressLoopRunning()) - .subscribe(ignored -> triggerProgressUpdate()); - } - public void handleIntent(Intent intent) { if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); if (intent == null) return; @@ -217,7 +215,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen int sizeBeforeAppend = playQueue.size(); playQueue.append(queue.getStreams()); - if (intent.getBooleanExtra(SELECT_ON_APPEND, false) && queue.getStreams().size() > 0) { + if (intent.getBooleanExtra(SELECT_ON_APPEND, false) && + queue.getStreams().size() > 0) { playQueue.setIndex(sizeBeforeAppend); } @@ -247,24 +246,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen playQueueAdapter = new PlayQueueAdapter(context, playQueue); } - public void initThumbnail(final String url) { - if (DEBUG) Log.d(TAG, "initThumbnail() called"); - if (url == null || url.isEmpty()) return; - ImageLoader.getInstance().resume(); - ImageLoader.getInstance().loadImage(url, new SimpleImageLoadingListener() { - @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - if (simpleExoPlayer == null) return; - if (DEBUG) Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]"); - onThumbnailReceived(loadedImage); - } - }); - } - - public void onThumbnailReceived(Bitmap thumbnail) { - if (DEBUG) Log.d(TAG, "onThumbnailReceived() called with: thumbnail = [" + thumbnail + "]"); - } - public void destroyPlayer() { if (DEBUG) Log.d(TAG, "destroyPlayer() called"); if (simpleExoPlayer != null) { @@ -292,7 +273,46 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen trackSelector = null; simpleExoPlayer = null; - recordManager = null; + } + + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail Loading + //////////////////////////////////////////////////////////////////////////*/ + + public void initThumbnail(final String url) { + if (DEBUG) Log.d(TAG, "Thumbnail - initThumbnail() called"); + if (url == null || url.isEmpty()) return; + ImageLoader.getInstance().resume(); + ImageLoader.getInstance().loadImage(url, this); + } + + @Override + public void onLoadingStarted(String imageUri, View view) { + if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingStarted() called on: " + + "imageUri = [" + imageUri + "], view = [" + view + "]"); + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", + failReason.getCause()); + } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + + "imageUri = [" + imageUri + "], view = [" + view + "], " + + "loadedImage = [" + loadedImage + "]"); + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + + "imageUri = [" + imageUri + "], view = [" + view + "]"); + } + + protected void clearThumbnailCache() { + ImageLoader.getInstance().clearMemoryCache(); } /*////////////////////////////////////////////////////////////////////////// @@ -371,9 +391,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } public void unregisterBroadcastReceiver() { - if (broadcastReceiver != null && context != null) { + try { context.unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; + } catch (final IllegalArgumentException unregisteredException) { + Log.e(TAG, "Broadcast receiver already unregistered.", unregisteredException); } } @@ -423,6 +444,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void onPlaying() { if (DEBUG) Log.d(TAG, "onPlaying() called"); if (!isProgressLoopRunning()) startProgressLoop(); + if (!isCurrentWindowValid()) seekToDefault(); } public void onBuffering() { @@ -480,64 +502,95 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } } + /*////////////////////////////////////////////////////////////////////////// + // Progress Updates + //////////////////////////////////////////////////////////////////////////*/ + + public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); + + protected void startProgressLoop() { + if (progressUpdateReactor != null) progressUpdateReactor.dispose(); + progressUpdateReactor = getProgressReactor(); + } + + protected void stopProgressLoop() { + if (progressUpdateReactor != null) progressUpdateReactor.dispose(); + progressUpdateReactor = null; + } + + public void triggerProgressUpdate() { + onUpdateProgress( + (int) simpleExoPlayer.getCurrentPosition(), + (int) simpleExoPlayer.getDuration(), + simpleExoPlayer.getBufferedPercentage() + ); + } + + + private Disposable getProgressReactor() { + return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .filter(ignored -> isProgressLoopRunning()) + .subscribe(ignored -> triggerProgressUpdate()); + } + /*////////////////////////////////////////////////////////////////////////// // ExoPlayer Listener //////////////////////////////////////////////////////////////////////////*/ - private void maybeRecover() { - final int currentSourceIndex = playQueue.getIndex(); - final PlayQueueItem currentSourceItem = playQueue.getItem(); + @Override + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason final int reason) { + if (DEBUG) Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " + + (manifest == null ? "no manifest" : "available manifest") + ", " + + "timeline size = [" + timeline.getWindowCount() + "], " + + "reason = [" + reason + "]"); - // Check if already playing correct window - final boolean isCurrentPeriodCorrect = - simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; - - // Check if recovering - if (isCurrentPeriodCorrect && currentSourceItem != null) { - /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, - * rounding this position to the nearest second will help alleviate this.*/ - final long position = currentSourceItem.getRecoveryPosition(); - - /* Skip recovering if the recovery position is not set.*/ - if (position == PlayQueueItem.RECOVERY_UNSET) return; - - if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + - " at: " + getTimeString((int)position)); - simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition()); - playQueue.unsetRecovery(currentSourceIndex); + switch (reason) { + case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block + case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock + case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes + if (playQueue != null && playbackManager != null && + // ensures MediaSourceManager#update is complete + timeline.getWindowCount() == playQueue.size()) { + playbackManager.load(); + } } } - @Override - public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { - if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); - } - @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - if (DEBUG) Log.d(TAG, "onTracksChanged(), track group size = " + trackGroups.length); + if (DEBUG) Log.d(TAG, "ExoPlayer - onTracksChanged(), " + + "track group size = " + trackGroups.length); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - if (DEBUG) Log.d(TAG, "playbackParameters(), speed: " + playbackParameters.speed + - ", pitch: " + playbackParameters.pitch); + if (DEBUG) Log.d(TAG, "ExoPlayer - playbackParameters(), " + + "speed: " + playbackParameters.speed + ", " + + "pitch: " + playbackParameters.pitch); } @Override - public void onLoadingChanged(boolean isLoading) { - if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]"); + public void onLoadingChanged(final boolean isLoading) { + if (DEBUG) Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " + + "isLoading = [" + isLoading + "]"); - if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) stopProgressLoop(); - else if (isLoading && !isProgressLoopRunning()) startProgressLoop(); + if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) { + stopProgressLoop(); + } else if (isLoading && !isProgressLoopRunning()) { + startProgressLoop(); + } } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (DEBUG) - Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]"); + if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " + + "playWhenReady = [" + playWhenReady + "], " + + "playbackState = [" + playbackState + "]"); + if (getCurrentState() == STATE_PAUSED_SEEK) { - if (DEBUG) Log.d(TAG, "onPlayerStateChanged() is currently blocked"); + if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); return; } @@ -572,24 +625,35 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } } + private void maybeRecover() { + final int currentSourceIndex = playQueue.getIndex(); + final PlayQueueItem currentSourceItem = playQueue.getItem(); + + // Check if already playing correct window + final boolean isCurrentPeriodCorrect = + simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; + + // Check if recovering + if (isCurrentPeriodCorrect && currentSourceItem != null) { + /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, + * rounding this position to the nearest second will help alleviate this.*/ + final long position = currentSourceItem.getRecoveryPosition(); + + /* Skip recovering if the recovery position is not set.*/ + if (position == PlayQueueItem.RECOVERY_UNSET) return; + + if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + + " at: " + getTimeString((int)position)); + simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition()); + playQueue.unsetRecovery(currentSourceIndex); + } + } + /** * Processes the exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. * There are multiple types of errors:

* * {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}:

- * If the current {@link com.google.android.exoplayer2.Timeline.Window window} is valid, - * then we know the error is produced by transitioning into a bad window, therefore we report - * an error to the play queue based on if the current error can be skipped. - * - * This is done because ExoPlayer reports the source exceptions before window is - * transitioned on seamless playback. Because player error causes ExoPlayer to go - * back to {@link Player#STATE_IDLE STATE_IDLE}, we reset and prepare the media source - * again to resume playback. - * - * In the event that this error is produced during a valid stream playback, we save the - * current position so the playback may be recovered and resumed manually by the user. This - * happens only if the playback is {@link #RECOVERY_SKIP_THRESHOLD} milliseconds until complete. - *

* * {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}:

* If a runtime error occurred, then we can try to recover it by restarting the playback @@ -598,11 +662,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen * {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}:

* If the renderer failed, treat the error as unrecoverable. * + * @see #processSourceError(IOException) * @see Player.EventListener#onPlayerError(ExoPlaybackException) * */ @Override public void onPlayerError(ExoPlaybackException error) { - if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]"); + if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + + "error = [" + error + "]"); if (errorToast != null) { errorToast.cancel(); errorToast = null; @@ -612,11 +678,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen switch (error.type) { case ExoPlaybackException.TYPE_SOURCE: - if (simpleExoPlayer.getCurrentPosition() < - simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { - setRecovery(); - } - playQueue.error(isCurrentWindowValid()); + processSourceError(error.getSourceException()); showStreamError(error); break; case ExoPlaybackException.TYPE_UNEXPECTED: @@ -631,9 +693,48 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } } + /** + * Processes {@link ExoPlaybackException} tagged with {@link ExoPlaybackException#TYPE_SOURCE}. + *

+ * If the current {@link com.google.android.exoplayer2.Timeline.Window window} is valid, + * then we know the error is produced by transitioning into a bad window, therefore we report + * an error to the play queue based on if the current error can be skipped. + *

+ * This is done because ExoPlayer reports the source exceptions before window is + * transitioned on seamless playback. Because player error causes ExoPlayer to go + * back to {@link Player#STATE_IDLE STATE_IDLE}, we reset and prepare the media source + * again to resume playback. + *

+ * In the event that this error is produced during a valid stream playback, we save the + * current position so the playback may be recovered and resumed manually by the user. This + * happens only if the playback is {@link #RECOVERY_SKIP_THRESHOLD} milliseconds until complete. + *

+ * In the event of livestreaming being lagged behind for any reason, most notably pausing for + * too long, a {@link BehindLiveWindowException} will be produced. This will trigger a reload + * instead of skipping or removal. + * */ + private void processSourceError(final IOException error) { + if (simpleExoPlayer == null || playQueue == null) return; + + if (simpleExoPlayer.getCurrentPosition() < + simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { + setRecovery(); + } + + final Throwable cause = error.getCause(); + if (cause instanceof BehindLiveWindowException) { + reload(); + } else if (cause instanceof UnknownHostException) { + playQueue.error(/*isNetworkProblem=*/true); + } else { + playQueue.error(isCurrentWindowValid()); + } + } + @Override - public void onPositionDiscontinuity(int reason) { - if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with reason = [" + reason + "]"); + public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { + if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + + "reason = [" + reason + "]"); // Refresh the playback if there is a transition to the next video final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); @@ -645,30 +746,28 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } else { playQueue.offsetIndex(+1); } - playbackManager.load(); - break; case DISCONTINUITY_REASON_SEEK: case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_INTERNAL: - default: break; } } @Override - public void onRepeatModeChanged(int i) { - if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]"); + public void onRepeatModeChanged(@Player.RepeatMode final int reason) { + if (DEBUG) Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " + + "mode = [" + reason + "]"); } @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - if (DEBUG) Log.d(TAG, "onShuffleModeEnabledChanged() called with: " + + public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { + if (DEBUG) Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " + "mode = [" + shuffleModeEnabled + "]"); } @Override public void onSeekProcessed() { - if (DEBUG) Log.d(TAG, "onSeekProcessed() called"); + if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); } /*////////////////////////////////////////////////////////////////////////// // Playback Listener @@ -677,7 +776,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen @Override public void block() { if (simpleExoPlayer == null) return; - if (DEBUG) Log.d(TAG, "Blocking..."); + if (DEBUG) Log.d(TAG, "Playback - block() called"); currentItem = null; currentInfo = null; @@ -690,12 +789,12 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen @Override public void unblock(final MediaSource mediaSource) { if (simpleExoPlayer == null) return; - if (DEBUG) Log.d(TAG, "Unblocking..."); + if (DEBUG) Log.d(TAG, "Playback - unblock() called"); if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); simpleExoPlayer.prepare(mediaSource); - simpleExoPlayer.seekToDefaultPosition(); + seekToDefault(); } @Override @@ -705,7 +804,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen currentItem = item; currentInfo = info; - if (DEBUG) Log.d(TAG, "Syncing..."); + if (DEBUG) Log.d(TAG, "Playback - sync() called with " + + (info == null ? "available" : "null") + " info, " + + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); if (simpleExoPlayer == null) return; // Check if on wrong window @@ -781,8 +882,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); } - public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); - public void onVideoPlayPause() { if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); @@ -794,7 +893,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (getCurrentState() == STATE_COMPLETED) { if (playQueue.getIndex() == 0) { - simpleExoPlayer.seekToDefaultPosition(); + seekToDefault(); } else { playQueue.setIndex(0); } @@ -839,11 +938,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } public void onSelected(final PlayQueueItem item) { + if (playQueue == null || simpleExoPlayer == null) return; + final int index = playQueue.indexOf(item); if (index == -1) return; - if (playQueue.getIndex() == index) { - simpleExoPlayer.seekToDefaultPosition(); + if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { + seekToDefault(); } else { playQueue.setIndex(index); } @@ -875,7 +976,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen //////////////////////////////////////////////////////////////////////////*/ private void registerView() { - if (databaseUpdateReactor == null || recordManager == null || currentInfo == null) return; + if (databaseUpdateReactor == null || currentInfo == null) return; databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() .subscribe( ignored -> {/* successful */}, @@ -890,30 +991,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } } - protected void clearThumbnailCache() { - ImageLoader.getInstance().clearMemoryCache(); - } - - protected void startProgressLoop() { - if (progressUpdateReactor != null) progressUpdateReactor.dispose(); - progressUpdateReactor = getProgressReactor(); - } - - protected void stopProgressLoop() { - if (progressUpdateReactor != null) progressUpdateReactor.dispose(); - progressUpdateReactor = null; - } - - public void triggerProgressUpdate() { - onUpdateProgress( - (int) simpleExoPlayer.getCurrentPosition(), - (int) simpleExoPlayer.getDuration(), - simpleExoPlayer.getBufferedPercentage() - ); - } - protected void savePlaybackState(final StreamInfo info, final long progress) { - if (context == null || info == null || databaseUpdateReactor == null) return; + if (info == null || databaseUpdateReactor == null) return; final Disposable stateSaver = recordManager.saveStreamState(info, progress) .observeOn(AndroidSchedulers.mainThread()) .onErrorComplete() 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 f4e7a0d6a..6263541bb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -419,13 +419,15 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onThumbnailReceived(Bitmap thumbnail) { - super.onThumbnailReceived(thumbnail); - if (thumbnail != null) { + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + super.onLoadingComplete(imageUri, view, loadedImage); + if (loadedImage != null) { // rebuild notification here since remote view does not release bitmaps, causing memory leaks notBuilder = createNotification(); - if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); + if (notRemoteView != null) { + notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); + } updateNotification(-1); } 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 5a7a9a462..58de44130 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -160,7 +160,6 @@ public abstract class VideoPlayer extends BasePlayer public VideoPlayer(String debugTag, Context context) { super(context); this.TAG = debugTag; - this.context = context; } public void setup(View rootView) { @@ -617,9 +616,9 @@ public abstract class VideoPlayer extends BasePlayer } @Override - public void onThumbnailReceived(Bitmap thumbnail) { - super.onThumbnailReceived(thumbnail); - if (thumbnail != null) endScreen.setImageBitmap(thumbnail); + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + super.onLoadingComplete(imageUri, view, loadedImage); + if (loadedImage != null) endScreen.setImageBitmap(loadedImage); } protected void onFullScreenButtonClicked() { 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 439885e58..bc7f92b42 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 @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -33,6 +34,7 @@ import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; import io.reactivex.functions.Consumer; +import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; @@ -40,66 +42,105 @@ import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; public class MediaSourceManager { @NonNull private final static String TAG = "MediaSourceManager"; - // WINDOW_SIZE determines how many streams AFTER the current stream should be loaded. - // The default value (1) ensures seamless playback under typical network settings. + /** + * Determines how many streams before and after the current stream should be loaded. + * The default value (1) ensures seamless playback under typical network settings. + *

+ * The streams after the current will be loaded into the playlist timeline while the + * streams before will only be cached for future usage. + * + * @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource) + * @see #update(int, MediaSource) + * */ private final static int WINDOW_SIZE = 1; @NonNull private final PlaybackListener playbackListener; @NonNull private final PlayQueue playQueue; - // Once a MediaSource item has been detected to be expired, the manager will immediately - // trigger a reload on the associated PlayQueueItem, which may disrupt playback, - // if the item is being played - private final long expirationTimeMillis; + /** + * Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing + * {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure + * the {@link StreamInfo} used in subsequent playback is up-to-date. + *

+ * Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to + * replace the expired one on whereupon {@link #loadImmediate()} is called. + * + * @see #loadImmediate() + * @see #isCorrectionNeeded(PlayQueueItem) + * */ + private final long windowRefreshTimeMillis; - // Process only the last load order when receiving a stream of load orders (lessens I/O) - // The higher it is, the less loading occurs during rapid noncritical timeline changes - // Not recommended to go below 100ms + /** + * Process only the last load order when receiving a stream of load orders (lessens I/O). + *

+ * The higher it is, the less loading occurs during rapid noncritical timeline changes. + *

+ * Not recommended to go below 100ms. + * + * @see #loadDebounced() + * */ private final long loadDebounceMillis; @NonNull private final Disposable debouncedLoader; @NonNull private final PublishSubject debouncedSignal; - private DynamicConcatenatingMediaSource sources; + @NonNull private Subscription playQueueReactor; - private Subscription playQueueReactor; - private CompositeDisposable loaderReactor; + /** + * Determines the maximum number of disposables allowed in the {@link #loaderReactor}. + * Once exceeded, new calls to {@link #loadImmediate()} will evict all disposables in the + * {@link #loaderReactor} in order to load a new set of items. + * + * @see #loadImmediate() + * @see #maybeLoadItem(PlayQueueItem) + * */ + private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; + @NonNull private final CompositeDisposable loaderReactor; + @NonNull private Set loadingItems; + @NonNull private final SerialDisposable syncReactor; - private boolean isBlocked; + @NonNull private final AtomicBoolean isBlocked; - private SerialDisposable syncReactor; - private PlayQueueItem syncedItem; - private Set loadingItems; + @NonNull private DynamicConcatenatingMediaSource sources; + + @Nullable private PlayQueueItem syncedItem; public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { this(listener, playQueue, /*loadDebounceMillis=*/400L, - /*expirationTimeMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES)); + /*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)); } private MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue, final long loadDebounceMillis, - final long expirationTimeMillis) { + final long windowRefreshTimeMillis) { + if (playQueue.getBroadcastReceiver() == null) { + throw new IllegalArgumentException("Play Queue has not been initialized."); + } + this.playbackListener = listener; this.playQueue = playQueue; - this.loadDebounceMillis = loadDebounceMillis; - this.expirationTimeMillis = expirationTimeMillis; - this.loaderReactor = new CompositeDisposable(); + this.windowRefreshTimeMillis = windowRefreshTimeMillis; + + this.loadDebounceMillis = loadDebounceMillis; this.debouncedSignal = PublishSubject.create(); this.debouncedLoader = getDebouncedLoader(); + this.playQueueReactor = EmptySubscription.INSTANCE; + this.loaderReactor = new CompositeDisposable(); + this.syncReactor = new SerialDisposable(); + + this.isBlocked = new AtomicBoolean(false); + this.sources = new DynamicConcatenatingMediaSource(); - this.syncReactor = new SerialDisposable(); this.loadingItems = Collections.synchronizedSet(new HashSet<>()); - if (playQueue.getBroadcastReceiver() != null) { - playQueue.getBroadcastReceiver() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getReactor()); - } + playQueue.getBroadcastReceiver() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getReactor()); } /*////////////////////////////////////////////////////////////////////////// @@ -114,16 +155,12 @@ public class MediaSourceManager { debouncedSignal.onComplete(); debouncedLoader.dispose(); - if (playQueueReactor != null) playQueueReactor.cancel(); - if (loaderReactor != null) loaderReactor.dispose(); - if (syncReactor != null) syncReactor.dispose(); - if (sources != null) sources.releaseSource(); + playQueueReactor.cancel(); + loaderReactor.dispose(); + syncReactor.dispose(); + sources.releaseSource(); - playQueueReactor = null; - loaderReactor = null; - syncReactor = null; syncedItem = null; - sources = null; } /** @@ -158,14 +195,14 @@ public class MediaSourceManager { return new Subscriber() { @Override public void onSubscribe(@NonNull Subscription d) { - if (playQueueReactor != null) playQueueReactor.cancel(); + playQueueReactor.cancel(); playQueueReactor = d; playQueueReactor.request(1); } @Override public void onNext(@NonNull PlayQueueEvent playQueueMessage) { - if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage); + onPlayQueueChanged(playQueueMessage); } @Override @@ -227,7 +264,7 @@ public class MediaSourceManager { tryBlock(); playQueue.fetch(); } - if (playQueueReactor != null) playQueueReactor.request(1); + playQueueReactor.request(1); } /*////////////////////////////////////////////////////////////////////////// @@ -240,7 +277,7 @@ public class MediaSourceManager { } private boolean isPlaybackReady() { - if (sources == null || sources.getSize() != playQueue.size()) return false; + if (sources.getSize() != playQueue.size()) return false; final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); final PlayQueueItem playQueueItem = playQueue.getItem(); @@ -256,19 +293,19 @@ public class MediaSourceManager { private void tryBlock() { if (DEBUG) Log.d(TAG, "tryBlock() called."); - if (isBlocked) return; + if (isBlocked.get()) return; playbackListener.block(); resetSources(); - isBlocked = true; + isBlocked.set(true); } private void tryUnblock() { if (DEBUG) Log.d(TAG, "tryUnblock() called."); - if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) { - isBlocked = false; + if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) { + isBlocked.set(false); playbackListener.unblock(sources); } } @@ -281,7 +318,7 @@ public class MediaSourceManager { if (DEBUG) Log.d(TAG, "sync() called."); final PlayQueueItem currentItem = playQueue.getItem(); - if (isBlocked || currentItem == null) return; + if (isBlocked.get() || currentItem == null) return; final Consumer onSuccess = info -> syncInternal(currentItem, info); final Consumer onError = throwable -> syncInternal(currentItem, null); @@ -295,11 +332,11 @@ public class MediaSourceManager { } } - private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item, + private void syncInternal(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { // Ensure the current item is up to date with the play queue if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { - playbackListener.sync(syncedItem, info); + playbackListener.sync(item, info); } } @@ -323,6 +360,12 @@ public class MediaSourceManager { final int currentIndex = playQueue.getIndex(); final PlayQueueItem currentItem = playQueue.getItem(currentIndex); if (currentItem == null) return; + + // Evict the items being loaded to free up memory + if (!loadingItems.contains(currentItem) && loaderReactor.size() > MAXIMUM_LOADER_SIZE) { + loaderReactor.clear(); + loadingItems.clear(); + } maybeLoadItem(currentItem); // The rest are just for seamless playback @@ -347,34 +390,17 @@ public class MediaSourceManager { private void maybeLoadItem(@NonNull final PlayQueueItem item) { if (DEBUG) Log.d(TAG, "maybeLoadItem() called."); - if (sources == null) return; - - final int index = playQueue.indexOf(item); - if (index > sources.getSize() - 1) return; - - final Consumer onDone = mediaSource -> { - if (DEBUG) Log.d(TAG, " Loaded: [" + item.getTitle() + - "] with url: " + item.getUrl()); - - final int itemIndex = playQueue.indexOf(item); - // Only update the playlist timeline for items at the current index or after. - if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) { - update(itemIndex, mediaSource); - } - - loadingItems.remove(item); - tryUnblock(); - sync(); - }; + if (playQueue.indexOf(item) >= sources.getSize()) return; if (!loadingItems.contains(item) && isCorrectionNeeded(item)) { - if (DEBUG) Log.d(TAG, "Loading: [" + item.getTitle() + + if (DEBUG) Log.d(TAG, "MediaSource - Loading: [" + item.getTitle() + "] with url: " + item.getUrl()); loadingItems.add(item); final Disposable loader = getLoadedMediaSource(item) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onDone); + /* No exception handling since getLoadedMediaSource guarantees nonnull return */ + .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); loaderReactor.add(loader); } @@ -392,14 +418,32 @@ public class MediaSourceManager { ", audio count: " + streamInfo.audio_streams.size() + ", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size()); - return new FailedMediaSource(stream, new IllegalStateException(exception)); + return new FailedMediaSource(stream, exception); } - final long expiration = System.currentTimeMillis() + expirationTimeMillis; + final long expiration = System.currentTimeMillis() + windowRefreshTimeMillis; return new LoadedMediaSource(source, stream, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); } + private void onMediaSourceReceived(@NonNull final PlayQueueItem item, + @NonNull final ManagedMediaSource mediaSource) { + if (DEBUG) Log.d(TAG, "MediaSource - Loaded: [" + item.getTitle() + + "] with url: " + item.getUrl()); + + final int itemIndex = playQueue.indexOf(item); + // Only update the playlist timeline for items at the current index or after. + if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) { + if (DEBUG) Log.d(TAG, "MediaSource - Updating: [" + item.getTitle() + + "] with url: " + item.getUrl()); + update(itemIndex, mediaSource); + } + + loadingItems.remove(item); + tryUnblock(); + sync(); + } + /** * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource} * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback @@ -411,8 +455,6 @@ public class MediaSourceManager { * {@link ManagedMediaSource}. * */ private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { - if (sources == null) return false; - final int index = playQueue.indexOf(item); if (index == -1 || index >= sources.getSize()) return false; @@ -432,13 +474,13 @@ public class MediaSourceManager { private void resetSources() { if (DEBUG) Log.d(TAG, "resetSources() called."); - if (this.sources != null) this.sources.releaseSource(); + this.sources.releaseSource(); this.sources = new DynamicConcatenatingMediaSource(); } private void populateSources() { if (DEBUG) Log.d(TAG, "populateSources() called."); - if (sources == null || sources.getSize() >= playQueue.size()) return; + if (sources.getSize() >= playQueue.size()) return; for (int index = sources.getSize() - 1; index < playQueue.size(); index++) { emplace(index, new PlaceholderMediaSource()); @@ -451,12 +493,11 @@ public class MediaSourceManager { /** * Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource} - * with position * in respect to the play queue only if no {@link MediaSource} + * with position in respect to the play queue only if no {@link MediaSource} * already exists at the given index. * */ - private void emplace(final int index, @NonNull final MediaSource source) { - if (sources == null) return; - if (index < 0 || index < sources.getSize()) return; + private synchronized void emplace(final int index, @NonNull final MediaSource source) { + if (index < sources.getSize()) return; sources.addMediaSource(index, source); } @@ -465,8 +506,7 @@ public class MediaSourceManager { * Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource} * at the given index. If this index is out of bound, then the removal is ignored. * */ - private void remove(final int index) { - if (sources == null) return; + private synchronized void remove(final int index) { if (index < 0 || index > sources.getSize()) return; sources.removeMediaSource(index); @@ -477,8 +517,7 @@ public class MediaSourceManager { * from the given source index to the target index. If either index is out of bound, * then the call is ignored. * */ - private void move(final int source, final int target) { - if (sources == null) return; + private synchronized void move(final int source, final int target) { if (source < 0 || target < 0) return; if (source >= sources.getSize() || target >= sources.getSize()) return; @@ -491,15 +530,13 @@ public class MediaSourceManager { * then the replacement is ignored. *

* Not recommended to use on indices LESS THAN the currently playing index, since - * this will modify the playback timeline prior to the index and cause desynchronization + * this will modify the playback timeline prior to the index and may cause desynchronization * on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}. * */ private synchronized void update(final int index, @NonNull final MediaSource source) { - if (sources == null) return; if (index < 0 || index >= sources.getSize()) return; - sources.addMediaSource(index + 1, source, () -> { - if (sources != null) sources.removeMediaSource(index); - }); + sources.addMediaSource(index + 1, source, () -> + sources.removeMediaSource(index)); } } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java index 7c701a637..dd320c2bc 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java @@ -63,22 +63,18 @@ public class PlayQueueAdapter extends RecyclerView.Adapter observer = new Observer() { + private Observer getReactor() { + return new Observer() { @Override public void onSubscribe(@NonNull Disposable d) { if (playQueueReactor != null) playQueueReactor.dispose(); @@ -99,9 +95,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter loadFromNetwork) { checkServiceId(serviceId); - loadFromNetwork = loadFromNetwork.doOnSuccess((@NonNull I i) -> cache.putInfo(i)); + loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info)); Single load; if (forceLoad) { diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java index 0f082cc11..47c45e82a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java @@ -20,6 +20,7 @@ package org.schabi.newpipe.util; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.util.LruCache; import android.util.Log; @@ -29,6 +30,8 @@ import org.schabi.newpipe.extractor.Info; import java.util.Map; import java.util.concurrent.TimeUnit; +import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; + public final class InfoCache { private static final boolean DEBUG = MainActivity.DEBUG; @@ -52,6 +55,7 @@ public final class InfoCache { return instance; } + @Nullable public Info getFromKey(int serviceId, @NonNull String url) { if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); synchronized (lruCache) { @@ -59,18 +63,19 @@ public final class InfoCache { } } - public void putInfo(@NonNull Info info) { + public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) { if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); - synchronized (lruCache) { - final CacheData data = new CacheData(info, DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS); - lruCache.put(keyOf(info), data); - } - } - public void removeInfo(@NonNull Info info) { - if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]"); + final long expirationMillis; + if (info.getServiceId() == SoundCloud.getServiceId()) { + expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES); + } else { + expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS); + } + synchronized (lruCache) { - lruCache.remove(keyOf(info)); + final CacheData data = new CacheData(info, expirationMillis); + lruCache.put(keyOf(serviceId, url), data); } } @@ -102,10 +107,7 @@ public final class InfoCache { } } - private static String keyOf(@NonNull final Info info) { - return keyOf(info.getServiceId(), info.getUrl()); - } - + @NonNull private static String keyOf(final int serviceId, @NonNull final String url) { return serviceId + url; } @@ -119,6 +121,7 @@ public final class InfoCache { } } + @Nullable private static Info getInfo(@NonNull final LruCache cache, @NonNull final String key) { final CacheData data = cache.get(key); @@ -136,12 +139,8 @@ public final class InfoCache { final private long expireTimestamp; final private Info info; - private CacheData(@NonNull final Info info, - final long timeout, - @NonNull final TimeUnit timeUnit) { - this.expireTimestamp = System.currentTimeMillis() + - TimeUnit.MILLISECONDS.convert(timeout, timeUnit); - + private CacheData(@NonNull final Info info, final long timeoutMillis) { + this.expireTimestamp = System.currentTimeMillis() + timeoutMillis; this.info = info; } From a88e19a8edfdf02d14736571f5ec785c72d0f3b9 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 3 Mar 2018 14:24:21 -0800 Subject: [PATCH 13/19] -Added toggle to enable fast inexact seek in players. -Improved player sync calls to recognize different metadata updates. -Changed MediaSourceManager to synchronize only after timeline changes and reenabled multiple sync calls to player. -Renamed listener and synchronization methods related to MediaSourceManager. --- .../newpipe/player/BackgroundPlayer.java | 13 ++-- .../org/schabi/newpipe/player/BasePlayer.java | 58 ++++++++++------ .../newpipe/player/MainVideoPlayer.java | 39 +++++------ .../newpipe/player/PopupVideoPlayer.java | 16 +++-- .../schabi/newpipe/player/VideoPlayer.java | 7 +- .../newpipe/player/helper/PlayerHelper.java | 11 +++ .../player/playback/MediaSourceManager.java | 68 +++++++++---------- .../player/playback/PlaybackListener.java | 12 ++-- app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/video_audio_settings.xml | 5 ++ 11 files changed, 132 insertions(+), 100 deletions(-) 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 f002115f8..f688b3aa8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -380,11 +380,10 @@ public final class BackgroundPlayer extends Service { // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - @Override - public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { - if (currentItem == item && currentInfo == info) return; - super.sync(item, info); - + protected void onMetadataChanged(@NonNull final PlayQueueItem item, + @Nullable final StreamInfo info, + final int newPlayQueueIndex, + final boolean hasPlayQueueItemChanged) { resetNotification(); updateNotification(-1); updateMetadata(); @@ -405,8 +404,8 @@ public final class BackgroundPlayer extends Service { } @Override - public void shutdown() { - super.shutdown(); + public void onPlaybackShutdown() { + super.onPlaybackShutdown(); onClose(); } 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 86a4d1234..bf661c897 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -61,6 +61,7 @@ import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.PlayerDataSource; +import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.PlaybackListener; @@ -196,6 +197,7 @@ public abstract class BasePlayer implements simpleExoPlayer.addListener(this); simpleExoPlayer.setPlayWhenReady(true); + simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); } public void initListeners() {} @@ -688,7 +690,7 @@ public abstract class BasePlayer implements break; default: showUnrecoverableError(error); - shutdown(); + onPlaybackShutdown(); break; } } @@ -774,9 +776,9 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ @Override - public void block() { + public void onPlaybackBlock() { if (simpleExoPlayer == null) return; - if (DEBUG) Log.d(TAG, "Playback - block() called"); + if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called"); currentItem = null; currentInfo = null; @@ -787,9 +789,9 @@ public abstract class BasePlayer implements } @Override - public void unblock(final MediaSource mediaSource) { + public void onPlaybackUnblock(final MediaSource mediaSource) { if (simpleExoPlayer == null) return; - if (DEBUG) Log.d(TAG, "Playback - unblock() called"); + if (DEBUG) Log.d(TAG, "Playback - onPlaybackUnblock() called"); if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); @@ -798,34 +800,50 @@ public abstract class BasePlayer implements } @Override - public void sync(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info) { - if (currentItem == item && currentInfo == info) return; - currentItem = item; - currentInfo = info; - - if (DEBUG) Log.d(TAG, "Playback - sync() called with " + + public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, + @Nullable final StreamInfo info) { + if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + (info == null ? "available" : "null") + " info, " + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); - if (simpleExoPlayer == null) return; + + final boolean hasPlayQueueItemChanged = currentItem != item; + final boolean hasStreamInfoChanged = currentInfo != info; + if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { + return; // Nothing to synchronize + } + + currentItem = item; + currentInfo = info; + if (hasPlayQueueItemChanged) { + // updates only to the stream info should not trigger another view count + registerView(); + initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); + } + + final int currentSourceIndex = playQueue.indexOf(item); + onMetadataChanged(item, info, currentSourceIndex, hasPlayQueueItemChanged); // Check if on wrong window - final int currentSourceIndex = playQueue.indexOf(item); + if (simpleExoPlayer == null) return; if (currentSourceIndex != playQueue.getIndex()) { Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex + "], queue index=[" + playQueue.getIndex() + "]"); - } else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) { + + // on metadata changed + } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) { final long startPos = info != null ? info.start_position : 0; if (DEBUG) Log.d(TAG, "Rewinding to correct window=[" + currentSourceIndex + "]," + " at=[" + getTimeString((int)startPos) + "]," + - " from=[" + simpleExoPlayer.getCurrentPeriodIndex() + "]."); + " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); simpleExoPlayer.seekTo(currentSourceIndex, startPos); } - - registerView(); - initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } + abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item, + @Nullable final StreamInfo info, + final int newPlayQueueIndex, + final boolean hasPlayQueueItemChanged); + @Nullable @Override public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { @@ -839,7 +857,7 @@ public abstract class BasePlayer implements } @Override - public void shutdown() { + public void onPlaybackShutdown() { if (DEBUG) Log.d(TAG, "Shutting down..."); destroy(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index d48994d0f..04d93bbdc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -391,31 +391,32 @@ public final class MainVideoPlayer extends Activity { updatePlaybackButtons(); } - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void shutdown() { - super.shutdown(); - finish(); - } - - @Override - public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { - super.sync(item, info); - titleTextView.setText(getVideoTitle()); - channelTextView.setText(getUploaderName()); - - //playPauseButton.setImageResource(R.drawable.ic_pause_white); - } - @Override public void onShuffleClicked() { super.onShuffleClicked(); updatePlaybackButtons(); } + /*////////////////////////////////////////////////////////////////////////// + // Playback Listener + //////////////////////////////////////////////////////////////////////////*/ + + protected void onMetadataChanged(@NonNull final PlayQueueItem item, + @Nullable final StreamInfo info, + final int newPlayQueueIndex, + final boolean hasPlayQueueItemChanged) { + super.onMetadataChanged(item, info, newPlayQueueIndex, false); + + titleTextView.setText(getVideoTitle()); + channelTextView.setText(getUploaderName()); + } + + @Override + public void onPlaybackShutdown() { + super.onPlaybackShutdown(); + finish(); + } + /*////////////////////////////////////////////////////////////////////////// // Player Overrides //////////////////////////////////////////////////////////////////////////*/ 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 6263541bb..80063e547 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -535,7 +535,8 @@ public final class PopupVideoPlayer extends Service { private void updatePlayback() { if (activityListener != null && simpleExoPlayer != null && playQueue != null) { - activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); + activityListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); } } @@ -574,16 +575,17 @@ public final class PopupVideoPlayer extends Service { // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - @Override - public void sync(@NonNull PlayQueueItem item, @Nullable StreamInfo info) { - if (currentItem == item && currentInfo == info) return; - super.sync(item, info); + protected void onMetadataChanged(@NonNull final PlayQueueItem item, + @Nullable final StreamInfo info, + final int newPlayQueueIndex, + final boolean hasPlayQueueItemChanged) { + super.onMetadataChanged(item, info, newPlayQueueIndex, false); updateMetadata(); } @Override - public void shutdown() { - super.shutdown(); + public void onPlaybackShutdown() { + super.onPlaybackShutdown(); onClose(); } 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 58de44130..5b4a1f80b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -324,9 +324,10 @@ public abstract class VideoPlayer extends BasePlayer protected abstract int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality); - @Override - public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { - super.sync(item, info); + protected void onMetadataChanged(@NonNull final PlayQueueItem item, + @Nullable final StreamInfo info, + final int newPlayQueueIndex, + final boolean hasPlayQueueItemChanged) { qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 813c69c22..553163d21 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -5,6 +5,7 @@ import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.util.MimeTypes; @@ -109,6 +110,12 @@ public class PlayerHelper { return isRememberingPopupDimensions(context, true); } + @NonNull + public static SeekParameters getSeekParameters(@NonNull final Context context) { + return isUsingInexactSeek(context, false) ? + SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT; + } + public static long getPreferredCacheSize(@NonNull final Context context) { return 64 * 1024 * 1024L; } @@ -176,4 +183,8 @@ public class PlayerHelper { private static boolean isRememberingPopupDimensions(@Nonnull final Context context, final boolean b) { return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b); } + + private static boolean isUsingInexactSeek(@NonNull final Context context, final boolean b) { + return getPreferences(context).getBoolean(context.getString(R.string.use_inexact_seek_key), b); + } } 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 bc7f92b42..3f7a8522c 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 @@ -50,7 +50,7 @@ public class MediaSourceManager { * streams before will only be cached for future usage. * * @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource) - * @see #update(int, MediaSource) + * @see #update(int, MediaSource, Runnable) * */ private final static int WINDOW_SIZE = 1; @@ -95,15 +95,13 @@ public class MediaSourceManager { * */ private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; @NonNull private final CompositeDisposable loaderReactor; - @NonNull private Set loadingItems; + @NonNull private final Set loadingItems; @NonNull private final SerialDisposable syncReactor; @NonNull private final AtomicBoolean isBlocked; @NonNull private DynamicConcatenatingMediaSource sources; - @Nullable private PlayQueueItem syncedItem; - public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { this(listener, playQueue, @@ -159,8 +157,6 @@ public class MediaSourceManager { loaderReactor.dispose(); syncReactor.dispose(); sources.releaseSource(); - - syncedItem = null; } /** @@ -182,9 +178,7 @@ public class MediaSourceManager { public void reset() { if (DEBUG) Log.d(TAG, "reset() called."); - tryBlock(); - - syncedItem = null; + maybeBlock(); populateSources(); } /*////////////////////////////////////////////////////////////////////////// @@ -215,7 +209,7 @@ public class MediaSourceManager { private void onPlayQueueChanged(final PlayQueueEvent event) { if (playQueue.isEmpty() && playQueue.isComplete()) { - playbackListener.shutdown(); + playbackListener.onPlaybackShutdown(); return; } @@ -261,7 +255,7 @@ public class MediaSourceManager { } if (!isPlayQueueReady()) { - tryBlock(); + maybeBlock(); playQueue.fetch(); } playQueueReactor.request(1); @@ -290,23 +284,23 @@ public class MediaSourceManager { return false; } - private void tryBlock() { - if (DEBUG) Log.d(TAG, "tryBlock() called."); + private void maybeBlock() { + if (DEBUG) Log.d(TAG, "maybeBlock() called."); if (isBlocked.get()) return; - playbackListener.block(); + playbackListener.onPlaybackBlock(); resetSources(); isBlocked.set(true); } - private void tryUnblock() { - if (DEBUG) Log.d(TAG, "tryUnblock() called."); + private void maybeUnblock() { + if (DEBUG) Log.d(TAG, "maybeUnblock() called."); if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) { isBlocked.set(false); - playbackListener.unblock(sources); + playbackListener.onPlaybackUnblock(sources); } } @@ -314,8 +308,8 @@ public class MediaSourceManager { // Metadata Synchronization TODO: maybe this should be a separate manager //////////////////////////////////////////////////////////////////////////*/ - private void sync() { - if (DEBUG) Log.d(TAG, "sync() called."); + private void maybeSync() { + if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); final PlayQueueItem currentItem = playQueue.getItem(); if (isBlocked.get() || currentItem == null) return; @@ -323,23 +317,25 @@ public class MediaSourceManager { final Consumer onSuccess = info -> syncInternal(currentItem, info); final Consumer onError = throwable -> syncInternal(currentItem, null); - if (syncedItem != currentItem) { - syncedItem = currentItem; - final Disposable sync = currentItem.getStream() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onSuccess, onError); - syncReactor.set(sync); - } + final Disposable sync = currentItem.getStream() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(onSuccess, onError); + syncReactor.set(sync); } private void syncInternal(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { // Ensure the current item is up to date with the play queue - if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { - playbackListener.sync(item, info); + if (playQueue.getItem() == item) { + playbackListener.onPlaybackSynchronize(item, info); } } + private void maybeSynchronizePlayer() { + maybeUnblock(); + maybeSync(); + } + /*////////////////////////////////////////////////////////////////////////// // MediaSource Loading //////////////////////////////////////////////////////////////////////////*/ @@ -404,8 +400,7 @@ public class MediaSourceManager { loaderReactor.add(loader); } - tryUnblock(); - sync(); + maybeSynchronizePlayer(); } private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { @@ -431,17 +426,15 @@ public class MediaSourceManager { if (DEBUG) Log.d(TAG, "MediaSource - Loaded: [" + item.getTitle() + "] with url: " + item.getUrl()); + loadingItems.remove(item); + final int itemIndex = playQueue.indexOf(item); // Only update the playlist timeline for items at the current index or after. if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) { if (DEBUG) Log.d(TAG, "MediaSource - Updating: [" + item.getTitle() + "] with url: " + item.getUrl()); - update(itemIndex, mediaSource); + update(itemIndex, mediaSource, this::maybeSynchronizePlayer); } - - loadingItems.remove(item); - tryUnblock(); - sync(); } /** @@ -533,10 +526,11 @@ public class MediaSourceManager { * this will modify the playback timeline prior to the index and may cause desynchronization * on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}. * */ - private synchronized void update(final int index, @NonNull final MediaSource source) { + private synchronized void update(final int index, @NonNull final MediaSource source, + @Nullable final Runnable finalizingAction) { if (index < 0 || index >= sources.getSize()) return; sources.addMediaSource(index + 1, source, () -> - sources.removeMediaSource(index)); + sources.removeMediaSource(index, finalizingAction)); } } 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 c6fdde656..b37a269e2 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 @@ -18,7 +18,7 @@ public interface PlaybackListener { * * May be called at any time. * */ - void block(); + void onPlaybackBlock(); /** * Called when the stream at the current queue index is ready. @@ -26,18 +26,16 @@ public interface PlaybackListener { * * May be called only when the player is blocked. * */ - void unblock(final MediaSource mediaSource); + void onPlaybackUnblock(final MediaSource mediaSource); /** * Called when the queue index is refreshed. * Signals to the listener to synchronize the player's window to the manager's * window. * - * Occurs once only per play queue item change. - * - * May be called only after unblock is called. + * May be called anytime at any amount once unblock is called. * */ - void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); + void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); /** * Requests the listener to resolve a stream info into a media source @@ -55,5 +53,5 @@ public interface PlaybackListener { * * May be called at any time. * */ - void shutdown(); + void onPlaybackShutdown(); } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index fc31ee02c..c207ed712 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -20,6 +20,7 @@ player_gesture_controls resume_on_audio_focus_gain popup_remember_size_pos_key + use_inexact_seek_key default_resolution 360p diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9dd5dcddb..6a8123bde 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -72,6 +72,8 @@ Black Remember popup size and position Remember last size and position of popup + Use fast inexact seek + Inexact seek allows the player to seek to positions faster with reduced precision Player gesture controls Use gestures to control the brightness and volume of the player Search suggestions diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index 7551834a2..e17b46ebc 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -100,5 +100,10 @@ android:summary="@string/popup_remember_size_pos_summary" android:title="@string/popup_remember_size_pos_title"/> + From 59558efed12f19cd99dd607d5d60901717b0d3b2 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 3 Mar 2018 20:58:06 -0800 Subject: [PATCH 14/19] -Added seamless shuffling. -Reenabled full window loading in MediaSourceManager. --- .../newpipe/player/BackgroundPlayer.java | 11 +++++--- .../org/schabi/newpipe/player/BasePlayer.java | 18 ++++++------- .../player/playback/MediaSourceManager.java | 26 ++++++++++++------- .../schabi/newpipe/playlist/PlayQueue.java | 6 +++-- .../newpipe/playlist/events/ReorderEvent.java | 14 +++++++++- 5 files changed, 50 insertions(+), 25 deletions(-) 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 f688b3aa8..a43f434ff 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -384,9 +384,11 @@ public final class BackgroundPlayer extends Service { @Nullable final StreamInfo info, final int newPlayQueueIndex, final boolean hasPlayQueueItemChanged) { - resetNotification(); - updateNotification(-1); - updateMetadata(); + if (shouldUpdateOnProgress || hasPlayQueueItemChanged) { + resetNotification(); + updateNotification(-1); + updateMetadata(); + } } @Override @@ -434,7 +436,8 @@ public final class BackgroundPlayer extends Service { private void updatePlayback() { if (activityListener != null && simpleExoPlayer != null && playQueue != null) { - activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), getPlaybackParameters()); + activityListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), getPlaybackParameters()); } } 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 bf661c897..f89bd2630 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -494,14 +494,8 @@ public abstract class BasePlayer implements public void onShuffleClicked() { if (DEBUG) Log.d(TAG, "onShuffleClicked() called"); - if (playQueue == null) return; - - setRecovery(); - if (playQueue.isShuffled()) { - playQueue.unshuffle(); - } else { - playQueue.shuffle(); - } + if (simpleExoPlayer == null) return; + simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); } /*////////////////////////////////////////////////////////////////////////// @@ -765,6 +759,12 @@ public abstract class BasePlayer implements public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { if (DEBUG) Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " + "mode = [" + shuffleModeEnabled + "]"); + if (playQueue == null) return; + if (shuffleModeEnabled) { + playQueue.shuffle(); + } else { + playQueue.unshuffle(); + } } @Override @@ -803,7 +803,7 @@ public abstract class BasePlayer implements public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + - (info == null ? "available" : "null") + " info, " + + (info != null ? "available" : "null") + " info, " + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); final boolean hasPlayQueueItemChanged = currentItem != item; 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 3f7a8522c..cb803dcd1 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 @@ -6,6 +6,7 @@ import android.util.Log; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ShuffleOrder; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -19,6 +20,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.events.MoveEvent; import org.schabi.newpipe.playlist.events.PlayQueueEvent; import org.schabi.newpipe.playlist.events.RemoveEvent; +import org.schabi.newpipe.playlist.events.ReorderEvent; import java.util.ArrayList; import java.util.Collections; @@ -216,7 +218,6 @@ public class MediaSourceManager { // Event specific action switch (event.type()) { case INIT: - case REORDER: case ERROR: reset(); break; @@ -231,6 +232,12 @@ public class MediaSourceManager { final MoveEvent moveEvent = (MoveEvent) event; move(moveEvent.getFromIndex(), moveEvent.getToIndex()); break; + case REORDER: + // Need to move to ensure the playing index from play queue matches that of + // the source timeline, and then window correction can take care of the rest + final ReorderEvent reorderEvent = (ReorderEvent) event; + move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); + break; case SELECT: case RECOVERY: default: @@ -305,7 +312,7 @@ public class MediaSourceManager { } /*////////////////////////////////////////////////////////////////////////// - // Metadata Synchronization TODO: maybe this should be a separate manager + // Metadata Synchronization //////////////////////////////////////////////////////////////////////////*/ private void maybeSync() { @@ -389,8 +396,8 @@ public class MediaSourceManager { if (playQueue.indexOf(item) >= sources.getSize()) return; if (!loadingItems.contains(item) && isCorrectionNeeded(item)) { - if (DEBUG) Log.d(TAG, "MediaSource - Loading: [" + item.getTitle() + - "] with url: " + item.getUrl()); + if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() + + "] with url=[" + item.getUrl() + "]"); loadingItems.add(item); final Disposable loader = getLoadedMediaSource(item) @@ -423,16 +430,16 @@ public class MediaSourceManager { private void onMediaSourceReceived(@NonNull final PlayQueueItem item, @NonNull final ManagedMediaSource mediaSource) { - if (DEBUG) Log.d(TAG, "MediaSource - Loaded: [" + item.getTitle() + - "] with url: " + item.getUrl()); + if (DEBUG) Log.d(TAG, "MediaSource - Loaded=[" + item.getTitle() + + "] with url=[" + item.getUrl() + "]"); loadingItems.remove(item); final int itemIndex = playQueue.indexOf(item); // Only update the playlist timeline for items at the current index or after. if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) { - if (DEBUG) Log.d(TAG, "MediaSource - Updating: [" + item.getTitle() + - "] with url: " + item.getUrl()); + if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " + + "title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]"); update(itemIndex, mediaSource, this::maybeSynchronizePlayer); } } @@ -468,7 +475,8 @@ public class MediaSourceManager { if (DEBUG) Log.d(TAG, "resetSources() called."); this.sources.releaseSource(); - this.sources = new DynamicConcatenatingMediaSource(); + this.sources = new DynamicConcatenatingMediaSource(false, + new ShuffleOrder.UnshuffledShuffleOrder(0)); } private void populateSources() { 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 8f4c5913d..19e6dc63d 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -351,6 +351,7 @@ public abstract class PlayQueue implements Serializable { if (backup == null) { backup = new ArrayList<>(streams); } + final int originIndex = getIndex(); final PlayQueueItem current = getItem(); Collections.shuffle(streams); @@ -360,7 +361,7 @@ public abstract class PlayQueue implements Serializable { } queueIndex.set(0); - broadcast(new ReorderEvent()); + broadcast(new ReorderEvent(originIndex, queueIndex.get())); } /** @@ -373,6 +374,7 @@ public abstract class PlayQueue implements Serializable { * */ public synchronized void unshuffle() { if (backup == null) return; + final int originIndex = getIndex(); final PlayQueueItem current = getItem(); streams.clear(); @@ -386,7 +388,7 @@ public abstract class PlayQueue implements Serializable { queueIndex.set(0); } - broadcast(new ReorderEvent()); + broadcast(new ReorderEvent(originIndex, queueIndex.get())); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/playlist/events/ReorderEvent.java b/app/src/main/java/org/schabi/newpipe/playlist/events/ReorderEvent.java index f1d09d457..19bb632d8 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/events/ReorderEvent.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/events/ReorderEvent.java @@ -1,12 +1,24 @@ package org.schabi.newpipe.playlist.events; public class ReorderEvent implements PlayQueueEvent { + private final int fromSelectedIndex; + private final int toSelectedIndex; + @Override public PlayQueueEventType type() { return PlayQueueEventType.REORDER; } - public ReorderEvent() { + public ReorderEvent(final int fromSelectedIndex, final int toSelectedIndex) { + this.fromSelectedIndex = fromSelectedIndex; + this.toSelectedIndex = toSelectedIndex; + } + public int getFromSelectedIndex() { + return fromSelectedIndex; + } + + public int getToSelectedIndex() { + return toSelectedIndex; } } From 7f068b691b7516629feda0e149e759d303b106e9 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 3 Mar 2018 20:58:53 -0800 Subject: [PATCH 15/19] -Removed system ui on main player for Kitkat or above. -[#1151] Hide video player UI on playing to avoid unnecessary interruptions after pause, seek and resize. --- .../org/schabi/newpipe/player/BasePlayer.java | 6 +- .../newpipe/player/MainVideoPlayer.java | 114 ++++++++++++------ .../newpipe/player/PopupVideoPlayer.java | 9 +- .../schabi/newpipe/player/VideoPlayer.java | 32 ++--- .../main/res/layout/activity_main_player.xml | 1 + 5 files changed, 99 insertions(+), 63 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 f89bd2630..58047639c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -449,15 +449,13 @@ public abstract class BasePlayer implements if (!isCurrentWindowValid()) seekToDefault(); } - public void onBuffering() { - } + public void onBuffering() {} public void onPaused() { if (isProgressLoopRunning()) stopProgressLoop(); } - public void onPausedSeek() { - } + public void onPausedSeek() {} public void onCompleted() { if (DEBUG) Log.d(TAG, "onCompleted() called"); diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 04d93bbdc..0bfdcd32a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -69,6 +69,9 @@ import org.schabi.newpipe.util.ThemeHelper; import java.util.List; +import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; +import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; +import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.util.AnimationUtils.animateView; /** @@ -114,7 +117,7 @@ public final class MainVideoPlayer extends Activity { return; } - showSystemUi(); + changeSystemUi(); setContentView(R.layout.activity_main_player); playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); @@ -206,31 +209,53 @@ public final class MainVideoPlayer extends Activity { // Utils //////////////////////////////////////////////////////////////////////////*/ + /** + * Prior to Kitkat, hiding system ui causes the player view to be overlaid and require two + * clicks to get rid of that invisible overlay. By showing the system UI on actions/events, + * that overlay is removed and the player view is put to the foreground. + * + * Post Kitkat, navbar and status bar can be pulled out by swiping the edge of + * screen, therefore, we can do nothing or hide the UI on actions/events. + * */ + private void changeSystemUi() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + showSystemUi(); + } else { + hideSystemUi(); + } + } + private void showSystemUi() { if (DEBUG) Log.d(TAG, "showSystemUi() called"); if (playerImpl != null && playerImpl.queueVisible) return; + + final int visibility; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - ); - } else getWindow().getDecorView().setSystemUiVisibility(0); + visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + } else { + visibility = View.STATUS_BAR_VISIBLE; + } + getWindow().getDecorView().setSystemUiVisibility(visibility); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } private void hideSystemUi() { if (DEBUG) Log.d(TAG, "hideSystemUi() called"); - if (android.os.Build.VERSION.SDK_INT >= 16) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + } getWindow().getDecorView().setSystemUiVisibility(visibility); } - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); } private void toggleOrientation() { @@ -307,6 +332,7 @@ public final class MainVideoPlayer extends Activity { private ImageButton switchPopupButton; private ImageButton switchBackgroundButton; + private RelativeLayout windowRootLayout; private View secondaryControls; VideoPlayerImpl(final Context context) { @@ -334,6 +360,19 @@ public final class MainVideoPlayer extends Activity { this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground); this.switchPopupButton = rootView.findViewById(R.id.switchPopup); + this.queueLayout = findViewById(R.id.playQueuePanel); + this.itemsListCloseButton = findViewById(R.id.playQueueClose); + this.itemsList = findViewById(R.id.playQueue); + + this.windowRootLayout = rootView.findViewById(R.id.playbackWindowRoot); + // Prior to Kitkat, there is no way of setting translucent navbar programmatically. + // Thus, fit system windows is opted instead. + // See https://stackoverflow.com/questions/29069070/completely-transparent-status-bar-and-navigation-bar-on-lollipop + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + windowRootLayout.setFitsSystemWindows(false); + windowRootLayout.invalidate(); + } + titleTextView.setSelected(true); channelTextView.setSelected(true); @@ -509,9 +548,9 @@ public final class MainVideoPlayer extends Activity { if (getCurrentState() != STATE_COMPLETED) { getControlsVisibilityHandler().removeCallbacksAndMessages(null); - animateView(getControlsRoot(), true, 300, 0, () -> { + animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { - hideControls(300, DEFAULT_CONTROLS_HIDE_TIME); + hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } }); } @@ -547,7 +586,7 @@ public final class MainVideoPlayer extends Activity { R.drawable.ic_expand_less_white_24dp)); animateView(secondaryControls, true, 200); } - showControls(300); + showControls(DEFAULT_CONTROLS_DURATION); } private void onScreenRotationClicked() { @@ -559,15 +598,13 @@ public final class MainVideoPlayer extends Activity { @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); - if (wasPlaying()) { - hideControls(100, 0); - } + if (wasPlaying()) showControlsThenHide(); } @Override public void onDismiss(PopupMenu menu) { super.onDismiss(menu); - if (isPlaying()) hideControls(300, 0); + if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); } @Override @@ -625,7 +662,8 @@ public final class MainVideoPlayer extends Activity { playPauseButton.setImageResource(R.drawable.ic_pause_white); animatePlayButtons(true, 200); }); - showSystemUi(); + + changeSystemUi(); getRootView().setKeepScreenOn(true); } @@ -637,7 +675,7 @@ public final class MainVideoPlayer extends Activity { animatePlayButtons(true, 200); }); - showSystemUi(); + changeSystemUi(); getRootView().setKeepScreenOn(false); } @@ -651,10 +689,9 @@ public final class MainVideoPlayer extends Activity { @Override public void onCompleted() { - showSystemUi(); animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { playPauseButton.setImageResource(R.drawable.ic_replay_white); - animatePlayButtons(true, 300); + animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); }); getRootView().setKeepScreenOn(false); @@ -684,8 +721,9 @@ public final class MainVideoPlayer extends Activity { if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); getControlsVisibilityHandler().removeCallbacksAndMessages(null); getControlsVisibilityHandler().postDelayed(() -> - animateView(getControlsRoot(), false, duration, 0, MainVideoPlayer.this::hideSystemUi), - delay + animateView(getControlsRoot(), false, duration, 0, + MainVideoPlayer.this::hideSystemUi), + /*delayMillis=*/delay ); } @@ -698,11 +736,6 @@ public final class MainVideoPlayer extends Activity { } private void buildQueue() { - queueLayout = findViewById(R.id.playQueuePanel); - - itemsListCloseButton = findViewById(R.id.playQueueClose); - - itemsList = findViewById(R.id.playQueue); itemsList.setAdapter(playQueueAdapter); itemsList.setClickable(true); itemsList.setLongClickable(true); @@ -831,10 +864,11 @@ public final class MainVideoPlayer extends Activity { if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true; - if (playerImpl.isControlsVisible()) playerImpl.hideControls(150, 0); - else { + if (playerImpl.isControlsVisible()) { + playerImpl.hideControls(150, 0); + } else { playerImpl.showControlsThenHide(); - showSystemUi(); + changeSystemUi(); } return true; } @@ -917,11 +951,15 @@ public final class MainVideoPlayer extends Activity { eventsNum = 0; /* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/ - if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getVolumeTextView(), false, 200, 200); - if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), false, 200, 200); + if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getVolumeTextView(), false, 200, 200); + } + if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getBrightnessTextView(), false, 200, 200); + } - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) { - playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME); + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } } 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 80063e547..123fbfee3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -70,6 +70,9 @@ import org.schabi.newpipe.util.ThemeHelper; import java.util.List; +import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; +import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; +import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -650,6 +653,8 @@ public final class PopupVideoPlayer extends Service { super.onPlaying(); updateNotification(R.drawable.ic_pause_white); lockManager.acquireWifiAndCpu(); + + hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } @Override @@ -782,8 +787,8 @@ public final class PopupVideoPlayer extends Service { private void onScrollEnd() { if (DEBUG) Log.d(TAG, "onScrollEnd() called"); if (playerImpl == null) return; - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) { - playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME); + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } } 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 5b4a1f80b..aa90b7b88 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -101,6 +101,7 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ protected static final int RENDERER_UNAVAILABLE = -1; + public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds private ArrayList availableStreams; @@ -450,7 +451,7 @@ public abstract class VideoPlayer extends BasePlayer super.onBlocked(); controlsVisibilityHandler.removeCallbacksAndMessages(null); - animateView(controlsRoot, false, 300); + animateView(controlsRoot, false, DEFAULT_CONTROLS_DURATION); playbackSeekBar.setEnabled(false); // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again @@ -475,7 +476,7 @@ public abstract class VideoPlayer extends BasePlayer playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); loadingPanel.setVisibility(View.GONE); - showControlsThenHide(); + animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); animateView(endScreen, false, 0); } @@ -707,7 +708,7 @@ public abstract class VideoPlayer extends BasePlayer if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called"); qualityPopupMenu.show(); isSomePopupMenuVisible = true; - showControls(300); + showControls(DEFAULT_CONTROLS_DURATION); final VideoStream videoStream = getSelectedVideoStream(); if (videoStream != null) { @@ -723,14 +724,14 @@ public abstract class VideoPlayer extends BasePlayer if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); playbackSpeedPopupMenu.show(); isSomePopupMenuVisible = true; - showControls(300); + showControls(DEFAULT_CONTROLS_DURATION); } private void onCaptionClicked() { if (DEBUG) Log.d(TAG, "onCaptionClicked() called"); captionPopupMenu.show(); isSomePopupMenuVisible = true; - showControls(300); + showControls(DEFAULT_CONTROLS_DURATION); } private void onResizeClicked() { @@ -763,7 +764,8 @@ public abstract class VideoPlayer extends BasePlayer if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); showControls(0); - animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, 300); + animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, + DEFAULT_CONTROLS_DURATION); } @Override @@ -819,7 +821,7 @@ public abstract class VideoPlayer extends BasePlayer PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) - ).setDuration(300); + ).setDuration(DEFAULT_CONTROLS_DURATION); controlViewAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -861,12 +863,8 @@ public abstract class VideoPlayer extends BasePlayer public void showControlsThenHide() { if (DEBUG) Log.d(TAG, "showControlsThenHide() called"); - animateView(controlsRoot, true, 300, 0, new Runnable() { - @Override - public void run() { - hideControls(300, DEFAULT_CONTROLS_HIDE_TIME); - } - }); + animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0, + () -> hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME)); } public void showControls(long duration) { @@ -878,12 +876,8 @@ public abstract class VideoPlayer extends BasePlayer public void hideControls(final long duration, long delay) { if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed(new Runnable() { - @Override - public void run() { - animateView(controlsRoot, false, duration); - } - }, delay); + controlsVisibilityHandler.postDelayed( + () -> animateView(controlsRoot, false, duration), delay); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 9abe43715..e7d337c17 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -134,6 +134,7 @@ tools:visibility="visible"> From d01aeab2425c45471d1f4e03ada84887f64992cd Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 4 Mar 2018 20:16:38 -0800 Subject: [PATCH 16/19] -Added auto-queuing to allow next or related streams to queue up when the last item on play queue is playing. -Added toggle to enable auto-queuing. -Modified main video player to only pause the video onPause. -Fixed main video player not saving play queue state onStop. --- .../org/schabi/newpipe/player/BasePlayer.java | 31 ++++++--- .../newpipe/player/MainVideoPlayer.java | 36 ++++++----- .../newpipe/player/helper/PlayerHelper.java | 64 +++++++++++++++++++ app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/content_settings.xml | 7 ++ 6 files changed, 115 insertions(+), 26 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 58047639c..73d0b9180 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -68,6 +68,7 @@ import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueAdapter; import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.SerializedCache; import java.io.IOException; @@ -818,22 +819,32 @@ public abstract class BasePlayer implements initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); } - final int currentSourceIndex = playQueue.indexOf(item); - onMetadataChanged(item, info, currentSourceIndex, hasPlayQueueItemChanged); + final int currentPlayQueueIndex = playQueue.indexOf(item); + onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); - // Check if on wrong window if (simpleExoPlayer == null) return; - if (currentSourceIndex != playQueue.getIndex()) { - Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex + - "], queue index=[" + playQueue.getIndex() + "]"); + final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + // Check if on wrong window + if (currentPlayQueueIndex != playQueue.getIndex()) { + Log.e(TAG, "Play Queue may be desynchronized: item " + + "index=[" + currentPlayQueueIndex + "], " + + "queue index=[" + playQueue.getIndex() + "]"); // on metadata changed - } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) { - final long startPos = info != null ? info.start_position : 0; - if (DEBUG) Log.d(TAG, "Rewinding to correct window=[" + currentSourceIndex + "]," + + } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { + final long startPos = info != null ? info.start_position : C.TIME_UNSET; + if (DEBUG) Log.d(TAG, "Rewinding to correct" + + " window=[" + currentPlayQueueIndex + "]," + " at=[" + getTimeString((int)startPos) + "]," + " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); - simpleExoPlayer.seekTo(currentSourceIndex, startPos); + simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); + } + + // when starting playback on the last item, maybe auto queue + if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && + PlayerHelper.isAutoQueueEnabled(context)) { + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); + if (autoQueue != null) playQueue.append(autoQueue.getStreams()); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 0bfdcd32a..146b511c4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -129,6 +129,7 @@ public final class MainVideoPlayer extends Activity { super.onSaveInstanceState(outState); if (this.playerImpl == null) return; + playerImpl.setRecovery(); final Intent intent = NavigationHelper.getPlayerIntent( getApplicationContext(), this.getClass(), @@ -156,31 +157,27 @@ public final class MainVideoPlayer extends Activity { } @Override - protected void onStop() { - super.onStop(); - if (DEBUG) Log.d(TAG, "onStop() called"); - activityPaused = true; + protected void onPause() { + super.onPause(); + if (DEBUG) Log.d(TAG, "onPause() called"); - if (playerImpl.getPlayer() != null) { - playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); - playerImpl.setRecovery(); - playerImpl.destroyPlayer(); + if (playerImpl.getPlayer() != null && playerImpl.isPlaying() && !activityPaused) { + playerImpl.wasPlaying = true; + playerImpl.onVideoPlayPause(); } + activityPaused = true; } @Override protected void onResume() { super.onResume(); if (DEBUG) Log.d(TAG, "onResume() called"); - if (activityPaused) { - playerImpl.initPlayer(); - playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white); - - playerImpl.getPlayer().setPlayWhenReady(playerImpl.wasPlaying); - playerImpl.initPlayback(playerImpl.playQueue); - - activityPaused = false; + if (playerImpl.getPlayer() != null && playerImpl.wasPlaying() + && !playerImpl.isPlaying() && activityPaused) { + playerImpl.onVideoPlayPause(); } + activityPaused = false; + if(globalScreenOrientationLocked()) { boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false); @@ -873,6 +870,13 @@ public final class MainVideoPlayer extends Activity { return true; } + @Override + public boolean onDown(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); + + return super.onDown(e); + } + private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 553163d21..87b0f701f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -4,22 +4,33 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.Subtitles; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.SubtitlesFormat; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.playlist.SinglePlayQueue; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; import java.util.Formatter; +import java.util.HashSet; +import java.util.List; import java.util.Locale; +import java.util.Set; import javax.annotation.Nonnull; @@ -76,6 +87,7 @@ public class PlayerHelper { return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); } + @NonNull public static String resizeTypeOf(@NonNull final Context context, @AspectRatioFrameLayout.ResizeMode final int resizeMode) { switch (resizeMode) { @@ -86,14 +98,58 @@ public class PlayerHelper { } } + @NonNull public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) { return info.getUrl() + video.getResolution() + video.getFormat().getName(); } + @NonNull public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) { return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName(); } + /** + * Given a {@link StreamInfo} and the existing queue items, provide the + * {@link SinglePlayQueue} consisting of the next video for auto queuing. + *

+ * This method detects and prevents cycle by naively checking if a + * candidate next video's url already exists in the existing items. + *

+ * To select the next video, {@link StreamInfo#getNextVideo()} is first + * checked. If it is nonnull and is not part of the existing items, then + * it will be used as the next video. Otherwise, an random item with + * non-repeating url will be selected from the {@link StreamInfo#getRelatedStreams()}. + * */ + @Nullable + public static PlayQueue autoQueueOf(@NonNull final StreamInfo info, + @NonNull final List existingItems) { + Set urls = new HashSet<>(existingItems.size()); + for (final PlayQueueItem item : existingItems) { + urls.add(item.getUrl()); + } + + final StreamInfoItem nextVideo = info.getNextVideo(); + if (nextVideo != null && !urls.contains(nextVideo.getUrl())) { + return new SinglePlayQueue(nextVideo); + } + + final List relatedItems = info.getRelatedStreams(); + if (relatedItems == null) return null; + + List autoQueueItems = new ArrayList<>(); + for (final InfoItem item : info.getRelatedStreams()) { + if (item instanceof StreamInfoItem && !urls.contains(item.getUrl())) { + autoQueueItems.add((StreamInfoItem) item); + } + } + Collections.shuffle(autoQueueItems); + return autoQueueItems.isEmpty() ? null : new SinglePlayQueue(autoQueueItems.get(0)); + } + + //////////////////////////////////////////////////////////////////////////// + // Settings Resolution + //////////////////////////////////////////////////////////////////////////// + public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { return isResumeAfterAudioFocusGain(context, false); } @@ -110,6 +166,10 @@ public class PlayerHelper { return isRememberingPopupDimensions(context, true); } + public static boolean isAutoQueueEnabled(@NonNull final Context context) { + return isAutoQueueEnabled(context, false); + } + @NonNull public static SeekParameters getSeekParameters(@NonNull final Context context) { return isUsingInexactSeek(context, false) ? @@ -187,4 +247,8 @@ public class PlayerHelper { private static boolean isUsingInexactSeek(@NonNull final Context context, final boolean b) { return getPreferences(context).getBoolean(context.getString(R.string.use_inexact_seek_key), b); } + + private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) { + return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b); + } } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index c207ed712..a897aa185 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -21,6 +21,7 @@ resume_on_audio_focus_gain popup_remember_size_pos_key use_inexact_seek_key + auto_queue_key default_resolution 360p diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a8123bde..d875036e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,6 +74,8 @@ Remember last size and position of popup Use fast inexact seek Inexact seek allows the player to seek to positions faster with reduced precision + Auto-queue next stream + Automatically append a related stream when playback starts on the last stream in play queue. Player gesture controls Use gestures to control the brightness and volume of the player Search suggestions diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index 20099d5c0..c8c1efb12 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -30,6 +30,13 @@ android:key="@string/show_search_suggestions_key" android:summary="@string/show_search_suggestions_summary" android:title="@string/show_search_suggestions_title"/> + + + Date: Mon, 5 Mar 2018 19:03:49 -0800 Subject: [PATCH 17/19] -Fixed main video player losing state when killed in background. -Disabled auto queuing when repeating is enabled. -Added method to use startForegroundService instead of startService in sdk 26 and up. --- .../org/schabi/newpipe/player/BasePlayer.java | 21 ++- .../newpipe/player/MainVideoPlayer.java | 160 +++++++++++------- .../schabi/newpipe/util/NavigationHelper.java | 18 +- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 125 insertions(+), 76 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 73d0b9180..d5ba7bb86 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -68,7 +68,6 @@ import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueAdapter; import org.schabi.newpipe.playlist.PlayQueueItem; -import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.SerializedCache; import java.io.IOException; @@ -230,17 +229,19 @@ public abstract class BasePlayer implements final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()); final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); - // Re-initialization + // Good to go... + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch); + } + + protected void initPlayback(@NonNull final PlayQueue queue, + @Player.RepeatMode final int repeatMode, + final float playbackSpeed, + final float playbackPitch) { destroyPlayer(); initPlayer(); setRepeatMode(repeatMode); setPlaybackParameters(playbackSpeed, playbackPitch); - // Good to go... - initPlayback(queue); - } - - protected void initPlayback(final PlayQueue queue) { playQueue = queue; playQueue.init(); playbackManager = new MediaSourceManager(this, playQueue); @@ -840,8 +841,9 @@ public abstract class BasePlayer implements simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); } - // when starting playback on the last item, maybe auto queue + // when starting playback on the last item when not repeating, maybe auto queue if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && + getRepeatMode() == Player.REPEAT_MODE_OFF && PlayerHelper.isAutoQueueEnabled(context)) { final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); if (autoQueue != null) playQueue.append(autoQueue.getStreams()); @@ -1077,11 +1079,12 @@ public abstract class BasePlayer implements && simpleExoPlayer.getPlayWhenReady(); } + @Player.RepeatMode public int getRepeatMode() { return simpleExoPlayer.getRepeatMode(); } - public void setRepeatMode(final int repeatMode) { + public void setRepeatMode(@Player.RepeatMode final int repeatMode) { simpleExoPlayer.setRepeatMode(repeatMode); } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 146b511c4..4f27d1fee 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -58,6 +58,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemHolder; @@ -65,24 +66,27 @@ import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; import java.util.List; +import java.util.Queue; +import java.util.UUID; import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; /** * Activity Player implementing VideoPlayer * * @author mauriciocolli */ -public final class MainVideoPlayer extends Activity { +public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead { private static final String TAG = ".MainVideoPlayer"; private static final boolean DEBUG = BasePlayer.DEBUG; - private static final String PLAYER_STATE_INTENT = "player_state_intent"; private GestureDetector gestureDetector; @@ -91,6 +95,8 @@ public final class MainVideoPlayer extends Activity { private SharedPreferences defaultPreferences; + @Nullable private StateSaver.SavedState savedState; + /*////////////////////////////////////////////////////////////////////////// // Activity LifeCycle //////////////////////////////////////////////////////////////////////////*/ @@ -104,42 +110,28 @@ public final class MainVideoPlayer extends Activity { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); setVolumeControlStream(AudioManager.STREAM_MUSIC); - final Intent intent; - if (savedInstanceState != null && savedInstanceState.getParcelable(PLAYER_STATE_INTENT) != null) { - intent = savedInstanceState.getParcelable(PLAYER_STATE_INTENT); - } else { - intent = getIntent(); - } - - if (intent == null) { - Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show(); - finish(); - return; - } - changeSystemUi(); setContentView(R.layout.activity_main_player); playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); - playerImpl.handleIntent(intent); + + if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { + return; // We have saved states, stop here to restore it + } + + final Intent intent = getIntent(); + if (intent != null) { + playerImpl.handleIntent(intent); + } else { + Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show(); + finish(); + } } @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (this.playerImpl == null) return; - - playerImpl.setRecovery(); - final Intent intent = NavigationHelper.getPlayerIntent( - getApplicationContext(), - this.getClass(), - this.playerImpl.getPlayQueue(), - this.playerImpl.getRepeatMode(), - this.playerImpl.getPlaybackSpeed(), - this.playerImpl.getPlaybackPitch(), - this.playerImpl.getPlaybackQuality() - ); - outState.putParcelable(PLAYER_STATE_INTENT, intent); + protected void onRestoreInstanceState(@NonNull Bundle bundle) { + super.onRestoreInstanceState(bundle); + savedState = StateSaver.tryToRestore(bundle, this); } @Override @@ -149,31 +141,12 @@ public final class MainVideoPlayer extends Activity { playerImpl.handleIntent(intent); } - @Override - public void onBackPressed() { - if (DEBUG) Log.d(TAG, "onBackPressed() called"); - super.onBackPressed(); - if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); - } - - @Override - protected void onPause() { - super.onPause(); - if (DEBUG) Log.d(TAG, "onPause() called"); - - if (playerImpl.getPlayer() != null && playerImpl.isPlaying() && !activityPaused) { - playerImpl.wasPlaying = true; - playerImpl.onVideoPlayPause(); - } - activityPaused = true; - } - @Override protected void onResume() { super.onResume(); if (DEBUG) Log.d(TAG, "onResume() called"); - if (playerImpl.getPlayer() != null && playerImpl.wasPlaying() - && !playerImpl.isPlaying() && activityPaused) { + if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() + && !playerImpl.isPlaying()) { playerImpl.onVideoPlayPause(); } activityPaused = false; @@ -181,15 +154,15 @@ public final class MainVideoPlayer extends Activity { if(globalScreenOrientationLocked()) { boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false); - setLandScape(lastOrientationWasLandscape); + setLandscape(lastOrientationWasLandscape); } } @Override - protected void onDestroy() { - super.onDestroy(); - if (DEBUG) Log.d(TAG, "onDestroy() called"); - if (playerImpl != null) playerImpl.destroy(); + public void onBackPressed() { + if (DEBUG) Log.d(TAG, "onBackPressed() called"); + super.onBackPressed(); + if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); } @Override @@ -202,8 +175,71 @@ public final class MainVideoPlayer extends Activity { } } + @Override + protected void onPause() { + super.onPause(); + if (DEBUG) Log.d(TAG, "onPause() called"); + + if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) { + playerImpl.wasPlaying = playerImpl.isPlaying(); + if (playerImpl.isPlaying()) playerImpl.onVideoPlayPause(); + } + activityPaused = true; + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (playerImpl == null) return; + + playerImpl.setRecovery(); + savedState = StateSaver.tryToSave(isChangingConfigurations(), savedState, + outState, this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (DEBUG) Log.d(TAG, "onDestroy() called"); + if (playerImpl != null) playerImpl.destroy(); + } + /*////////////////////////////////////////////////////////////////////////// - // Utils + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public String generateSuffix() { + return "." + UUID.randomUUID().toString() + ".player"; + } + + @Override + public void writeTo(Queue objectsToSave) { + if (objectsToSave == null) return; + objectsToSave.add(playerImpl.getPlayQueue()); + objectsToSave.add(playerImpl.getRepeatMode()); + objectsToSave.add(playerImpl.getPlaybackSpeed()); + objectsToSave.add(playerImpl.getPlaybackPitch()); + objectsToSave.add(playerImpl.getPlaybackQuality()); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull Queue savedObjects) throws Exception { + @NonNull final PlayQueue queue = (PlayQueue) savedObjects.poll(); + final int repeatMode = (int) savedObjects.poll(); + final float playbackSpeed = (float) savedObjects.poll(); + final float playbackPitch = (float) savedObjects.poll(); + @NonNull final String playbackQuality = (String) savedObjects.poll(); + + playerImpl.setPlaybackQuality(playbackQuality); + playerImpl.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch); + + StateSaver.onDestroy(savedState); + } + + /*////////////////////////////////////////////////////////////////////////// + // View //////////////////////////////////////////////////////////////////////////*/ /** @@ -256,17 +292,17 @@ public final class MainVideoPlayer extends Activity { } private void toggleOrientation() { - setLandScape(!isLandScape()); + setLandscape(!isLandscape()); defaultPreferences.edit() - .putBoolean(getString(R.string.last_orientation_landscape_key), !isLandScape()) + .putBoolean(getString(R.string.last_orientation_landscape_key), !isLandscape()) .apply(); } - private boolean isLandScape() { + private boolean isLandscape() { return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels; } - private void setLandScape(boolean v) { + private void setLandscape(boolean v) { setRequestedOrientation(v ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 4fc854416..ee94ac81f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -139,12 +139,12 @@ public class NavigationHelper { } Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - context.startService(getPlayerIntent(context, PopupVideoPlayer.class, queue)); + startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue)); } public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue) { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); - context.startService(getPlayerIntent(context, BackgroundPlayer.class, queue)); + startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue)); } public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue) { @@ -158,7 +158,8 @@ public class NavigationHelper { } Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); - context.startService(getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend)); + startService(context, + getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend)); } public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue) { @@ -167,7 +168,16 @@ public class NavigationHelper { public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) { Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); - context.startService(getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend)); + startService(context, + getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend)); + } + + public static void startService(@NonNull final Context context, @NonNull final Intent intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d875036e4..5cde58f8d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,7 +75,7 @@ Use fast inexact seek Inexact seek allows the player to seek to positions faster with reduced precision Auto-queue next stream - Automatically append a related stream when playback starts on the last stream in play queue. + Automatically append a related stream when playback starts on the last stream in a non-repeating play queue. Player gesture controls Use gestures to control the brightness and volume of the player Search suggestions From 03d7a416f360530d59c83b5809ab59aecb9c87e9 Mon Sep 17 00:00:00 2001 From: Schabi Date: Tue, 6 Mar 2018 19:24:58 +0100 Subject: [PATCH 18/19] add live lable to toolbar --- .../newpipe/fragments/detail/VideoDetailFragment.java | 6 +++++- app/src/main/res/values/strings.xml | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 6d505b00e..8e8179b9a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -16,6 +16,7 @@ import android.support.v4.content.ContextCompat; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; +import android.support.v7.widget.Toolbar; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; @@ -139,6 +140,7 @@ public class VideoDetailFragment //////////////////////////////////////////////////////////////////////////*/ private Menu menu; + private Toolbar toolbar; private Spinner spinnerToolbar; private ParallaxScrollView parallaxScrollRootView; @@ -460,7 +462,8 @@ public class VideoDetailFragment protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner); + toolbar = activity.findViewById(R.id.toolbar); + spinnerToolbar = toolbar.findViewById(R.id.toolbar_spinner); parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content); @@ -1198,6 +1201,7 @@ public class VideoDetailFragment case AUDIO_LIVE_STREAM: detailControlsDownload.setVisibility(View.GONE); spinnerToolbar.setVisibility(View.GONE); + toolbar.setTitle(R.string.live); break; default: if (!info.video_streams.isEmpty() || !info.video_only_streams.isEmpty()) break; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5cde58f8d..e64f15206 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,6 +113,7 @@ Show age restricted content Age Restricted Video. Allowing such material is possible from Settings. live + LIVE Downloads Downloads Error report From c3efb40b8ed2ce11a3492b8de5490a8dea5dfab5 Mon Sep 17 00:00:00 2001 From: Schabi Date: Tue, 6 Mar 2018 19:44:17 +0100 Subject: [PATCH 19/19] move to latest version of extractor --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 74a005ce3..630e6ba4d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.karyogamy:NewPipeExtractor:b4206479cb' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:b1130629bb' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19'