1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-09-28 23:10:50 +00:00

-[#1060] Added toggle to disable thumbnail loading.

-Added button to wipe metadata cache.
-Added more paddings on player buttons.
-Added new animations to main player secondary controls and play queue expand/collapse.
-Refactored play queue item touch callback for use in all players.
-Improved MediaSourceManager to better handle expired stream reloading.
-[#1186] Changed live sync button text to "LIVE".
-Removed MediaSourceManager loader coupling on main players.
-Moved service dependent expiry resolution to ServiceHelper.
-[#1186] Fixed livestream timeline updates causing negative time position.
-[#1186] Fixed livestream not starting from live-edge.
-Fixed main player system UI not retracting on playback start.
This commit is contained in:
John Zhen Mo 2018-03-13 20:25:22 -07:00
parent 1e57b5ea49
commit 61b422502b
18 changed files with 305 additions and 179 deletions

View File

@ -1,25 +1,40 @@
package org.schabi.newpipe; package org.schabi.newpipe;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader; import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
public class ImageDownloader extends BaseImageDownloader { 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) { public ImageDownloader(Context context) {
super(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) { private boolean isDownloadingThumbnail() {
super(context, connectTimeout, readTimeout); return preferences.getBoolean(downloadThumbnailKey, true);
} }
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
Downloader downloader = (Downloader) NewPipe.getDownloader(); if (isDownloadingThumbnail()) {
return downloader.stream(imageUri); final Downloader downloader = (Downloader) NewPipe.getDownloader();
return downloader.stream(imageUri);
} else {
return DUMMY_INPUT_STREAM;
}
} }
} }

View File

@ -57,6 +57,7 @@ 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;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.LoadController;
@ -244,6 +245,7 @@ public abstract class BasePlayer implements
playQueue = queue; playQueue = queue;
playQueue.init(); playQueue.init();
if (playbackManager != null) playbackManager.dispose();
playbackManager = new MediaSourceManager(this, playQueue); playbackManager = new MediaSourceManager(this, playQueue);
if (playQueueAdapter != null) playQueueAdapter.dispose(); if (playQueueAdapter != null) playQueueAdapter.dispose();
@ -272,7 +274,6 @@ public abstract class BasePlayer implements
public void destroy() { public void destroy() {
if (DEBUG) Log.d(TAG, "destroy() called"); if (DEBUG) Log.d(TAG, "destroy() called");
destroyPlayer(); destroyPlayer();
clearThumbnailCache();
unregisterBroadcastReceiver(); unregisterBroadcastReceiver();
trackSelector = null; trackSelector = null;
@ -314,11 +315,6 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " +
"imageUri = [" + imageUri + "], view = [" + view + "]"); "imageUri = [" + imageUri + "], view = [" + view + "]");
} }
protected void clearThumbnailCache() {
ImageLoader.getInstance().clearMemoryCache();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// MediaSource Building // MediaSource Building
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -448,7 +444,6 @@ public abstract class BasePlayer implements
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() {}
@ -522,11 +517,9 @@ public abstract class BasePlayer implements
); );
} }
private Disposable getProgressReactor() { private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.filter(ignored -> isProgressLoopRunning())
.subscribe(ignored -> triggerProgressUpdate()); .subscribe(ignored -> triggerProgressUpdate());
} }
@ -541,16 +534,19 @@ public abstract class BasePlayer implements
(manifest == null ? "no manifest" : "available manifest") + ", " + (manifest == null ? "no manifest" : "available manifest") + ", " +
"timeline size = [" + timeline.getWindowCount() + "], " + "timeline size = [" + timeline.getWindowCount() + "], " +
"reason = [" + reason + "]"); "reason = [" + reason + "]");
if (playQueue == null) return;
switch (reason) { switch (reason) {
case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block
case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock
case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes
if (playQueue != null && playbackManager != null && // ensures MediaSourceManager#update is complete
// ensures MediaSourceManager#update is complete final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size();
timeline.getWindowCount() == playQueue.size()) { // Ensure dynamic/livestream timeline changes does not cause negative position
playbackManager.load(); if (isPlaylistStable && !isCurrentWindowValid()) {
simpleExoPlayer.seekTo(/*clampToMillis=*/0);
} }
break;
} }
} }
@ -775,6 +771,16 @@ public abstract class BasePlayer implements
// Playback Listener // 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 @Override
public void onPlaybackBlock() { public void onPlaybackBlock() {
if (simpleExoPlayer == null) return; if (simpleExoPlayer == null) return;
@ -796,7 +802,6 @@ public abstract class BasePlayer implements
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
simpleExoPlayer.prepare(mediaSource); simpleExoPlayer.prepare(mediaSource);
seekToDefault();
} }
@Override @Override
@ -825,16 +830,24 @@ public abstract class BasePlayer implements
if (simpleExoPlayer == null) return; if (simpleExoPlayer == null) return;
final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
// Check if on wrong window // Check if on wrong window
if (currentPlayQueueIndex != playQueue.getIndex()) { 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 + "], " + "index=[" + currentPlayQueueIndex + "], " +
"queue index=[" + playQueue.getIndex() + "]"); "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()) { } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) {
final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; 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 + "]," + " window=[" + currentPlayQueueIndex + "]," +
" at=[" + getTimeString((int)startPos) + "]," + " at=[" + getTimeString((int)startPos) + "]," +
" from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "].");
@ -858,6 +871,11 @@ public abstract class BasePlayer implements
@Nullable @Nullable
@Override @Override
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { 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()) { if (!info.getHlsUrl().isEmpty()) {
return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS);
} else if (!info.getDashMpdUrl().isEmpty()) { } 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 (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
if (playWhenReady) audioReactor.requestAudioFocus(); if (playWhenReady) audioReactor.requestAudioFocus();
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
// On live prepared
if (simpleExoPlayer.isCurrentWindowDynamic()) seekToDefault();
} }
public void onVideoPlayPause() { public void onVideoPlayPause() {
@ -945,14 +966,15 @@ public abstract class BasePlayer implements
if (simpleExoPlayer == null || playQueue == null) return; if (simpleExoPlayer == null || playQueue == null) return;
if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); 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
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. * is the first in a queue.*/
* Also restart the track if the current track is the first in a queue.*/ if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT ||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { playQueue.getIndex() == 0) {
final long startPos = currentInfo == null ? 0 : currentInfo.getStartPosition(); seekToDefault();
simpleExoPlayer.seekTo(startPos); playQueue.offsetIndex(0);
} else { } else {
savePlaybackState();
playQueue.offsetIndex(-1); playQueue.offsetIndex(-1);
} }
} }
@ -962,7 +984,6 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "onPlayNext() called"); if (DEBUG) Log.d(TAG, "onPlayNext() called");
savePlaybackState(); savePlaybackState();
playQueue.offsetIndex(+1); playQueue.offsetIndex(+1);
} }
@ -975,8 +996,9 @@ public abstract class BasePlayer implements
if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) {
seekToDefault(); seekToDefault();
} else { } else {
playQueue.setIndex(index); savePlaybackState();
} }
playQueue.setIndex(index);
} }
public void seekBy(int milliSeconds) { public void seekBy(int milliSeconds) {
@ -1015,8 +1037,11 @@ public abstract class BasePlayer implements
protected void reload() { protected void reload() {
if (playbackManager != null) { if (playbackManager != null) {
playbackManager.reset(); playbackManager.dispose();
playbackManager.load(); }
if (playQueue != null) {
playbackManager = new MediaSourceManager(this, playQueue);
} }
} }

View File

@ -62,6 +62,7 @@ import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder; import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; 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.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; 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.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.AnimationUtils.animateView;
import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; 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); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
setVolumeControlStream(AudioManager.STREAM_MUSIC); setVolumeControlStream(AudioManager.STREAM_MUSIC);
changeSystemUi(); hideSystemUi();
setContentView(R.layout.activity_main_player); setContentView(R.layout.activity_main_player);
playerImpl = new VideoPlayerImpl(this); playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(findViewById(android.R.id.content)); playerImpl.setup(findViewById(android.R.id.content));
@ -597,28 +600,27 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
updatePlaybackButtons(); updatePlaybackButtons();
getControlsRoot().setVisibility(View.INVISIBLE); getControlsRoot().setVisibility(View.INVISIBLE);
queueLayout.setVisibility(View.VISIBLE); animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true,
DEFAULT_CONTROLS_DURATION);
itemsList.scrollToPosition(playQueue.getIndex()); itemsList.scrollToPosition(playQueue.getIndex());
} }
private void onQueueClosed() { private void onQueueClosed() {
queueLayout.setVisibility(View.GONE); animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false,
DEFAULT_CONTROLS_DURATION);
queueVisible = false; queueVisible = false;
} }
private void onMoreOptionsClicked() { private void onMoreOptionsClicked() {
if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called");
if (secondaryControls.getVisibility() == View.VISIBLE) { final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE;
moreOptionsButton.setImageDrawable(getResources().getDrawable(
R.drawable.ic_expand_more_white_24dp)); animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION,
animateView(secondaryControls, false, 200); isMoreControlsVisible ? 0 : 180);
} else { animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible,
moreOptionsButton.setImageDrawable(getResources().getDrawable( DEFAULT_CONTROLS_DURATION);
R.drawable.ic_expand_less_white_24dp));
animateView(secondaryControls, true, 200);
}
showControls(DEFAULT_CONTROLS_DURATION); showControls(DEFAULT_CONTROLS_DURATION);
} }
@ -696,7 +698,6 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
animatePlayButtons(true, 200); animatePlayButtons(true, 200);
}); });
changeSystemUi();
getRootView().setKeepScreenOn(true); getRootView().setKeepScreenOn(true);
} }
@ -798,31 +799,11 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
} }
private ItemTouchHelper.SimpleCallback getItemTouchCallback() { private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { return new PlayQueueItemTouchCallback() {
@Override @Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { public void onMove(int sourceIndex, int targetIndex) {
if (source.getItemViewType() != target.getItemViewType()) { if (playQueue != null) playQueue.move(sourceIndex, targetIndex);
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
playQueue.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) {}
}; };
} }

View File

@ -34,6 +34,7 @@ import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder; import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper; 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 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 View rootView;
private RecyclerView itemsList; private RecyclerView itemsList;
@ -398,43 +396,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
} }
private ItemTouchHelper.SimpleCallback getItemTouchCallback() { private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { return new PlayQueueItemTouchCallback() {
@Override @Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, public void onMove(int sourceIndex, int targetIndex) {
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();
if (player != null) player.getPlayQueue().move(sourceIndex, 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) {}
}; };
} }

