1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-01-24 00:23:17 +01:00

-Added toggle to enable fast inexact seek in players.

-Improved player sync calls to recognize different metadata updates.
-Changed MediaSourceManager to synchronize only after timeline changes and reenabled multiple sync calls to player.
-Renamed listener and synchronization methods related to MediaSourceManager.
This commit is contained in:
John Zhen Mo 2018-03-03 14:24:21 -08:00
parent 0c17f0825b
commit a88e19a8ed
11 changed files with 132 additions and 100 deletions

View File

@ -380,11 +380,10 @@ public final class BackgroundPlayer extends Service {
// Playback Listener // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override protected void onMetadataChanged(@NonNull final PlayQueueItem item,
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { @Nullable final StreamInfo info,
if (currentItem == item && currentInfo == info) return; final int newPlayQueueIndex,
super.sync(item, info); final boolean hasPlayQueueItemChanged) {
resetNotification(); resetNotification();
updateNotification(-1); updateNotification(-1);
updateMetadata(); updateMetadata();
@ -405,8 +404,8 @@ public final class BackgroundPlayer extends Service {
} }
@Override @Override
public void shutdown() { public void onPlaybackShutdown() {
super.shutdown(); super.onPlaybackShutdown();
onClose(); onClose();
} }

View File

@ -61,6 +61,7 @@ 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;
import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlaybackListener;
@ -196,6 +197,7 @@ public abstract class BasePlayer implements
simpleExoPlayer.addListener(this); simpleExoPlayer.addListener(this);
simpleExoPlayer.setPlayWhenReady(true); simpleExoPlayer.setPlayWhenReady(true);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
} }
public void initListeners() {} public void initListeners() {}
@ -688,7 +690,7 @@ public abstract class BasePlayer implements
break; break;
default: default:
showUnrecoverableError(error); showUnrecoverableError(error);
shutdown(); onPlaybackShutdown();
break; break;
} }
} }
@ -774,9 +776,9 @@ public abstract class BasePlayer implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void block() { public void onPlaybackBlock() {
if (simpleExoPlayer == null) return; if (simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Playback - block() called"); if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called");
currentItem = null; currentItem = null;
currentInfo = null; currentInfo = null;
@ -787,9 +789,9 @@ public abstract class BasePlayer implements
} }
@Override @Override
public void unblock(final MediaSource mediaSource) { public void onPlaybackUnblock(final MediaSource mediaSource) {
if (simpleExoPlayer == null) return; if (simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Playback - unblock() called"); if (DEBUG) Log.d(TAG, "Playback - onPlaybackUnblock() called");
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
@ -798,34 +800,50 @@ public abstract class BasePlayer implements
} }
@Override @Override
public void sync(@NonNull final PlayQueueItem item, public void onPlaybackSynchronize(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info) { @Nullable final StreamInfo info) {
if (currentItem == item && currentInfo == info) return; if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " +
currentItem = item;
currentInfo = info;
if (DEBUG) Log.d(TAG, "Playback - sync() called with " +
(info == null ? "available" : "null") + " info, " + (info == null ? "available" : "null") + " info, " +
"item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
if (simpleExoPlayer == null) return;
final boolean hasPlayQueueItemChanged = currentItem != item;
final boolean hasStreamInfoChanged = currentInfo != info;
if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) {
return; // Nothing to synchronize
}
currentItem = item;
currentInfo = info;
if (hasPlayQueueItemChanged) {
// updates only to the stream info should not trigger another view count
registerView();
initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl());
}
final int currentSourceIndex = playQueue.indexOf(item);
onMetadataChanged(item, info, currentSourceIndex, hasPlayQueueItemChanged);
// Check if on wrong window // Check if on wrong window
final int currentSourceIndex = playQueue.indexOf(item); if (simpleExoPlayer == null) return;
if (currentSourceIndex != playQueue.getIndex()) { if (currentSourceIndex != playQueue.getIndex()) {
Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex + Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex +
"], queue index=[" + playQueue.getIndex() + "]"); "], queue index=[" + playQueue.getIndex() + "]");
} else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) {
// on metadata changed
} else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) {
final long startPos = info != null ? info.start_position : 0; final long startPos = info != null ? info.start_position : 0;
if (DEBUG) Log.d(TAG, "Rewinding to correct window=[" + currentSourceIndex + "]," + if (DEBUG) Log.d(TAG, "Rewinding to correct window=[" + currentSourceIndex + "]," +
" at=[" + getTimeString((int)startPos) + "]," + " at=[" + getTimeString((int)startPos) + "]," +
" from=[" + simpleExoPlayer.getCurrentPeriodIndex() + "]."); " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "].");
simpleExoPlayer.seekTo(currentSourceIndex, startPos); simpleExoPlayer.seekTo(currentSourceIndex, startPos);
} }
registerView();
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
} }
abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info,
final int newPlayQueueIndex,
final boolean hasPlayQueueItemChanged);
@Nullable @Nullable
@Override @Override
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
@ -839,7 +857,7 @@ public abstract class BasePlayer implements
} }
@Override @Override
public void shutdown() { public void onPlaybackShutdown() {
if (DEBUG) Log.d(TAG, "Shutting down..."); if (DEBUG) Log.d(TAG, "Shutting down...");
destroy(); destroy();
} }

