mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-24 00:50:32 +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:
parent
9ea08c8a4b
commit
0c17f0825b
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user