View File

@ -21,15 +21,15 @@ import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent; import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent; import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
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 java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
@ -42,7 +42,7 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager { 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. * 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; @NonNull private final PlayQueue playQueue;
/** /**
* Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing * Determines the gap time between the playback position and the playback duration which
* {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure * the {@link #getEdgeIntervalSignal()} begins to request loading.
* 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 #progressUpdateIntervalMillis
* @see #isCorrectionNeeded(PlayQueueItem)
* */ * */
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<Long> nearEndIntervalSignal;
/** /**
* Process only the last load order when receiving a stream of load orders (lessens I/O). * 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, public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) { @NonNull final PlayQueue playQueue) {
this(listener, playQueue, this(listener, playQueue, /*loadDebounceMillis=*/400L,
/*loadDebounceMillis=*/400L, /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS),
/*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)); /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS));
} }
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 windowRefreshTimeMillis) { final long playbackNearEndGapMillis,
final long progressUpdateIntervalMillis) {
if (playQueue.getBroadcastReceiver() == null) { if (playQueue.getBroadcastReceiver() == null) {
throw new IllegalArgumentException("Play Queue has not been initialized."); 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.playbackListener = listener;
this.playQueue = playQueue; this.playQueue = playQueue;
this.windowRefreshTimeMillis = windowRefreshTimeMillis; this.playbackNearEndGapMillis = playbackNearEndGapMillis;
this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
this.nearEndIntervalSignal = getEdgeIntervalSignal();
this.loadDebounceMillis = loadDebounceMillis; this.loadDebounceMillis = loadDebounceMillis;
this.debouncedSignal = PublishSubject.create(); this.debouncedSignal = PublishSubject.create();
@ -161,28 +170,6 @@ public class MediaSourceManager {
sources.releaseSource(); 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 // Event Reactor
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -219,11 +206,13 @@ public class MediaSourceManager {
switch (event.type()) { switch (event.type()) {
case INIT: case INIT:
case ERROR: case ERROR:
reset(); maybeBlock();
break;
case APPEND: case APPEND:
populateSources(); populateSources();
break; break;
case SELECT:
maybeRenewCurrentIndex();
break;
case REMOVE: case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event; final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex()); remove(removeEvent.getRemoveIndex());
@ -238,7 +227,6 @@ public class MediaSourceManager {
final ReorderEvent reorderEvent = (ReorderEvent) event; final ReorderEvent reorderEvent = (ReorderEvent) event;
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex());
break; break;
case SELECT:
case RECOVERY: case RECOVERY:
default: default:
break; break;
@ -347,8 +335,13 @@ public class MediaSourceManager {
// MediaSource Loading // MediaSource Loading
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private Observable<Long> getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
.filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis));
}
private Disposable getDebouncedLoader() { private Disposable getDebouncedLoader() {
return debouncedSignal return debouncedSignal.mergeWith(nearEndIntervalSignal)
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate()); .subscribe(timestamp -> loadImmediate());
@ -359,13 +352,14 @@ public class MediaSourceManager {
} }
private void loadImmediate() { private void loadImmediate() {
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
// The current item has higher priority // The current item has higher priority
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 // 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(); loaderReactor.clear();
loadingItems.clear(); loadingItems.clear();
} }
@ -377,7 +371,7 @@ public class MediaSourceManager {
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightLimit = currentIndex + WINDOW_SIZE + 1; final int rightLimit = currentIndex + WINDOW_SIZE + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit); final int rightBound = Math.min(playQueue.size(), rightLimit);
final List<PlayQueueItem> items = new ArrayList<>( final Set<PlayQueueItem> items = new HashSet<>(
playQueue.getStreams().subList(leftBound,rightBound)); playQueue.getStreams().subList(leftBound,rightBound));
// Do a round robin // Do a round robin
@ -385,6 +379,7 @@ public class MediaSourceManager {
if (excess >= 0) { if (excess >= 0) {
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
} }
items.remove(currentItem);
for (final PlayQueueItem item : items) { for (final PlayQueueItem item : items) {
maybeLoadItem(item); maybeLoadItem(item);
@ -405,9 +400,9 @@ public class MediaSourceManager {
/* No exception handling since getLoadedMediaSource guarantees nonnull return */ /* No exception handling since getLoadedMediaSource guarantees nonnull return */
.subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource));
loaderReactor.add(loader); loaderReactor.add(loader);
} else {
maybeSynchronizePlayer();
} }
maybeSynchronizePlayer();
} }
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) { private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
@ -423,7 +418,8 @@ public class MediaSourceManager {
return new FailedMediaSource(stream, exception); 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); return new LoadedMediaSource(source, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); }).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 // MediaSource Playlist Helpers
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -476,6 +490,7 @@ public class MediaSourceManager {
this.sources.releaseSource(); this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource(false, this.sources = new DynamicConcatenatingMediaSource(false,
// Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
new ShuffleOrder.UnshuffledShuffleOrder(0)); new ShuffleOrder.UnshuffledShuffleOrder(0));
} }

