mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-26 21:07:38 +00:00 
			
		
		
		
	-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.
This commit is contained in:
		| @@ -55,7 +55,7 @@ dependencies { | |||||||
|         exclude module: 'support-annotations' |         exclude module: 'support-annotations' | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     implementation 'com.github.karyogamy:NewPipeExtractor:4cf4ee394f' |     implementation 'com.github.karyogamy:NewPipeExtractor:b4206479cb' | ||||||
|  |  | ||||||
|     testImplementation 'junit:junit:4.12' |     testImplementation 'junit:junit:4.12' | ||||||
|     testImplementation 'org.mockito:mockito-core:1.10.19' |     testImplementation 'org.mockito:mockito-core:1.10.19' | ||||||
|   | |||||||
| @@ -322,7 +322,7 @@ public class VideoDetailFragment | |||||||
|         if (serializable instanceof StreamInfo) { |         if (serializable instanceof StreamInfo) { | ||||||
|             //noinspection unchecked |             //noinspection unchecked | ||||||
|             currentInfo = (StreamInfo) serializable; |             currentInfo = (StreamInfo) serializable; | ||||||
|             InfoCache.getInstance().putInfo(currentInfo); |             InfoCache.getInstance().putInfo(serviceId, url, currentInfo); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         serializable = savedState.getSerializable(STACK_KEY); |         serializable = savedState.getSerializable(STACK_KEY); | ||||||
|   | |||||||
| @@ -33,6 +33,7 @@ import android.support.annotation.NonNull; | |||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
| import android.support.v4.app.NotificationCompat; | import android.support.v4.app.NotificationCompat; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  | import android.view.View; | ||||||
| import android.widget.RemoteViews; | import android.widget.RemoteViews; | ||||||
|  |  | ||||||
| import com.google.android.exoplayer2.PlaybackParameters; | import com.google.android.exoplayer2.PlaybackParameters; | ||||||
| @@ -292,15 +293,15 @@ public final class BackgroundPlayer extends Service { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void onThumbnailReceived(Bitmap thumbnail) { |         public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||||
|             super.onThumbnailReceived(thumbnail); |             super.onLoadingComplete(imageUri, view, loadedImage); | ||||||
|  |  | ||||||
|             if (thumbnail != null) { |             if (loadedImage != null) { | ||||||
|                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks |                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks | ||||||
|                 resetNotification(); |                 resetNotification(); | ||||||
|  |  | ||||||
|                 if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); |                 if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); | ||||||
|                 if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); |                 if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); | ||||||
|  |  | ||||||
|                 updateNotification(-1); |                 updateNotification(-1); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ import com.google.android.exoplayer2.Player; | |||||||
| import com.google.android.exoplayer2.RenderersFactory; | import com.google.android.exoplayer2.RenderersFactory; | ||||||
| import com.google.android.exoplayer2.SimpleExoPlayer; | import com.google.android.exoplayer2.SimpleExoPlayer; | ||||||
| import com.google.android.exoplayer2.Timeline; | 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.MediaSource; | ||||||
| import com.google.android.exoplayer2.source.TrackGroupArray; | import com.google.android.exoplayer2.source.TrackGroupArray; | ||||||
| import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; | 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.upstream.DefaultBandwidthMeter; | ||||||
| import com.google.android.exoplayer2.util.Util; | import com.google.android.exoplayer2.util.Util; | ||||||
| import com.nostra13.universalimageloader.core.ImageLoader; | 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.Downloader; | ||||||
| import org.schabi.newpipe.R; | 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.playlist.PlayQueueItem; | ||||||
| import org.schabi.newpipe.util.SerializedCache; | import org.schabi.newpipe.util.SerializedCache; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.net.UnknownHostException; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
| import io.reactivex.Observable; | import io.reactivex.Observable; | ||||||
| @@ -86,17 +90,18 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; | |||||||
|  * @author mauriciocolli |  * @author mauriciocolli | ||||||
|  */ |  */ | ||||||
| @SuppressWarnings({"WeakerAccess"}) | @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 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; |     @NonNull final protected BroadcastReceiver broadcastReceiver; | ||||||
|     protected IntentFilter intentFilter; |     @NonNull final protected IntentFilter intentFilter; | ||||||
|  |  | ||||||
|     protected PlayQueueAdapter playQueueAdapter; |     @NonNull final protected HistoryRecordManager recordManager; | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Intent |     // 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_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 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 PlayQueue playQueue; | ||||||
|  |     protected PlayQueueAdapter playQueueAdapter; | ||||||
|  |  | ||||||
|  |     protected MediaSourceManager playbackManager; | ||||||
|  |  | ||||||
|     protected StreamInfo currentInfo; |     protected StreamInfo currentInfo; | ||||||
|     protected PlayQueueItem currentItem; |     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 PROGRESS_LOOP_INTERVAL = 500; | ||||||
|     protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds |     protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds | ||||||
|  |  | ||||||
|  |     protected CustomTrackSelector trackSelector; | ||||||
|  |     protected PlayerDataSource dataSource; | ||||||
|  |  | ||||||
|     protected SimpleExoPlayer simpleExoPlayer; |     protected SimpleExoPlayer simpleExoPlayer; | ||||||
|     protected AudioReactor audioReactor; |     protected AudioReactor audioReactor; | ||||||
|  |  | ||||||
|     protected boolean isPrepared = false; |     protected boolean isPrepared = false; | ||||||
|  |  | ||||||
|     protected CustomTrackSelector trackSelector; |  | ||||||
|  |  | ||||||
|     protected PlayerDataSource dataSource; |  | ||||||
|  |  | ||||||
|     protected Disposable progressUpdateReactor; |     protected Disposable progressUpdateReactor; | ||||||
|     protected CompositeDisposable databaseUpdateReactor; |     protected CompositeDisposable databaseUpdateReactor; | ||||||
|  |  | ||||||
|     protected HistoryRecordManager recordManager; |  | ||||||
|  |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     public BasePlayer(Context context) { |     public BasePlayer(@NonNull final Context context) { | ||||||
|         this.context = context; |         this.context = context; | ||||||
|  |  | ||||||
|         this.broadcastReceiver = new BroadcastReceiver() { |         this.broadcastReceiver = new BroadcastReceiver() { | ||||||
| @@ -162,6 +166,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         this.intentFilter = new IntentFilter(); |         this.intentFilter = new IntentFilter(); | ||||||
|         setupBroadcastReceiver(intentFilter); |         setupBroadcastReceiver(intentFilter); | ||||||
|         context.registerReceiver(broadcastReceiver, intentFilter); |         context.registerReceiver(broadcastReceiver, intentFilter); | ||||||
|  |  | ||||||
|  |         this.recordManager = new HistoryRecordManager(context); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setup() { |     public void setup() { | ||||||
| @@ -172,7 +178,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     public void initPlayer() { |     public void initPlayer() { | ||||||
|         if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); |         if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); | ||||||
|  |  | ||||||
|         if (recordManager == null) recordManager = new HistoryRecordManager(context); |  | ||||||
|         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); |         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); | ||||||
|         databaseUpdateReactor = new CompositeDisposable(); |         databaseUpdateReactor = new CompositeDisposable(); | ||||||
|  |  | ||||||
| @@ -195,13 +200,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|  |  | ||||||
|     public void initListeners() {} |     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) { |     public void handleIntent(Intent intent) { | ||||||
|         if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); |         if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); | ||||||
|         if (intent == null) return; |         if (intent == null) return; | ||||||
| @@ -217,7 +215,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|             int sizeBeforeAppend = playQueue.size(); |             int sizeBeforeAppend = playQueue.size(); | ||||||
|             playQueue.append(queue.getStreams()); |             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); |                 playQueue.setIndex(sizeBeforeAppend); | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -247,24 +246,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         playQueueAdapter = new PlayQueueAdapter(context, playQueue); |         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() { |     public void destroyPlayer() { | ||||||
|         if (DEBUG) Log.d(TAG, "destroyPlayer() called"); |         if (DEBUG) Log.d(TAG, "destroyPlayer() called"); | ||||||
|         if (simpleExoPlayer != null) { |         if (simpleExoPlayer != null) { | ||||||
| @@ -292,7 +273,46 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|  |  | ||||||
|         trackSelector = null; |         trackSelector = null; | ||||||
|         simpleExoPlayer = 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() { |     public void unregisterBroadcastReceiver() { | ||||||
|         if (broadcastReceiver != null && context != null) { |         try { | ||||||
|             context.unregisterReceiver(broadcastReceiver); |             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() { |     public void onPlaying() { | ||||||
|         if (DEBUG) Log.d(TAG, "onPlaying() called"); |         if (DEBUG) Log.d(TAG, "onPlaying() called"); | ||||||
|         if (!isProgressLoopRunning()) startProgressLoop(); |         if (!isProgressLoopRunning()) startProgressLoop(); | ||||||
|  |         if (!isCurrentWindowValid()) seekToDefault(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onBuffering() { |     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 |     // ExoPlayer Listener | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     private void maybeRecover() { |     @Override | ||||||
|         final int currentSourceIndex = playQueue.getIndex(); |     public void onTimelineChanged(Timeline timeline, Object manifest, | ||||||
|         final PlayQueueItem currentSourceItem = playQueue.getItem(); |                                   @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 |         switch (reason) { | ||||||
|         final boolean isCurrentPeriodCorrect = |             case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block | ||||||
|                 simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; |             case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock | ||||||
|  |             case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes | ||||||
|         // Check if recovering |                 if (playQueue != null && playbackManager != null && | ||||||
|         if (isCurrentPeriodCorrect && currentSourceItem != null) { |                         // ensures MediaSourceManager#update is complete | ||||||
|             /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, |                         timeline.getWindowCount() == playQueue.size()) { | ||||||
|              * rounding this position to the nearest second will help alleviate this.*/ |                     playbackManager.load(); | ||||||
|             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); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { |  | ||||||
|         if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { |     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 |     @Override | ||||||
|     public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { |     public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { | ||||||
|         if (DEBUG) Log.d(TAG, "playbackParameters(), speed: " + playbackParameters.speed + |         if (DEBUG) Log.d(TAG, "ExoPlayer - playbackParameters(), " + | ||||||
|                 ", pitch: " + playbackParameters.pitch); |                 "speed: " + playbackParameters.speed + ", " + | ||||||
|  |                 "pitch: " + playbackParameters.pitch); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onLoadingChanged(boolean isLoading) { |     public void onLoadingChanged(final boolean isLoading) { | ||||||
|         if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]"); |         if (DEBUG) Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " + | ||||||
|  |                 "isLoading = [" + isLoading + "]"); | ||||||
|  |  | ||||||
|         if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) stopProgressLoop(); |         if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) { | ||||||
|         else if (isLoading && !isProgressLoopRunning()) startProgressLoop(); |             stopProgressLoop(); | ||||||
|  |         } else if (isLoading && !isProgressLoopRunning()) { | ||||||
|  |             startProgressLoop(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { |     public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { | ||||||
|         if (DEBUG) |         if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " + | ||||||
|             Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]"); |                 "playWhenReady = [" + playWhenReady + "], " + | ||||||
|  |                 "playbackState = [" + playbackState + "]"); | ||||||
|  |  | ||||||
|         if (getCurrentState() == STATE_PAUSED_SEEK) { |         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; |             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}. |      * Processes the exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. | ||||||
|      * There are multiple types of errors: <br><br> |      * There are multiple types of errors: <br><br> | ||||||
|      * |      * | ||||||
|      * {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}: <br><br> |      * {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}: <br><br> | ||||||
|      * 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. |  | ||||||
|      * <br><br> |  | ||||||
|      * |      * | ||||||
|      * {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: <br><br> |      * {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: <br><br> | ||||||
|      * If a runtime error occurred, then we can try to recover it by restarting the playback |      * 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}: <br><br> |      * {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: <br><br> | ||||||
|      * If the renderer failed, treat the error as unrecoverable. |      * If the renderer failed, treat the error as unrecoverable. | ||||||
|      * |      * | ||||||
|  |      * @see #processSourceError(IOException) | ||||||
|      * @see Player.EventListener#onPlayerError(ExoPlaybackException) |      * @see Player.EventListener#onPlayerError(ExoPlaybackException) | ||||||
|      *  */ |      *  */ | ||||||
|     @Override |     @Override | ||||||
|     public void onPlayerError(ExoPlaybackException error) { |     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) { |         if (errorToast != null) { | ||||||
|             errorToast.cancel(); |             errorToast.cancel(); | ||||||
|             errorToast = null; |             errorToast = null; | ||||||
| @@ -612,11 +678,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|  |  | ||||||
|         switch (error.type) { |         switch (error.type) { | ||||||
|             case ExoPlaybackException.TYPE_SOURCE: |             case ExoPlaybackException.TYPE_SOURCE: | ||||||
|                 if (simpleExoPlayer.getCurrentPosition() < |                 processSourceError(error.getSourceException()); | ||||||
|                         simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { |  | ||||||
|                     setRecovery(); |  | ||||||
|                 } |  | ||||||
|                 playQueue.error(isCurrentWindowValid()); |  | ||||||
|                 showStreamError(error); |                 showStreamError(error); | ||||||
|                 break; |                 break; | ||||||
|             case ExoPlaybackException.TYPE_UNEXPECTED: |             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}. | ||||||
|  |      * <br><br> | ||||||
|  |      * 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. | ||||||
|  |      * <br><br> | ||||||
|  |      * 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. | ||||||
|  |      * <br><br> | ||||||
|  |      * 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. | ||||||
|  |      * <br><br> | ||||||
|  |      * 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 |     @Override | ||||||
|     public void onPositionDiscontinuity(int reason) { |     public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { | ||||||
|         if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with reason = [" + reason + "]"); |         if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + | ||||||
|  |                 "reason = [" + reason + "]"); | ||||||
|         // Refresh the playback if there is a transition to the next video |         // Refresh the playback if there is a transition to the next video | ||||||
|         final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); |         final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); | ||||||
|  |  | ||||||
| @@ -645,30 +746,28 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|                 } else { |                 } else { | ||||||
|                     playQueue.offsetIndex(+1); |                     playQueue.offsetIndex(+1); | ||||||
|                 } |                 } | ||||||
|                 playbackManager.load(); |  | ||||||
|                 break; |  | ||||||
|             case DISCONTINUITY_REASON_SEEK: |             case DISCONTINUITY_REASON_SEEK: | ||||||
|             case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: |             case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: | ||||||
|             case DISCONTINUITY_REASON_INTERNAL: |             case DISCONTINUITY_REASON_INTERNAL: | ||||||
|             default: |  | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onRepeatModeChanged(int i) { |     public void onRepeatModeChanged(@Player.RepeatMode final int reason) { | ||||||
|         if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]"); |         if (DEBUG) Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " + | ||||||
|  |                 "mode = [" + reason + "]"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { |     public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { | ||||||
|         if (DEBUG) Log.d(TAG, "onShuffleModeEnabledChanged() called with: " + |         if (DEBUG) Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " + | ||||||
|                 "mode = [" + shuffleModeEnabled + "]"); |                 "mode = [" + shuffleModeEnabled + "]"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onSeekProcessed() { |     public void onSeekProcessed() { | ||||||
|         if (DEBUG) Log.d(TAG, "onSeekProcessed() called"); |         if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); | ||||||
|     } |     } | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Playback Listener |     // Playback Listener | ||||||
| @@ -677,7 +776,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     @Override |     @Override | ||||||
|     public void block() { |     public void block() { | ||||||
|         if (simpleExoPlayer == null) return; |         if (simpleExoPlayer == null) return; | ||||||
|         if (DEBUG) Log.d(TAG, "Blocking..."); |         if (DEBUG) Log.d(TAG, "Playback - block() called"); | ||||||
|  |  | ||||||
|         currentItem = null; |         currentItem = null; | ||||||
|         currentInfo = null; |         currentInfo = null; | ||||||
| @@ -690,12 +789,12 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     @Override |     @Override | ||||||
|     public void unblock(final MediaSource mediaSource) { |     public void unblock(final MediaSource mediaSource) { | ||||||
|         if (simpleExoPlayer == null) return; |         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); |         if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); | ||||||
|  |  | ||||||
|         simpleExoPlayer.prepare(mediaSource); |         simpleExoPlayer.prepare(mediaSource); | ||||||
|         simpleExoPlayer.seekToDefaultPosition(); |         seekToDefault(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -705,7 +804,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         currentItem = item; |         currentItem = item; | ||||||
|         currentInfo = info; |         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; |         if (simpleExoPlayer == null) return; | ||||||
|  |  | ||||||
|         // Check if on wrong window |         // Check if on wrong window | ||||||
| @@ -781,8 +882,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); |         changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); |  | ||||||
|  |  | ||||||
|     public void onVideoPlayPause() { |     public void onVideoPlayPause() { | ||||||
|         if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); |         if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); | ||||||
|  |  | ||||||
| @@ -794,7 +893,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|  |  | ||||||
|         if (getCurrentState() == STATE_COMPLETED) { |         if (getCurrentState() == STATE_COMPLETED) { | ||||||
|             if (playQueue.getIndex() == 0) { |             if (playQueue.getIndex() == 0) { | ||||||
|                 simpleExoPlayer.seekToDefaultPosition(); |                 seekToDefault(); | ||||||
|             } else { |             } else { | ||||||
|                 playQueue.setIndex(0); |                 playQueue.setIndex(0); | ||||||
|             } |             } | ||||||
| @@ -839,11 +938,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onSelected(final PlayQueueItem item) { |     public void onSelected(final PlayQueueItem item) { | ||||||
|  |         if (playQueue == null || simpleExoPlayer == null) return; | ||||||
|  |  | ||||||
|         final int index = playQueue.indexOf(item); |         final int index = playQueue.indexOf(item); | ||||||
|         if (index == -1) return; |         if (index == -1) return; | ||||||
|  |  | ||||||
|         if (playQueue.getIndex() == index) { |         if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { | ||||||
|             simpleExoPlayer.seekToDefaultPosition(); |             seekToDefault(); | ||||||
|         } else { |         } else { | ||||||
|             playQueue.setIndex(index); |             playQueue.setIndex(index); | ||||||
|         } |         } | ||||||
| @@ -875,7 +976,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     private void registerView() { |     private void registerView() { | ||||||
|         if (databaseUpdateReactor == null || recordManager == null || currentInfo == null) return; |         if (databaseUpdateReactor == null || currentInfo == null) return; | ||||||
|         databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() |         databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() | ||||||
|                 .subscribe( |                 .subscribe( | ||||||
|                         ignored -> {/* successful */}, |                         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) { |     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) |         final Disposable stateSaver = recordManager.saveStreamState(info, progress) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .onErrorComplete() |                 .onErrorComplete() | ||||||
|   | |||||||
| @@ -419,13 +419,15 @@ public final class PopupVideoPlayer extends Service { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void onThumbnailReceived(Bitmap thumbnail) { |         public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||||
|             super.onThumbnailReceived(thumbnail); |             super.onLoadingComplete(imageUri, view, loadedImage); | ||||||
|             if (thumbnail != null) { |             if (loadedImage != null) { | ||||||
|                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks |                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks | ||||||
|                 notBuilder = createNotification(); |                 notBuilder = createNotification(); | ||||||
|  |  | ||||||
|                 if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); |                 if (notRemoteView != null) { | ||||||
|  |                     notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 updateNotification(-1); |                 updateNotification(-1); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -160,7 +160,6 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     public VideoPlayer(String debugTag, Context context) { |     public VideoPlayer(String debugTag, Context context) { | ||||||
|         super(context); |         super(context); | ||||||
|         this.TAG = debugTag; |         this.TAG = debugTag; | ||||||
|         this.context = context; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setup(View rootView) { |     public void setup(View rootView) { | ||||||
| @@ -617,9 +616,9 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onThumbnailReceived(Bitmap thumbnail) { |     public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||||
|         super.onThumbnailReceived(thumbnail); |         super.onLoadingComplete(imageUri, view, loadedImage); | ||||||
|         if (thumbnail != null) endScreen.setImageBitmap(thumbnail); |         if (loadedImage != null) endScreen.setImageBitmap(loadedImage); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void onFullScreenButtonClicked() { |     protected void onFullScreenButtonClicked() { | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ import java.util.HashSet; | |||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  |  | ||||||
| import io.reactivex.Single; | import io.reactivex.Single; | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
| @@ -33,6 +34,7 @@ import io.reactivex.disposables.CompositeDisposable; | |||||||
| import io.reactivex.disposables.Disposable; | import io.reactivex.disposables.Disposable; | ||||||
| import io.reactivex.disposables.SerialDisposable; | import io.reactivex.disposables.SerialDisposable; | ||||||
| import io.reactivex.functions.Consumer; | import io.reactivex.functions.Consumer; | ||||||
|  | import io.reactivex.internal.subscriptions.EmptySubscription; | ||||||
| import io.reactivex.subjects.PublishSubject; | import io.reactivex.subjects.PublishSubject; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; | import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; | ||||||
| @@ -40,66 +42,105 @@ import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; | |||||||
| public class MediaSourceManager { | public class MediaSourceManager { | ||||||
|     @NonNull private final static String TAG = "MediaSourceManager"; |     @NonNull private final static String TAG = "MediaSourceManager"; | ||||||
|  |  | ||||||
|     // WINDOW_SIZE determines how many streams AFTER the current stream should be loaded. |     /** | ||||||
|     // The default value (1) ensures seamless playback under typical network settings. |      * Determines how many streams before and after the current stream should be loaded. | ||||||
|  |      * The default value (1) ensures seamless playback under typical network settings. | ||||||
|  |      * <br><br> | ||||||
|  |      * 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; |     private final static int WINDOW_SIZE = 1; | ||||||
|  |  | ||||||
|     @NonNull private final PlaybackListener playbackListener; |     @NonNull private final PlaybackListener playbackListener; | ||||||
|     @NonNull private final PlayQueue playQueue; |     @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, |      * Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing | ||||||
|     // if the item is being played |      * {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure | ||||||
|     private final long expirationTimeMillis; |      * the {@link StreamInfo} used in subsequent playback is up-to-date. | ||||||
|  |      * <br><br> | ||||||
|  |      * 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 |      * Process only the last load order when receiving a stream of load orders (lessens I/O). | ||||||
|     // Not recommended to go below 100ms |      * <br><br> | ||||||
|  |      * The higher it is, the less loading occurs during rapid noncritical timeline changes. | ||||||
|  |      * <br><br> | ||||||
|  |      * Not recommended to go below 100ms. | ||||||
|  |      * | ||||||
|  |      * @see #loadDebounced() | ||||||
|  |      * */ | ||||||
|     private final long loadDebounceMillis; |     private final long loadDebounceMillis; | ||||||
|     @NonNull private final Disposable debouncedLoader; |     @NonNull private final Disposable debouncedLoader; | ||||||
|     @NonNull private final PublishSubject<Long> debouncedSignal; |     @NonNull private final PublishSubject<Long> 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<PlayQueueItem> loadingItems; | ||||||
|  |     @NonNull private final SerialDisposable syncReactor; | ||||||
|  |  | ||||||
|     private boolean isBlocked; |     @NonNull private final AtomicBoolean isBlocked; | ||||||
|  |  | ||||||
|     private SerialDisposable syncReactor; |     @NonNull private DynamicConcatenatingMediaSource sources; | ||||||
|     private PlayQueueItem syncedItem; |  | ||||||
|     private Set<PlayQueueItem> loadingItems; |     @Nullable private PlayQueueItem syncedItem; | ||||||
|  |  | ||||||
|     public MediaSourceManager(@NonNull final PlaybackListener listener, |     public MediaSourceManager(@NonNull final PlaybackListener listener, | ||||||
|                               @NonNull final PlayQueue playQueue) { |                               @NonNull final PlayQueue playQueue) { | ||||||
|         this(listener, playQueue, |         this(listener, playQueue, | ||||||
|                 /*loadDebounceMillis=*/400L, |                 /*loadDebounceMillis=*/400L, | ||||||
|                 /*expirationTimeMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES)); |                 /*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private MediaSourceManager(@NonNull final PlaybackListener listener, |     private MediaSourceManager(@NonNull final PlaybackListener listener, | ||||||
|                                @NonNull final PlayQueue playQueue, |                                @NonNull final PlayQueue playQueue, | ||||||
|                                final long loadDebounceMillis, |                                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.playbackListener = listener; | ||||||
|         this.playQueue = playQueue; |         this.playQueue = playQueue; | ||||||
|         this.loadDebounceMillis = loadDebounceMillis; |  | ||||||
|         this.expirationTimeMillis = expirationTimeMillis; |  | ||||||
|  |  | ||||||
|         this.loaderReactor = new CompositeDisposable(); |         this.windowRefreshTimeMillis = windowRefreshTimeMillis; | ||||||
|  |  | ||||||
|  |         this.loadDebounceMillis = loadDebounceMillis; | ||||||
|         this.debouncedSignal = PublishSubject.create(); |         this.debouncedSignal = PublishSubject.create(); | ||||||
|         this.debouncedLoader = getDebouncedLoader(); |         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.sources = new DynamicConcatenatingMediaSource(); | ||||||
|  |  | ||||||
|         this.syncReactor = new SerialDisposable(); |  | ||||||
|         this.loadingItems = Collections.synchronizedSet(new HashSet<>()); |         this.loadingItems = Collections.synchronizedSet(new HashSet<>()); | ||||||
|  |  | ||||||
|         if (playQueue.getBroadcastReceiver() != null) { |         playQueue.getBroadcastReceiver() | ||||||
|             playQueue.getBroadcastReceiver() |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                     .observeOn(AndroidSchedulers.mainThread()) |                 .subscribe(getReactor()); | ||||||
|                     .subscribe(getReactor()); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -114,16 +155,12 @@ public class MediaSourceManager { | |||||||
|         debouncedSignal.onComplete(); |         debouncedSignal.onComplete(); | ||||||
|         debouncedLoader.dispose(); |         debouncedLoader.dispose(); | ||||||
|  |  | ||||||
|         if (playQueueReactor != null) playQueueReactor.cancel(); |         playQueueReactor.cancel(); | ||||||
|         if (loaderReactor != null) loaderReactor.dispose(); |         loaderReactor.dispose(); | ||||||
|         if (syncReactor != null) syncReactor.dispose(); |         syncReactor.dispose(); | ||||||
|         if (sources != null) sources.releaseSource(); |         sources.releaseSource(); | ||||||
|  |  | ||||||
|         playQueueReactor = null; |  | ||||||
|         loaderReactor = null; |  | ||||||
|         syncReactor = null; |  | ||||||
|         syncedItem = null; |         syncedItem = null; | ||||||
|         sources = null; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -158,14 +195,14 @@ public class MediaSourceManager { | |||||||
|         return new Subscriber<PlayQueueEvent>() { |         return new Subscriber<PlayQueueEvent>() { | ||||||
|             @Override |             @Override | ||||||
|             public void onSubscribe(@NonNull Subscription d) { |             public void onSubscribe(@NonNull Subscription d) { | ||||||
|                 if (playQueueReactor != null) playQueueReactor.cancel(); |                 playQueueReactor.cancel(); | ||||||
|                 playQueueReactor = d; |                 playQueueReactor = d; | ||||||
|                 playQueueReactor.request(1); |                 playQueueReactor.request(1); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onNext(@NonNull PlayQueueEvent playQueueMessage) { |             public void onNext(@NonNull PlayQueueEvent playQueueMessage) { | ||||||
|                 if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage); |                 onPlayQueueChanged(playQueueMessage); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
| @@ -227,7 +264,7 @@ public class MediaSourceManager { | |||||||
|             tryBlock(); |             tryBlock(); | ||||||
|             playQueue.fetch(); |             playQueue.fetch(); | ||||||
|         } |         } | ||||||
|         if (playQueueReactor != null) playQueueReactor.request(1); |         playQueueReactor.request(1); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -240,7 +277,7 @@ public class MediaSourceManager { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean isPlaybackReady() { |     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 MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); | ||||||
|         final PlayQueueItem playQueueItem = playQueue.getItem(); |         final PlayQueueItem playQueueItem = playQueue.getItem(); | ||||||
| @@ -256,19 +293,19 @@ public class MediaSourceManager { | |||||||
|     private void tryBlock() { |     private void tryBlock() { | ||||||
|         if (DEBUG) Log.d(TAG, "tryBlock() called."); |         if (DEBUG) Log.d(TAG, "tryBlock() called."); | ||||||
|  |  | ||||||
|         if (isBlocked) return; |         if (isBlocked.get()) return; | ||||||
|  |  | ||||||
|         playbackListener.block(); |         playbackListener.block(); | ||||||
|         resetSources(); |         resetSources(); | ||||||
|  |  | ||||||
|         isBlocked = true; |         isBlocked.set(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void tryUnblock() { |     private void tryUnblock() { | ||||||
|         if (DEBUG) Log.d(TAG, "tryUnblock() called."); |         if (DEBUG) Log.d(TAG, "tryUnblock() called."); | ||||||
|  |  | ||||||
|         if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) { |         if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) { | ||||||
|             isBlocked = false; |             isBlocked.set(false); | ||||||
|             playbackListener.unblock(sources); |             playbackListener.unblock(sources); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -281,7 +318,7 @@ public class MediaSourceManager { | |||||||
|         if (DEBUG) Log.d(TAG, "sync() called."); |         if (DEBUG) Log.d(TAG, "sync() called."); | ||||||
|  |  | ||||||
|         final PlayQueueItem currentItem = playQueue.getItem(); |         final PlayQueueItem currentItem = playQueue.getItem(); | ||||||
|         if (isBlocked || currentItem == null) return; |         if (isBlocked.get() || currentItem == null) return; | ||||||
|  |  | ||||||
|         final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); |         final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); | ||||||
|         final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null); |         final Consumer<Throwable> 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) { |                               @Nullable final StreamInfo info) { | ||||||
|         // Ensure the current item is up to date with the play queue |         // Ensure the current item is up to date with the play queue | ||||||
|         if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { |         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 int currentIndex = playQueue.getIndex(); | ||||||
|         final PlayQueueItem currentItem = playQueue.getItem(currentIndex); |         final PlayQueueItem currentItem = playQueue.getItem(currentIndex); | ||||||
|         if (currentItem == null) return; |         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); |         maybeLoadItem(currentItem); | ||||||
|  |  | ||||||
|         // The rest are just for seamless playback |         // The rest are just for seamless playback | ||||||
| @@ -347,34 +390,17 @@ public class MediaSourceManager { | |||||||
|  |  | ||||||
|     private void maybeLoadItem(@NonNull final PlayQueueItem item) { |     private void maybeLoadItem(@NonNull final PlayQueueItem item) { | ||||||
|         if (DEBUG) Log.d(TAG, "maybeLoadItem() called."); |         if (DEBUG) Log.d(TAG, "maybeLoadItem() called."); | ||||||
|         if (sources == null) return; |         if (playQueue.indexOf(item) >= sources.getSize()) return; | ||||||
|  |  | ||||||
|         final int index = playQueue.indexOf(item); |  | ||||||
|         if (index > sources.getSize() - 1) return; |  | ||||||
|  |  | ||||||
|         final Consumer<ManagedMediaSource> 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 (!loadingItems.contains(item) && isCorrectionNeeded(item)) { |         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()); |                     "] with url: " + item.getUrl()); | ||||||
|  |  | ||||||
|             loadingItems.add(item); |             loadingItems.add(item); | ||||||
|             final Disposable loader = getLoadedMediaSource(item) |             final Disposable loader = getLoadedMediaSource(item) | ||||||
|                     .observeOn(AndroidSchedulers.mainThread()) |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                     .subscribe(onDone); |                     /* No exception handling since getLoadedMediaSource guarantees nonnull return */ | ||||||
|  |                     .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); | ||||||
|             loaderReactor.add(loader); |             loaderReactor.add(loader); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -392,14 +418,32 @@ public class MediaSourceManager { | |||||||
|                                 ", audio count: " + streamInfo.audio_streams.size() + |                                 ", audio count: " + streamInfo.audio_streams.size() + | ||||||
|                                 ", video count: " + streamInfo.video_only_streams.size() + |                                 ", video count: " + streamInfo.video_only_streams.size() + | ||||||
|                                 streamInfo.video_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); |             return new LoadedMediaSource(source, stream, expiration); | ||||||
|         }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); |         }).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} |      * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource} | ||||||
|      * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback |      * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback | ||||||
| @@ -411,8 +455,6 @@ public class MediaSourceManager { | |||||||
|      * {@link ManagedMediaSource}. |      * {@link ManagedMediaSource}. | ||||||
|      * */ |      * */ | ||||||
|     private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { |     private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { | ||||||
|         if (sources == null) return false; |  | ||||||
|  |  | ||||||
|         final int index = playQueue.indexOf(item); |         final int index = playQueue.indexOf(item); | ||||||
|         if (index == -1 || index >= sources.getSize()) return false; |         if (index == -1 || index >= sources.getSize()) return false; | ||||||
|  |  | ||||||
| @@ -432,13 +474,13 @@ public class MediaSourceManager { | |||||||
|     private void resetSources() { |     private void resetSources() { | ||||||
|         if (DEBUG) Log.d(TAG, "resetSources() called."); |         if (DEBUG) Log.d(TAG, "resetSources() called."); | ||||||
|  |  | ||||||
|         if (this.sources != null) this.sources.releaseSource(); |         this.sources.releaseSource(); | ||||||
|         this.sources = new DynamicConcatenatingMediaSource(); |         this.sources = new DynamicConcatenatingMediaSource(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void populateSources() { |     private void populateSources() { | ||||||
|         if (DEBUG) Log.d(TAG, "populateSources() called."); |         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++) { |         for (int index = sources.getSize() - 1; index < playQueue.size(); index++) { | ||||||
|             emplace(index, new PlaceholderMediaSource()); |             emplace(index, new PlaceholderMediaSource()); | ||||||
| @@ -451,12 +493,11 @@ public class MediaSourceManager { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource} |      * 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. |      * already exists at the given index. | ||||||
|      * */ |      * */ | ||||||
|     private void emplace(final int index, @NonNull final MediaSource source) { |     private synchronized void emplace(final int index, @NonNull final MediaSource source) { | ||||||
|         if (sources == null) return; |         if (index < sources.getSize()) return; | ||||||
|         if (index < 0 || index < sources.getSize()) return; |  | ||||||
|  |  | ||||||
|         sources.addMediaSource(index, source); |         sources.addMediaSource(index, source); | ||||||
|     } |     } | ||||||
| @@ -465,8 +506,7 @@ public class MediaSourceManager { | |||||||
|      * Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource} |      * Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource} | ||||||
|      * at the given index. If this index is out of bound, then the removal is ignored. |      * at the given index. If this index is out of bound, then the removal is ignored. | ||||||
|      * */ |      * */ | ||||||
|     private void remove(final int index) { |     private synchronized void remove(final int index) { | ||||||
|         if (sources == null) return; |  | ||||||
|         if (index < 0 || index > sources.getSize()) return; |         if (index < 0 || index > sources.getSize()) return; | ||||||
|  |  | ||||||
|         sources.removeMediaSource(index); |         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, |      * from the given source index to the target index. If either index is out of bound, | ||||||
|      * then the call is ignored. |      * then the call is ignored. | ||||||
|      * */ |      * */ | ||||||
|     private void move(final int source, final int target) { |     private synchronized void move(final int source, final int target) { | ||||||
|         if (sources == null) return; |  | ||||||
|         if (source < 0 || target < 0) return; |         if (source < 0 || target < 0) return; | ||||||
|         if (source >= sources.getSize() || target >= sources.getSize()) return; |         if (source >= sources.getSize() || target >= sources.getSize()) return; | ||||||
|  |  | ||||||
| @@ -491,15 +530,13 @@ public class MediaSourceManager { | |||||||
|      * then the replacement is ignored. |      * then the replacement is ignored. | ||||||
|      * <br><br> |      * <br><br> | ||||||
|      * Not recommended to use on indices LESS THAN the currently playing index, since |      * 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}. |      * on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}. | ||||||
|      * */ |      * */ | ||||||
|     private synchronized void update(final int index, @NonNull final MediaSource source) { |     private synchronized void update(final int index, @NonNull final MediaSource source) { | ||||||
|         if (sources == null) return; |  | ||||||
|         if (index < 0 || index >= sources.getSize()) return; |         if (index < 0 || index >= sources.getSize()) return; | ||||||
|  |  | ||||||
|         sources.addMediaSource(index + 1, source, () -> { |         sources.addMediaSource(index + 1, source, () -> | ||||||
|             if (sources != null) sources.removeMediaSource(index); |                 sources.removeMediaSource(index)); | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -63,22 +63,18 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public PlayQueueAdapter(final Context context, final PlayQueue playQueue) { |     public PlayQueueAdapter(final Context context, final PlayQueue playQueue) { | ||||||
|  |         if (playQueue.getBroadcastReceiver() == null) { | ||||||
|  |             throw new IllegalStateException("Play Queue has not been initialized."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         this.playQueueItemBuilder = new PlayQueueItemBuilder(context); |         this.playQueueItemBuilder = new PlayQueueItemBuilder(context); | ||||||
|         this.playQueue = playQueue; |         this.playQueue = playQueue; | ||||||
|  |  | ||||||
|         startReactor(); |         playQueue.getBroadcastReceiver().toObservable().subscribe(getReactor()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) { |     private Observer<PlayQueueEvent> getReactor() { | ||||||
|         playQueueItemBuilder.setOnSelectedListener(listener); |         return new Observer<PlayQueueEvent>() { | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void unsetSelectedListener() { |  | ||||||
|         playQueueItemBuilder.setOnSelectedListener(null); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void startReactor() { |  | ||||||
|         final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() { |  | ||||||
|             @Override |             @Override | ||||||
|             public void onSubscribe(@NonNull Disposable d) { |             public void onSubscribe(@NonNull Disposable d) { | ||||||
|                 if (playQueueReactor != null) playQueueReactor.dispose(); |                 if (playQueueReactor != null) playQueueReactor.dispose(); | ||||||
| @@ -99,9 +95,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | |||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         if (playQueue.getBroadcastReceiver() != null) { |  | ||||||
|             playQueue.getBroadcastReceiver().toObservable().subscribe(observer); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void onPlayQueueChanged(final PlayQueueEvent message) { |     private void onPlayQueueChanged(final PlayQueueEvent message) { | ||||||
| @@ -148,6 +141,14 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | |||||||
|         playQueueReactor = null; |         playQueueReactor = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) { | ||||||
|  |         playQueueItemBuilder.setOnSelectedListener(listener); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void unsetSelectedListener() { | ||||||
|  |         playQueueItemBuilder.setOnSelectedListener(null); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public void setFooter(View footer) { |     public void setFooter(View footer) { | ||||||
|         this.footer = footer; |         this.footer = footer; | ||||||
|         notifyItemChanged(playQueue.size()); |         notifyItemChanged(playQueue.size()); | ||||||
|   | |||||||
| @@ -172,7 +172,7 @@ public final class ExtractorHelper { | |||||||
|                                                          String url, |                                                          String url, | ||||||
|                                                          Single<I> loadFromNetwork) { |                                                          Single<I> loadFromNetwork) { | ||||||
|         checkServiceId(serviceId); |         checkServiceId(serviceId); | ||||||
|         loadFromNetwork = loadFromNetwork.doOnSuccess((@NonNull I i) -> cache.putInfo(i)); |         loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info)); | ||||||
|  |  | ||||||
|         Single<I> load; |         Single<I> load; | ||||||
|         if (forceLoad) { |         if (forceLoad) { | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ | |||||||
| package org.schabi.newpipe.util; | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
| import android.support.v4.util.LruCache; | import android.support.v4.util.LruCache; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| @@ -29,6 +30,8 @@ import org.schabi.newpipe.extractor.Info; | |||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; | ||||||
|  |  | ||||||
|  |  | ||||||
| public final class InfoCache { | public final class InfoCache { | ||||||
|     private static final boolean DEBUG = MainActivity.DEBUG; |     private static final boolean DEBUG = MainActivity.DEBUG; | ||||||
| @@ -52,6 +55,7 @@ public final class InfoCache { | |||||||
|         return instance; |         return instance; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|     public Info getFromKey(int serviceId, @NonNull String url) { |     public Info getFromKey(int serviceId, @NonNull String url) { | ||||||
|         if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); |         if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); | ||||||
|         synchronized (lruCache) { |         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 + "]"); |         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) { |         final long expirationMillis; | ||||||
|         if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]"); |         if (info.getServiceId() == SoundCloud.getServiceId()) { | ||||||
|  |             expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES); | ||||||
|  |         } else { | ||||||
|  |             expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         synchronized (lruCache) { |         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) { |     @NonNull | ||||||
|         return keyOf(info.getServiceId(), info.getUrl()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static String keyOf(final int serviceId, @NonNull final String url) { |     private static String keyOf(final int serviceId, @NonNull final String url) { | ||||||
|         return serviceId + url; |         return serviceId + url; | ||||||
|     } |     } | ||||||
| @@ -119,6 +121,7 @@ public final class InfoCache { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|     private static Info getInfo(@NonNull final LruCache<String, CacheData> cache, |     private static Info getInfo(@NonNull final LruCache<String, CacheData> cache, | ||||||
|                                 @NonNull final String key) { |                                 @NonNull final String key) { | ||||||
|         final CacheData data = cache.get(key); |         final CacheData data = cache.get(key); | ||||||
| @@ -136,12 +139,8 @@ public final class InfoCache { | |||||||
|         final private long expireTimestamp; |         final private long expireTimestamp; | ||||||
|         final private Info info; |         final private Info info; | ||||||
|  |  | ||||||
|         private CacheData(@NonNull final Info info, |         private CacheData(@NonNull final Info info, final long timeoutMillis) { | ||||||
|                           final long timeout, |             this.expireTimestamp = System.currentTimeMillis() + timeoutMillis; | ||||||
|                           @NonNull final TimeUnit timeUnit) { |  | ||||||
|             this.expireTimestamp = System.currentTimeMillis() + |  | ||||||
|                     TimeUnit.MILLISECONDS.convert(timeout, timeUnit); |  | ||||||
|  |  | ||||||
|             this.info = info; |             this.info = info; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 John Zhen Mo
					John Zhen Mo