diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java
index 5ea067d00..8baabed6b 100644
--- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java
+++ b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java
@@ -1,25 +1,40 @@
package org.schabi.newpipe;
import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
import org.schabi.newpipe.extractor.NewPipe;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ImageDownloader extends BaseImageDownloader {
+ private static final ByteArrayInputStream DUMMY_INPUT_STREAM =
+ new ByteArrayInputStream(new byte[]{});
+
+ private final SharedPreferences preferences;
+ private final String downloadThumbnailKey;
+
public ImageDownloader(Context context) {
super(context);
+ this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
}
- public ImageDownloader(Context context, int connectTimeout, int readTimeout) {
- super(context, connectTimeout, readTimeout);
+ private boolean isDownloadingThumbnail() {
+ return preferences.getBoolean(downloadThumbnailKey, true);
}
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
- Downloader downloader = (Downloader) NewPipe.getDownloader();
- return downloader.stream(imageUri);
+ if (isDownloadingThumbnail()) {
+ final Downloader downloader = (Downloader) NewPipe.getDownloader();
+ return downloader.stream(imageUri);
+ } else {
+ return DUMMY_INPUT_STREAM;
+ }
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
index cee885e22..5355e19ee 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -57,6 +57,7 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.LoadController;
@@ -244,6 +245,7 @@ public abstract class BasePlayer implements
playQueue = queue;
playQueue.init();
+ if (playbackManager != null) playbackManager.dispose();
playbackManager = new MediaSourceManager(this, playQueue);
if (playQueueAdapter != null) playQueueAdapter.dispose();
@@ -272,7 +274,6 @@ public abstract class BasePlayer implements
public void destroy() {
if (DEBUG) Log.d(TAG, "destroy() called");
destroyPlayer();
- clearThumbnailCache();
unregisterBroadcastReceiver();
trackSelector = null;
@@ -314,11 +315,6 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " +
"imageUri = [" + imageUri + "], view = [" + view + "]");
}
-
- protected void clearThumbnailCache() {
- ImageLoader.getInstance().clearMemoryCache();
- }
-
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Building
//////////////////////////////////////////////////////////////////////////*/
@@ -448,7 +444,6 @@ public abstract class BasePlayer implements
public void onPlaying() {
if (DEBUG) Log.d(TAG, "onPlaying() called");
if (!isProgressLoopRunning()) startProgressLoop();
- if (!isCurrentWindowValid()) seekToDefault();
}
public void onBuffering() {}
@@ -522,11 +517,9 @@ public abstract class BasePlayer implements
);
}
-
private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
- .filter(ignored -> isProgressLoopRunning())
.subscribe(ignored -> triggerProgressUpdate());
}
@@ -541,16 +534,19 @@ public abstract class BasePlayer implements
(manifest == null ? "no manifest" : "available manifest") + ", " +
"timeline size = [" + timeline.getWindowCount() + "], " +
"reason = [" + reason + "]");
+ if (playQueue == null) return;
switch (reason) {
case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block
case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock
case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes
- if (playQueue != null && playbackManager != null &&
- // ensures MediaSourceManager#update is complete
- timeline.getWindowCount() == playQueue.size()) {
- playbackManager.load();
+ // ensures MediaSourceManager#update is complete
+ final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size();
+ // Ensure dynamic/livestream timeline changes does not cause negative position
+ if (isPlaylistStable && !isCurrentWindowValid()) {
+ simpleExoPlayer.seekTo(/*clampToMillis=*/0);
}
+ break;
}
}
@@ -775,6 +771,16 @@ public abstract class BasePlayer implements
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
+ @Override
+ public boolean isNearPlaybackEdge(final long timeToEndMillis) {
+ // If live, then not near playback edge
+ if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false;
+
+ final long currentPositionMillis = simpleExoPlayer.getCurrentPosition();
+ final long currentDurationMillis = simpleExoPlayer.getDuration();
+ return currentDurationMillis - currentPositionMillis < timeToEndMillis;
+ }
+
@Override
public void onPlaybackBlock() {
if (simpleExoPlayer == null) return;
@@ -796,7 +802,6 @@ public abstract class BasePlayer implements
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
simpleExoPlayer.prepare(mediaSource);
- seekToDefault();
}
@Override
@@ -825,16 +830,24 @@ public abstract class BasePlayer implements
if (simpleExoPlayer == null) return;
final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
+ final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
// Check if on wrong window
if (currentPlayQueueIndex != playQueue.getIndex()) {
- Log.e(TAG, "Play Queue may be desynchronized: item " +
+ Log.e(TAG, "Playback - Play Queue may be desynchronized: item " +
"index=[" + currentPlayQueueIndex + "], " +
"queue index=[" + playQueue.getIndex() + "]");
- // on metadata changed
+ // Check if bad seek position
+ } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex > currentPlaylistSize) ||
+ currentPlaylistIndex < 0) {
+ Log.e(TAG, "Playback - Trying to seek to " +
+ "index=[" + currentPlayQueueIndex + "] with " +
+ "playlist length=[" + currentPlaylistSize + "]");
+
+ // If not playing correct stream, change window position
} else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) {
final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET;
- if (DEBUG) Log.d(TAG, "Rewinding to correct" +
+ if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" +
" window=[" + currentPlayQueueIndex + "]," +
" at=[" + getTimeString((int)startPos) + "]," +
" from=[" + simpleExoPlayer.getCurrentWindowIndex() + "].");
@@ -858,6 +871,11 @@ public abstract class BasePlayer implements
@Nullable
@Override
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
+ final StreamType streamType = info.getStreamType();
+ if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) {
+ return null;
+ }
+
if (!info.getHlsUrl().isEmpty()) {
return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS);
} else if (!info.getDashMpdUrl().isEmpty()) {
@@ -909,6 +927,9 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
if (playWhenReady) audioReactor.requestAudioFocus();
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
+
+ // On live prepared
+ if (simpleExoPlayer.isCurrentWindowDynamic()) seekToDefault();
}
public void onVideoPlayPause() {
@@ -945,14 +966,15 @@ public abstract class BasePlayer implements
if (simpleExoPlayer == null || playQueue == null) return;
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
- savePlaybackState();
-
- /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track.
- * Also restart the track if the current track is the first in a queue.*/
- if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) {
- final long startPos = currentInfo == null ? 0 : currentInfo.getStartPosition();
- simpleExoPlayer.seekTo(startPos);
+ /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds,
+ * restart current track. Also restart the track if the current track
+ * is the first in a queue.*/
+ if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT ||
+ playQueue.getIndex() == 0) {
+ seekToDefault();
+ playQueue.offsetIndex(0);
} else {
+ savePlaybackState();
playQueue.offsetIndex(-1);
}
}
@@ -962,7 +984,6 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "onPlayNext() called");
savePlaybackState();
-
playQueue.offsetIndex(+1);
}
@@ -975,8 +996,9 @@ public abstract class BasePlayer implements
if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) {
seekToDefault();
} else {
- playQueue.setIndex(index);
+ savePlaybackState();
}
+ playQueue.setIndex(index);
}
public void seekBy(int milliSeconds) {
@@ -1015,8 +1037,11 @@ public abstract class BasePlayer implements
protected void reload() {
if (playbackManager != null) {
- playbackManager.reset();
- playbackManager.load();
+ playbackManager.dispose();
+ }
+
+ if (playQueue != null) {
+ playbackManager = new MediaSourceManager(this, playQueue);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
index 4f27d1fee..dd7e0c71e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
@@ -62,6 +62,7 @@ import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
+import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
@@ -76,6 +77,8 @@ import java.util.UUID;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
+import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
+import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
@@ -110,7 +113,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
- changeSystemUi();
+ hideSystemUi();
setContentView(R.layout.activity_main_player);
playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(findViewById(android.R.id.content));
@@ -597,28 +600,27 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
updatePlaybackButtons();
getControlsRoot().setVisibility(View.INVISIBLE);
- queueLayout.setVisibility(View.VISIBLE);
+ animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true,
+ DEFAULT_CONTROLS_DURATION);
itemsList.scrollToPosition(playQueue.getIndex());
}
private void onQueueClosed() {
- queueLayout.setVisibility(View.GONE);
+ animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false,
+ DEFAULT_CONTROLS_DURATION);
queueVisible = false;
}
private void onMoreOptionsClicked() {
if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called");
- if (secondaryControls.getVisibility() == View.VISIBLE) {
- moreOptionsButton.setImageDrawable(getResources().getDrawable(
- R.drawable.ic_expand_more_white_24dp));
- animateView(secondaryControls, false, 200);
- } else {
- moreOptionsButton.setImageDrawable(getResources().getDrawable(
- R.drawable.ic_expand_less_white_24dp));
- animateView(secondaryControls, true, 200);
- }
+ final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE;
+
+ animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION,
+ isMoreControlsVisible ? 0 : 180);
+ animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible,
+ DEFAULT_CONTROLS_DURATION);
showControls(DEFAULT_CONTROLS_DURATION);
}
@@ -696,7 +698,6 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
animatePlayButtons(true, 200);
});
- changeSystemUi();
getRootView().setKeepScreenOn(true);
}
@@ -798,31 +799,11 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
- return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
+ return new PlayQueueItemTouchCallback() {
@Override
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
- if (source.getItemViewType() != target.getItemViewType()) {
- return false;
- }
-
- final int sourceIndex = source.getLayoutPosition();
- final int targetIndex = target.getLayoutPosition();
- playQueue.move(sourceIndex, targetIndex);
- return true;
+ public void onMove(int sourceIndex, int targetIndex) {
+ if (playQueue != null) playQueue.move(sourceIndex, targetIndex);
}
-
- @Override
- public boolean isLongPressDragEnabled() {
- return false;
- }
-
- @Override
- public boolean isItemViewSwipeEnabled() {
- return false;
- }
-
- @Override
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
};
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
index c68133094..1c3ffe911 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
@@ -34,6 +34,7 @@ import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
+import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
@@ -61,9 +62,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
- private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
- private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
-
private View rootView;
private RecyclerView itemsList;
@@ -398,43 +396,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
- return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
+ return new PlayQueueItemTouchCallback() {
@Override
- public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
- int viewSizeOutOfBounds, int totalSize,
- long msSinceStartScroll) {
- final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
- viewSizeOutOfBounds, totalSize, msSinceStartScroll);
- final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
- Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
- return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
- }
-
- @Override
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
- RecyclerView.ViewHolder target) {
- if (source.getItemViewType() != target.getItemViewType()) {
- return false;
- }
-
- final int sourceIndex = source.getLayoutPosition();
- final int targetIndex = target.getLayoutPosition();
+ public void onMove(int sourceIndex, int targetIndex) {
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
- return true;
}
-
- @Override
- public boolean isLongPressDragEnabled() {
- return false;
- }
-
- @Override
- public boolean isItemViewSwipeEnabled() {
- return false;
- }
-
- @Override
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
};
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
index ea13a28e7..50c069b40 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
@@ -21,15 +21,15 @@ import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent;
+import org.schabi.newpipe.util.ServiceHelper;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
@@ -42,7 +42,7 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager {
- @NonNull private final static String TAG = "MediaSourceManager";
+ @NonNull private final String TAG = "MediaSourceManager@" + hashCode();
/**
* Determines how many streams before and after the current stream should be loaded.
@@ -60,17 +60,18 @@ public class MediaSourceManager {
@NonNull private final PlayQueue playQueue;
/**
- * Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing
- * {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure
- * the {@link StreamInfo} used in subsequent playback is up-to-date.
- *
- * Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to
- * replace the expired one on whereupon {@link #loadImmediate()} is called.
+ * Determines the gap time between the playback position and the playback duration which
+ * the {@link #getEdgeIntervalSignal()} begins to request loading.
*
- * @see #loadImmediate()
- * @see #isCorrectionNeeded(PlayQueueItem)
+ * @see #progressUpdateIntervalMillis
* */
- private final long windowRefreshTimeMillis;
+ private final long playbackNearEndGapMillis;
+ /**
+ * Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between
+ * each request for loading, once {@link #playbackNearEndGapMillis} has reached.
+ * */
+ private final long progressUpdateIntervalMillis;
+ @NonNull private final Observable nearEndIntervalSignal;
/**
* Process only the last load order when receiving a stream of load orders (lessens I/O).
@@ -106,23 +107,31 @@ public class MediaSourceManager {
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
- this(listener, playQueue,
- /*loadDebounceMillis=*/400L,
- /*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES));
+ this(listener, playQueue, /*loadDebounceMillis=*/400L,
+ /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS),
+ /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS));
}
private MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue,
final long loadDebounceMillis,
- final long windowRefreshTimeMillis) {
+ final long playbackNearEndGapMillis,
+ final long progressUpdateIntervalMillis) {
if (playQueue.getBroadcastReceiver() == null) {
throw new IllegalArgumentException("Play Queue has not been initialized.");
}
+ if (playbackNearEndGapMillis < progressUpdateIntervalMillis) {
+ throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis +
+ " ms] must be longer than update interval=[ " + progressUpdateIntervalMillis +
+ " ms] for them to be useful.");
+ }
this.playbackListener = listener;
this.playQueue = playQueue;
- this.windowRefreshTimeMillis = windowRefreshTimeMillis;
+ this.playbackNearEndGapMillis = playbackNearEndGapMillis;
+ this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
+ this.nearEndIntervalSignal = getEdgeIntervalSignal();
this.loadDebounceMillis = loadDebounceMillis;
this.debouncedSignal = PublishSubject.create();
@@ -161,28 +170,6 @@ public class MediaSourceManager {
sources.releaseSource();
}
- /**
- * Loads the current playing stream and the streams within its windowSize bound.
- *
- * Unblocks the player once the item at the current index is loaded.
- * */
- public void load() {
- if (DEBUG) Log.d(TAG, "load() called.");
- loadDebounced();
- }
-
- /**
- * Blocks the player and repopulate the sources.
- *
- * Does not ensure the player is unblocked and should be done explicitly
- * through {@link #load() load}.
- * */
- public void reset() {
- if (DEBUG) Log.d(TAG, "reset() called.");
-
- maybeBlock();
- populateSources();
- }
/*//////////////////////////////////////////////////////////////////////////
// Event Reactor
//////////////////////////////////////////////////////////////////////////*/
@@ -219,11 +206,13 @@ public class MediaSourceManager {
switch (event.type()) {
case INIT:
case ERROR:
- reset();
- break;
+ maybeBlock();
case APPEND:
populateSources();
break;
+ case SELECT:
+ maybeRenewCurrentIndex();
+ break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex());
@@ -238,7 +227,6 @@ public class MediaSourceManager {
final ReorderEvent reorderEvent = (ReorderEvent) event;
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex());
break;
- case SELECT:
case RECOVERY:
default:
break;
@@ -347,8 +335,13 @@ public class MediaSourceManager {
// MediaSource Loading
//////////////////////////////////////////////////////////////////////////*/
+ private Observable getEdgeIntervalSignal() {
+ return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
+ .filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis));
+ }
+
private Disposable getDebouncedLoader() {
- return debouncedSignal
+ return debouncedSignal.mergeWith(nearEndIntervalSignal)
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate());
@@ -359,13 +352,14 @@ public class MediaSourceManager {
}
private void loadImmediate() {
+ if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return;
// Evict the items being loaded to free up memory
- if (!loadingItems.contains(currentItem) && loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
+ if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
loaderReactor.clear();
loadingItems.clear();
}
@@ -377,7 +371,7 @@ public class MediaSourceManager {
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightLimit = currentIndex + WINDOW_SIZE + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
- final List items = new ArrayList<>(
+ final Set items = new HashSet<>(
playQueue.getStreams().subList(leftBound,rightBound));
// Do a round robin
@@ -385,6 +379,7 @@ public class MediaSourceManager {
if (excess >= 0) {
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
}
+ items.remove(currentItem);
for (final PlayQueueItem item : items) {
maybeLoadItem(item);
@@ -405,9 +400,9 @@ public class MediaSourceManager {
/* No exception handling since getLoadedMediaSource guarantees nonnull return */
.subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource));
loaderReactor.add(loader);
+ } else {
+ maybeSynchronizePlayer();
}
-
- maybeSynchronizePlayer();
}
private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
@@ -423,7 +418,8 @@ public class MediaSourceManager {
return new FailedMediaSource(stream, exception);
}
- final long expiration = System.currentTimeMillis() + windowRefreshTimeMillis;
+ final long expiration = System.currentTimeMillis() +
+ ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
return new LoadedMediaSource(source, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
}
@@ -467,6 +463,24 @@ public class MediaSourceManager {
}
}
+ /**
+ * Checks if the current playing index contains an expired {@link ManagedMediaSource}.
+ * If so, the expired source is replaced by a {@link PlaceholderMediaSource} and
+ * {@link #loadImmediate()} is called to reload the current item.
+ * */
+ private void maybeRenewCurrentIndex() {
+ final int currentIndex = playQueue.getIndex();
+ if (sources.getSize() <= currentIndex) return;
+
+ final ManagedMediaSource currentSource =
+ (ManagedMediaSource) sources.getMediaSource(currentIndex);
+ final PlayQueueItem currentItem = playQueue.getItem();
+ if (!currentSource.canReplace(currentItem)) return;
+
+ if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
+ "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
+ update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate);
+ }
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Helpers
//////////////////////////////////////////////////////////////////////////*/
@@ -476,6 +490,7 @@ public class MediaSourceManager {
this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource(false,
+ // Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
new ShuffleOrder.UnshuffledShuffleOrder(0));
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
index b37a269e2..34c7702bc 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
@@ -11,6 +11,16 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
import java.util.List;
public interface PlaybackListener {
+
+ /**
+ * Called to check if the currently playing stream is close to the end of its playback.
+ * Implementation should return true when the current playback position is within
+ * timeToEndMillis or less until its playback completes or transitions.
+ *
+ * May be called at any time.
+ * */
+ boolean isNearPlaybackEdge(final long timeToEndMillis);
+
/**
* Called when the stream at the current queue index is not ready yet.
* Signals to the listener to block the player from playing anything and notify the source
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java
new file mode 100644
index 000000000..405dba11e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java
@@ -0,0 +1,52 @@
+package org.schabi.newpipe.playlist;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+
+public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback {
+ private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
+ private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
+
+ public PlayQueueItemTouchCallback() {
+ super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
+ }
+
+ public abstract void onMove(final int sourceIndex, final int targetIndex);
+
+ @Override
+ public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
+ int viewSizeOutOfBounds, int totalSize,
+ long msSinceStartScroll) {
+ final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
+ viewSizeOutOfBounds, totalSize, msSinceStartScroll);
+ final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
+ Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
+ return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
+ }
+
+ @Override
+ public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
+ RecyclerView.ViewHolder target) {
+ if (source.getItemViewType() != target.getItemViewType()) {
+ return false;
+ }
+
+ final int sourceIndex = source.getLayoutPosition();
+ final int targetIndex = target.getLayoutPosition();
+ onMove(sourceIndex, targetIndex);
+ return true;
+ }
+
+ @Override
+ public boolean isLongPressDragEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isItemViewSwipeEnabled() {
+ return false;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java
index e0836e06c..53e8d6fc4 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java
@@ -1,12 +1,35 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.preference.Preference;
+import android.widget.Toast;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.util.InfoCache;
public class HistorySettingsFragment extends BasePreferenceFragment {
+ private String cacheWipeKey;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ cacheWipeKey = getString(R.string.metadata_cache_wipe_key);
+ }
+
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.history_settings);
}
+
+ @Override
+ public boolean onPreferenceTreeClick(Preference preference) {
+ if (preference.getKey().equals(cacheWipeKey)) {
+ InfoCache.getInstance().clearCache();
+ Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice,
+ Toast.LENGTH_SHORT).show();
+ }
+
+ return super.onPreferenceTreeClick(preference);
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
index 47c45e82a..ecc66bb40 100644
--- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
+++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
@@ -43,7 +43,6 @@ public final class InfoCache {
* Trim the cache to this size
*/
private static final int TRIM_CACHE_TO = 30;
- private static final int DEFAULT_TIMEOUT_HOURS = 4;
private static final LruCache lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE);
@@ -66,13 +65,7 @@ public final class InfoCache {
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) {
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
- final long expirationMillis;
- if (info.getServiceId() == SoundCloud.getServiceId()) {
- expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES);
- } else {
- expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS);
- }
-
+ final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
synchronized (lruCache) {
final CacheData data = new CacheData(info, expirationMillis);
lruCache.put(keyOf(serviceId, url), data);
diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
index 7d71750eb..9d71ae83a 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
@@ -12,6 +12,10 @@ import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
+
public class ServiceHelper {
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
@@ -98,4 +102,12 @@ public class ServiceHelper {
PreferenceManager.getDefaultSharedPreferences(context).edit().
putString(context.getString(R.string.current_service_key), serviceName).apply();
}
+
+ public static long getCacheExpirationMillis(final int serviceId) {
+ if (serviceId == SoundCloud.getServiceId()) {
+ return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
+ } else {
+ return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
+ }
+ }
}
diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml
index c3480c547..11765f901 100644
--- a/app/src/main/res/layout-land/activity_player_queue_control.xml
+++ b/app/src/main/res/layout-land/activity_player_queue_control.xml
@@ -301,9 +301,13 @@
android:id="@+id/live_sync"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
android:gravity="center"
- android:text="@string/live_sync"
+ android:text="@string/duration_live"
+ android:textAllCaps="true"
android:textColor="?attr/colorAccent"
+ android:maxLength="4"
android:background="?attr/selectableItemBackground"
android:visibility="gone"/>
diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml
index e7d337c17..8f608de3a 100644
--- a/app/src/main/res/layout/activity_main_player.xml
+++ b/app/src/main/res/layout/activity_main_player.xml
@@ -308,7 +308,7 @@
android:id="@+id/toggleOrientation"
android:layout_width="30dp"
android:layout_height="30dp"
- android:layout_marginLeft="2dp"
+ android:layout_marginLeft="4dp"
android:layout_marginRight="2dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
@@ -325,8 +325,8 @@
android:id="@+id/switchPopup"
android:layout_width="30dp"
android:layout_height="30dp"
- android:layout_marginLeft="2dp"
- android:layout_marginRight="2dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/toggleOrientation"
android:layout_centerVertical="true"
android:clickable="true"
@@ -341,8 +341,8 @@
android:id="@+id/switchBackground"
android:layout_width="30dp"
android:layout_height="30dp"
- android:layout_marginLeft="2dp"
- android:layout_marginRight="2dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchPopup"
android:layout_centerVertical="true"
android:clickable="true"
@@ -403,9 +403,13 @@
android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
android:gravity="center"
- android:text="@string/live_sync"
+ android:text="@string/duration_live"
+ android:textAllCaps="true"
android:textColor="@android:color/white"
+ android:maxLength="4"
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml
index 639a8037c..7f649e382 100644
--- a/app/src/main/res/layout/activity_player_queue_control.xml
+++ b/app/src/main/res/layout/activity_player_queue_control.xml
@@ -151,9 +151,13 @@
android:id="@+id/live_sync"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
android:gravity="center"
- android:text="@string/live_sync"
+ android:text="@string/duration_live"
+ android:textAllCaps="true"
android:textColor="?attr/colorAccent"
+ android:maxLength="4"
android:background="?attr/selectableItemBackground"
android:visibility="gone"/>
diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml
index 9bbd72fec..0c3ea77df 100644
--- a/app/src/main/res/layout/player_popup.xml
+++ b/app/src/main/res/layout/player_popup.xml
@@ -195,9 +195,13 @@
android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
android:gravity="center_vertical"
- android:text="@string/live_sync"
+ android:text="@string/duration_live"
+ android:textAllCaps="true"
android:textColor="@android:color/white"
+ android:maxLength="4"
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index a897aa185..68d75737a 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -160,6 +160,10 @@
import_data
export_data
+ download_thumbnail_key
+
+ cache_wipe_key
+
file_rename
file_replacement_character
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c97f12809..e1a353807 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -74,6 +74,11 @@
Remember last size and position of popup
Use fast inexact seek
Inexact seek allows the player to seek to positions faster with reduced precision
+ Load thumbnails
+ Disable to stop all non-cached thumbnail from loading and save on data and memory usage
+ Wipe cached metadata
+ Remove all cached webpage data
+ Metadata cache wiped
Auto-queue next stream
Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.
Player gesture controls
@@ -89,7 +94,7 @@
Download
Next video
Show next and similar videos
- Show Hold to Append Tip
+ Show hold to append tip
Show tip when background or popup button is pressed on video details page
URL not supported
Default content country
@@ -98,7 +103,7 @@
Player
Behavior
Video & Audio
- History
+ History & Cache
Popup
Appearance
Other
@@ -418,18 +423,16 @@
ZOOM
Auto-generated
- Caption Font Size
- Smaller Font
- Normal Font
- Larger Font
-
- SYNC
+ Caption font size
+ Smaller font
+ Normal font
+ Larger font
Enable LeakCanary
Memory leak monitoring may cause app to become unresponsive when heap dumping
- Report Out-of-Lifecycle Errors
+ Report Out-of-lifecycle errors
Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose
diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml
index c8c1efb12..2ce8bf9e6 100644
--- a/app/src/main/res/xml/content_settings.xml
+++ b/app/src/main/res/xml/content_settings.xml
@@ -37,6 +37,12 @@
android:summary="@string/auto_queue_summary"
android:title="@string/auto_queue_title"/>
+
+
+
+