View File

@ -11,6 +11,16 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
import java.util.List; import java.util.List;
public interface PlaybackListener { 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. * 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 * Signals to the listener to block the player from playing anything and notify the source

View File

@ -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) {}
}

View File

@ -1,12 +1,35 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.os.Bundle; 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.R;
import org.schabi.newpipe.util.InfoCache;
public class HistorySettingsFragment extends BasePreferenceFragment { 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 @Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.history_settings); 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);
}
} }

View File

@ -43,7 +43,6 @@ public final class InfoCache {
* Trim the cache to this size * Trim the cache to this size
*/ */
private static final int TRIM_CACHE_TO = 30; private static final int TRIM_CACHE_TO = 30;
private static final int DEFAULT_TIMEOUT_HOURS = 4;
private static final LruCache<String, CacheData> lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE); private static final LruCache<String, CacheData> 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) { 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 + "]");
final long expirationMillis; final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
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) {
final CacheData data = new CacheData(info, expirationMillis); final CacheData data = new CacheData(info, expirationMillis);
lruCache.put(keyOf(serviceId, url), data); lruCache.put(keyOf(serviceId, url), data);

View File

@ -12,6 +12,10 @@ import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import java.util.concurrent.TimeUnit;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
public class ServiceHelper { public class ServiceHelper {
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
@ -98,4 +102,12 @@ public class ServiceHelper {
PreferenceManager.getDefaultSharedPreferences(context).edit(). PreferenceManager.getDefaultSharedPreferences(context).edit().
putString(context.getString(R.string.current_service_key), serviceName).apply(); 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);
}
}
} }