View File

@ -391,31 +391,32 @@ public final class MainVideoPlayer extends Activity {
updatePlaybackButtons(); updatePlaybackButtons();
} }
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void shutdown() {
super.shutdown();
finish();
}
@Override
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
super.sync(item, info);
titleTextView.setText(getVideoTitle());
channelTextView.setText(getUploaderName());
//playPauseButton.setImageResource(R.drawable.ic_pause_white);
}
@Override @Override
public void onShuffleClicked() { public void onShuffleClicked() {
super.onShuffleClicked(); super.onShuffleClicked();
updatePlaybackButtons(); updatePlaybackButtons();
} }
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info,
final int newPlayQueueIndex,
final boolean hasPlayQueueItemChanged) {
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
titleTextView.setText(getVideoTitle());
channelTextView.setText(getUploaderName());
}
@Override
public void onPlaybackShutdown() {
super.onPlaybackShutdown();
finish();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Player Overrides // Player Overrides
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View File

@ -535,7 +535,8 @@ public final class PopupVideoPlayer extends Service {
private void updatePlayback() { private void updatePlayback() {
if (activityListener != null && simpleExoPlayer != null && playQueue != null) { if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
} }
} }
@ -574,16 +575,17 @@ public final class PopupVideoPlayer extends Service {
// Playback Listener // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override protected void onMetadataChanged(@NonNull final PlayQueueItem item,
public void sync(@NonNull PlayQueueItem item, @Nullable StreamInfo info) { @Nullable final StreamInfo info,
if (currentItem == item && currentInfo == info) return; final int newPlayQueueIndex,
super.sync(item, info); final boolean hasPlayQueueItemChanged) {
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
updateMetadata(); updateMetadata();
} }
@Override @Override
public void shutdown() { public void onPlaybackShutdown() {
super.shutdown(); super.onPlaybackShutdown();
onClose(); onClose();
} }

View File

@ -324,9 +324,10 @@ public abstract class VideoPlayer extends BasePlayer
protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality); protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality);
@Override protected void onMetadataChanged(@NonNull final PlayQueueItem item,
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { @Nullable final StreamInfo info,
super.sync(item, info); final int newPlayQueueIndex,
final boolean hasPlayQueueItemChanged) {
qualityTextView.setVisibility(View.GONE); qualityTextView.setVisibility(View.GONE);
playbackSpeedTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE);

View File

@ -5,6 +5,7 @@ import android.content.SharedPreferences;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
@ -109,6 +110,12 @@ public class PlayerHelper {
return isRememberingPopupDimensions(context, true); return isRememberingPopupDimensions(context, true);
} }
@NonNull
public static SeekParameters getSeekParameters(@NonNull final Context context) {
return isUsingInexactSeek(context, false) ?
SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;
}
public static long getPreferredCacheSize(@NonNull final Context context) { public static long getPreferredCacheSize(@NonNull final Context context) {
return 64 * 1024 * 1024L; return 64 * 1024 * 1024L;
} }
@ -176,4 +183,8 @@ public class PlayerHelper {
private static boolean isRememberingPopupDimensions(@Nonnull final Context context, final boolean b) { private static boolean isRememberingPopupDimensions(@Nonnull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b); return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b);
} }
private static boolean isUsingInexactSeek(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.use_inexact_seek_key), b);
}
} }

View File

