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;
}