View File

@ -301,9 +301,13 @@
android:id="@+id/live_sync" android:id="@+id/live_sync"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/live_sync" android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
android:maxLength="4"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>

View File

@ -308,7 +308,7 @@
android:id="@+id/toggleOrientation" android:id="@+id/toggleOrientation"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginLeft="2dp" android:layout_marginLeft="4dp"
android:layout_marginRight="2dp" android:layout_marginRight="2dp"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
@ -325,8 +325,8 @@
android:id="@+id/switchPopup" android:id="@+id/switchPopup"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginLeft="2dp" android:layout_marginLeft="4dp"
android:layout_marginRight="2dp" android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/toggleOrientation" android:layout_toLeftOf="@id/toggleOrientation"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:clickable="true" android:clickable="true"
@ -341,8 +341,8 @@
android:id="@+id/switchBackground" android:id="@+id/switchBackground"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginLeft="2dp" android:layout_marginLeft="4dp"
android:layout_marginRight="2dp" android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchPopup" android:layout_toLeftOf="@id/switchPopup"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:clickable="true" android:clickable="true"
@ -403,9 +403,13 @@
android:id="@+id/playbackLiveSync" android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/live_sync" android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:maxLength="4"
android:visibility="gone" android:visibility="gone"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />

View File