@ -50,7 +50,7 @@ public class MediaSourceManager {
* streams before will only be cached for future usage. * streams before will only be cached for future usage.
* *
* @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource) * @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource)
* @see #update(int, MediaSource) * @see #update(int, MediaSource, Runnable)
* */ * */
private final static int WINDOW_SIZE = 1; private final static int WINDOW_SIZE = 1;
@ -95,15 +95,13 @@ public class MediaSourceManager {
* */ * */
private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1;
@NonNull private final CompositeDisposable loaderReactor; @NonNull private final CompositeDisposable loaderReactor;
@NonNull private Set<PlayQueueItem> loadingItems; @NonNull private final Set<PlayQueueItem> loadingItems;
@NonNull private final SerialDisposable syncReactor; @NonNull private final SerialDisposable syncReactor;
@NonNull private final AtomicBoolean isBlocked; @NonNull private final AtomicBoolean isBlocked;
@NonNull private DynamicConcatenatingMediaSource sources; @NonNull private DynamicConcatenatingMediaSource sources;
@Nullable private PlayQueueItem syncedItem;
public MediaSourceManager(@NonNull final PlaybackListener listener, public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) { @NonNull final PlayQueue playQueue) {
this(listener, playQueue, this(listener, playQueue,
@ -159,8 +157,6 @@ public class MediaSourceManager {
loaderReactor.dispose(); loaderReactor.dispose();
syncReactor.dispose(); syncReactor.dispose();
sources.releaseSource(); sources.releaseSource();
syncedItem = null;
} }
/** /**
@ -182,9 +178,7 @@ public class MediaSourceManager {
public void reset() { public void reset() {
if (DEBUG) Log.d(TAG, "reset() called."); if (DEBUG) Log.d(TAG, "reset() called.");
tryBlock(); maybeBlock();
syncedItem = null;
populateSources(); populateSources();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -215,7 +209,7 @@ public class MediaSourceManager {
private void onPlayQueueChanged(final PlayQueueEvent event) { private void onPlayQueueChanged(final PlayQueueEvent event) {
if (playQueue.isEmpty() && playQueue.isComplete()) { if (playQueue.isEmpty() && playQueue.isComplete()) {
playbackListener.shutdown(); playbackListener.onPlaybackShutdown();
return; return;
} }
@ -261,7 +255,7 @@ public class MediaSourceManager {
} }
if (!isPlayQueueReady()) { if (!isPlayQueueReady()) {
tryBlock(); maybeBlock();
playQueue.fetch(); playQueue.fetch();
} }
playQueueReactor.request(1); playQueueReactor.request(1);
@ -290,23 +284,23 @@ public class MediaSourceManager {
return false; return false;
} }
private void tryBlock() { private void maybeBlock() {
if (DEBUG) Log.d(TAG, "tryBlock() called."); if (DEBUG) Log.d(TAG, "maybeBlock() called.");
if (isBlocked.get()) return; if (isBlocked.get()) return;
playbackListener.block(); playbackListener.onPlaybackBlock();
resetSources(); resetSources();
isBlocked.set(true); isBlocked.set(true);
} }
private void tryUnblock() { private void maybeUnblock() {
if (DEBUG) Log.d(TAG, "tryUnblock() called."); if (DEBUG) Log.d(TAG, "maybeUnblock() called.");
if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) { if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) {
isBlocked.set(false); isBlocked.set(false);
playbackListener.unblock(sources); playbackListener.onPlaybackUnblock(sources);
} }
} }
@ -314,8 +308,8 @@ public class MediaSourceManager {
// Metadata Synchronization TODO: maybe this should be a separate manager // Metadata Synchronization TODO: maybe this should be a separate manager
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void sync() { private void maybeSync() {
if (DEBUG) Log.d(TAG, "sync() called."); if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called.");
final PlayQueueItem currentItem = playQueue.getItem(); final PlayQueueItem currentItem = playQueue.getItem();
if (isBlocked.get() || currentItem == null) return; if (isBlocked.get() || currentItem == null) return;
@ -323,23 +317,25 @@ public class MediaSourceManager {
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null); final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
if (syncedItem != currentItem) { final Disposable sync = currentItem.getStream()
syncedItem = currentItem; .observeOn(AndroidSchedulers.mainThread())
final Disposable sync = currentItem.getStream() .subscribe(onSuccess, onError);
.observeOn(AndroidSchedulers.mainThread()) syncReactor.set(sync);
.subscribe(onSuccess, onError);
syncReactor.set(sync);
}
} }
private void syncInternal(@NonNull final PlayQueueItem item, private void syncInternal(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info) { @Nullable final StreamInfo info) {
// Ensure the current item is up to date with the play queue // Ensure the current item is up to date with the play queue
if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { if (playQueue.getItem() == item) {
playbackListener.sync(item, info); playbackListener.onPlaybackSynchronize(item, info);
} }
} }
private void maybeSynchronizePlayer() {
maybeUnblock();
maybeSync();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// MediaSource Loading // MediaSource Loading
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -404,8 +400,7 @@ public class MediaSourceManager {
loaderReactor.add(loader); loaderReactor.add(loader);
} }
tryUnblock(); maybeSynchronizePlayer();
sync();
} }
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) { private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
@ -431,17 +426,15 @@ public class MediaSourceManager {
if (DEBUG) Log.d(TAG, "MediaSource - Loaded: [" + item.getTitle() + if (DEBUG) Log.d(TAG, "MediaSource - Loaded: [" + item.getTitle() +
"] with url: " + item.getUrl()); "] with url: " + item.getUrl());
loadingItems.remove(item);
final int itemIndex = playQueue.indexOf(item); final int itemIndex = playQueue.indexOf(item);
// Only update the playlist timeline for items at the current index or after. // Only update the playlist timeline for items at the current index or after.
if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) { if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Updating: [" + item.getTitle() + if (DEBUG) Log.d(TAG, "MediaSource - Updating: [" + item.getTitle() +
"] with url: " + item.getUrl()); "] with url: " + item.getUrl());
update(itemIndex, mediaSource); update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
} }
loadingItems.remove(item);
tryUnblock();
sync();
} }
/** /**
@ -533,10 +526,11 @@ public class MediaSourceManager {
* this will modify the playback timeline prior to the index and may cause desynchronization * this will modify the playback timeline prior to the index and may cause desynchronization
* on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}. * on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}.
* */ * */
private synchronized void update(final int index, @NonNull final MediaSource source) { private synchronized void update(final int index, @NonNull final MediaSource source,
@Nullable final Runnable finalizingAction) {
if (index < 0 || index >= sources.getSize()) return; if (index < 0 || index >= sources.getSize()) return;
sources.addMediaSource(index + 1, source, () -> sources.addMediaSource(index + 1, source, () ->
sources.removeMediaSource(index)); sources.removeMediaSource(index, finalizingAction));
} }
} }

View File

@ -18,7 +18,7 @@ public interface PlaybackListener {
* *
* May be called at any time. * May be called at any time.
* */ * */
void block(); void onPlaybackBlock();
/** /**
* Called when the stream at the current queue index is ready. * Called when the stream at the current queue index is ready.
@ -26,18 +26,16 @@ public interface PlaybackListener {
* *
* May be called only when the player is blocked. * May be called only when the player is blocked.
* */ * */
void unblock(final MediaSource mediaSource); void onPlaybackUnblock(final MediaSource mediaSource);
/** /**
* Called when the queue index is refreshed. * Called when the queue index is refreshed.
* Signals to the listener to synchronize the player's window to the manager's * Signals to the listener to synchronize the player's window to the manager's
* window. * window.
* *
* Occurs once only per play queue item change. * May be called anytime at any amount once unblock is called.
*
* May be called only after unblock is called.
* */ * */
void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
/** /**
* Requests the listener to resolve a stream info into a media source * Requests the listener to resolve a stream info into a media source
@ -55,5 +53,5 @@ public interface PlaybackListener {
* *
* May be called at any time. * May be called at any time.
* */ * */
void shutdown(); void onPlaybackShutdown();
} }

