From 0c17f0825b6f8b23ccba2c845fa5275754b29848 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 3 Mar 2018 11:42:23 -0800 Subject: [PATCH] -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; }