@ -151,9 +151,13 @@
android:id="@+id/live_sync" android:id="@+id/live_sync"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/live_sync" android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
android:maxLength="4"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>

View File

@ -195,9 +195,13 @@
android:id="@+id/playbackLiveSync" android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:text="@string/live_sync" android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:maxLength="4"
android:visibility="gone" android:visibility="gone"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />

View File

@ -160,6 +160,10 @@
<string name="import_data">import_data</string> <string name="import_data">import_data</string>
<string name="export_data">export_data</string> <string name="export_data">export_data</string>
<string name="download_thumbnail_key" translatable="false">download_thumbnail_key</string>
<string name="metadata_cache_wipe_key" translatable="false">cache_wipe_key</string>
<!-- FileName Downloads --> <!-- FileName Downloads -->
<string name="settings_file_charset_key" translatable="false">file_rename</string> <string name="settings_file_charset_key" translatable="false">file_rename</string>
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>

View File

@ -74,6 +74,11 @@
<string name="popup_remember_size_pos_summary">Remember last size and position of popup</string> <string name="popup_remember_size_pos_summary">Remember last size and position of popup</string>
<string name="use_inexact_seek_title">Use fast inexact seek</string> <string name="use_inexact_seek_title">Use fast inexact seek</string>
<string name="use_inexact_seek_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string> <string name="use_inexact_seek_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string>
<string name="download_thumbnail_title">Load thumbnails</string>
<string name="download_thumbnail_summary">Disable to stop all non-cached thumbnail from loading and save on data and memory usage</string>
<string name="metadata_cache_wipe_title">Wipe cached metadata</string>
<string name="metadata_cache_wipe_summary">Remove all cached webpage data</string>
<string name="metadata_cache_wipe_complete_notice">Metadata cache wiped</string>
<string name="auto_queue_title">Auto-queue next stream</string> <string name="auto_queue_title">Auto-queue next stream</string>
<string name="auto_queue_summary">Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.</string> <string name="auto_queue_summary">Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.</string>
<string name="player_gesture_controls_title">Player gesture controls</string> <string name="player_gesture_controls_title">Player gesture controls</string>
@ -89,7 +94,7 @@
<string name="download_dialog_title">Download</string> <string name="download_dialog_title">Download</string>
<string name="next_video_title">Next video</string> <string name="next_video_title">Next video</string>
<string name="show_next_and_similar_title">Show next and similar videos</string> <string name="show_next_and_similar_title">Show next and similar videos</string>
<string name="show_hold_to_append_title">Show Hold to Append Tip</string> <string name="show_hold_to_append_title">Show hold to append tip</string>
<string name="show_hold_to_append_summary">Show tip when background or popup button is pressed on video details page</string> <string name="show_hold_to_append_summary">Show tip when background or popup button is pressed on video details page</string>
<string name="url_not_supported_toast">URL not supported</string> <string name="url_not_supported_toast">URL not supported</string>
<string name="default_content_country_title">Default content country</string> <string name="default_content_country_title">Default content country</string>
@ -98,7 +103,7 @@
<string name="settings_category_player_title">Player</string> <string name="settings_category_player_title">Player</string>
<string name="settings_category_player_behavior_title">Behavior</string> <string name="settings_category_player_behavior_title">Behavior</string>
<string name="settings_category_video_audio_title">Video &amp; Audio</string> <string name="settings_category_video_audio_title">Video &amp; Audio</string>
<string name="settings_category_history_title">History</string> <string name="settings_category_history_title">History &amp; Cache</string>
<string name="settings_category_popup_title">Popup</string> <string name="settings_category_popup_title">Popup</string>
<string name="settings_category_appearance_title">Appearance</string> <string name="settings_category_appearance_title">Appearance</string>
<string name="settings_category_other_title">Other</string> <string name="settings_category_other_title">Other</string>
@ -418,18 +423,16 @@
<string name="resize_zoom">ZOOM</string> <string name="resize_zoom">ZOOM</string>
<string name="caption_auto_generated">Auto-generated</string> <string name="caption_auto_generated">Auto-generated</string>
<string name="caption_font_size_settings_title">Caption Font Size</string> <string name="caption_font_size_settings_title">Caption font size</string>
<string name="smaller_caption_font_size">Smaller Font</string> <string name="smaller_caption_font_size">Smaller font</string>
<string name="normal_caption_font_size">Normal Font</string> <string name="normal_caption_font_size">Normal font</string>
<string name="larger_caption_font_size">Larger Font</string> <string name="larger_caption_font_size">Larger font</string>
<string name="live_sync">SYNC</string>
<!-- Debug Settings --> <!-- Debug Settings -->
<string name="enable_leak_canary_title">Enable LeakCanary</string> <string name="enable_leak_canary_title">Enable LeakCanary</string>
<string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string> <string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string>
<string name="enable_disposed_exceptions_title">Report Out-of-Lifecycle Errors</string> <string name="enable_disposed_exceptions_title">Report Out-of-lifecycle errors</string>
<string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string> <string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string>
<!-- Subscriptions import/export --> <!-- Subscriptions import/export -->

View File

@ -37,6 +37,12 @@
android:summary="@string/auto_queue_summary" android:summary="@string/auto_queue_summary"
android:title="@string/auto_queue_title"/> android:title="@string/auto_queue_title"/>
<SwitchPreference
android:defaultValue="true"
android:key="@string/download_thumbnail_key"
android:title="@string/download_thumbnail_title"
android:summary="@string/download_thumbnail_summary"/>
<ListPreference <ListPreference
android:defaultValue="@string/kiosk_page_key" android:defaultValue="@string/kiosk_page_key"
android:entries="@array/main_page_content_names" android:entries="@array/main_page_content_names"

View File

@ -16,4 +16,9 @@
android:summary="@string/enable_search_history_summary" android:summary="@string/enable_search_history_summary"
android:title="@string/enable_search_history_title"/> android:title="@string/enable_search_history_title"/>
<Preference
android:key="@string/metadata_cache_wipe_key"
android:summary="@string/metadata_cache_wipe_summary"
android:title="@string/metadata_cache_wipe_title"/>
</PreferenceScreen> </PreferenceScreen>