View File

@ -20,6 +20,7 @@
<string name="player_gesture_controls_key" translatable="false">player_gesture_controls</string> <string name="player_gesture_controls_key" translatable="false">player_gesture_controls</string>
<string name="resume_on_audio_focus_gain_key" translatable="false">resume_on_audio_focus_gain</string> <string name="resume_on_audio_focus_gain_key" translatable="false">resume_on_audio_focus_gain</string>
<string name="popup_remember_size_pos_key" translatable="false">popup_remember_size_pos_key</string> <string name="popup_remember_size_pos_key" translatable="false">popup_remember_size_pos_key</string>
<string name="use_inexact_seek_key" translatable="false">use_inexact_seek_key</string>
<string name="default_resolution_key" translatable="false">default_resolution</string> <string name="default_resolution_key" translatable="false">default_resolution</string>
<string name="default_resolution_value" translatable="false">360p</string> <string name="default_resolution_value" translatable="false">360p</string>

View File

@ -72,6 +72,8 @@
<string name="black_theme_title">Black</string> <string name="black_theme_title">Black</string>
<string name="popup_remember_size_pos_title">Remember popup size and position</string> <string name="popup_remember_size_pos_title">Remember popup size and position</string>
<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_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string>
<string name="player_gesture_controls_title">Player gesture controls</string> <string name="player_gesture_controls_title">Player gesture controls</string>
<string name="player_gesture_controls_summary">Use gestures to control the brightness and volume of the player</string> <string name="player_gesture_controls_summary">Use gestures to control the brightness and volume of the player</string>
<string name="show_search_suggestions_title">Search suggestions</string> <string name="show_search_suggestions_title">Search suggestions</string>

View File

@ -100,5 +100,10 @@
android:summary="@string/popup_remember_size_pos_summary" android:summary="@string/popup_remember_size_pos_summary"
android:title="@string/popup_remember_size_pos_title"/> android:title="@string/popup_remember_size_pos_title"/>
<SwitchPreference
android:defaultValue="false"
android:key="@string/use_inexact_seek_key"
android:summary="@string/use_inexact_seek_summary"
android:title="@string/use_inexact_seek_title"/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>