Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Weblate 2018-04-14 11:39:08 +02:00
commit 8834195cc6
30 changed files with 654 additions and 360 deletions

View File

@ -8,8 +8,8 @@ android {
applicationId "org.schabi.newpipe" applicationId "org.schabi.newpipe"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 27 targetSdkVersion 27
versionCode 60 versionCode 61
versionName "0.13.1" versionName "0.13.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@ -42,7 +42,7 @@ android {
ext { ext {
supportLibVersion = '27.1.0' supportLibVersion = '27.1.0'
exoPlayerLibVersion = '2.7.1' exoPlayerLibVersion = '2.7.3'
roomDbLibVersion = '1.0.0' roomDbLibVersion = '1.0.0'
leakCanaryLibVersion = '1.5.4' leakCanaryLibVersion = '1.5.4'
okHttpLibVersion = '1.5.0' okHttpLibVersion = '1.5.0'
@ -73,6 +73,7 @@ dependencies {
implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
implementation 'com.nononsenseapps:filepicker:4.2.1' implementation 'com.nononsenseapps:filepicker:4.2.1'
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion" implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion" implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"

View File

@ -42,7 +42,11 @@
<service <service
android:name=".player.BackgroundPlayer" android:name=".player.BackgroundPlayer"
android:exported="false"/> android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<activity <activity
android:name=".player.BackgroundPlayerActivity" android:name=".player.BackgroundPlayerActivity"

View File

@ -87,6 +87,7 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -538,7 +539,8 @@ public class VideoDetailFragment
final String[] commands = new String[]{ final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup) context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.append_playlist)
}; };
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
@ -549,6 +551,12 @@ public class VideoDetailFragment
case 1: case 1:
NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item));
break; break;
case 2:
if (getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(getFragmentManager(), TAG);
}
break;
default: default:
break; break;
} }

View File

@ -36,6 +36,7 @@ import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.playlist.ChannelPlayQueue; import org.schabi.newpipe.playlist.ChannelPlayQueue;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
@ -49,6 +50,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -161,6 +163,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
context.getResources().getString(R.string.start_here_on_main), context.getResources().getString(R.string.start_here_on_main),
context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup), context.getResources().getString(R.string.start_here_on_popup),
context.getResources().getString(R.string.append_playlist)
}; };
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
@ -183,6 +186,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
case 4: case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break; break;
case 5:
if (getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(getFragmentManager(), TAG);
}
break;
default: default:
break; break;
} }

View File

@ -56,7 +56,8 @@ public final class BookmarkFragment
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final AppDatabase database = NewPipeDatabase.getInstance(getContext()); if (activity == null) return;
final AppDatabase database = NewPipeDatabase.getInstance(activity);
localPlaylistManager = new LocalPlaylistManager(database); localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database); remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable(); disposables = new CompositeDisposable();

View File

@ -118,8 +118,12 @@ public final class BackgroundPlayer extends Service {
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent +
"], flags = [" + flags + "], startId = [" + startId + "]");
basePlayerImpl.handleIntent(intent); basePlayerImpl.handleIntent(intent);
if (basePlayerImpl.mediaSessionManager != null) {
basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
}
return START_NOT_STICKY; return START_NOT_STICKY;
} }
@ -160,6 +164,11 @@ public final class BackgroundPlayer extends Service {
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
shouldUpdateOnProgress = on; shouldUpdateOnProgress = on;
basePlayerImpl.triggerProgressUpdate(); basePlayerImpl.triggerProgressUpdate();
if (on) {
basePlayerImpl.startProgressLoop();
} else {
basePlayerImpl.stopProgressLoop();
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -545,7 +554,6 @@ public final class BackgroundPlayer extends Service {
super.onPaused(); super.onPaused();
updateNotification(R.drawable.ic_play_arrow_white); updateNotification(R.drawable.ic_play_arrow_white);
if (isProgressLoopRunning()) stopProgressLoop();
lockManager.releaseWifiAndCpu(); lockManager.releaseWifiAndCpu();
} }

View File

@ -46,7 +46,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -64,6 +64,7 @@ import org.schabi.newpipe.player.helper.LoadController;
import org.schabi.newpipe.player.helper.MediaSessionManager; import org.schabi.newpipe.player.helper.MediaSessionManager;
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.helper.PlayerHelper;
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.playback.BasePlayerMediaSession; import org.schabi.newpipe.player.playback.BasePlayerMediaSession;
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;
@ -124,7 +125,6 @@ public abstract class BasePlayer implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f};
protected PlayQueue playQueue; protected PlayQueue playQueue;
protected PlayQueueAdapter playQueueAdapter; protected PlayQueueAdapter playQueueAdapter;
@ -140,10 +140,10 @@ public abstract class BasePlayer implements
// Player // Player
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected final static int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds protected final static int FAST_FORWARD_REWIND_AMOUNT_MILLIS = 10000; // 10 Seconds
protected final static int PLAY_PREV_ACTIVATION_LIMIT = 5000; // 5 seconds protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
protected final static int PROGRESS_LOOP_INTERVAL = 500; protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds
protected CustomTrackSelector trackSelector; protected CustomTrackSelector trackSelector;
protected PlayerDataSource dataSource; protected PlayerDataSource dataSource;
@ -177,11 +177,11 @@ public abstract class BasePlayer implements
} }
public void setup() { public void setup() {
if (simpleExoPlayer == null) initPlayer(); if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true);
initListeners(); initListeners();
} }
public void initPlayer() { public void initPlayer(final boolean playOnReady) {
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
@ -191,15 +191,15 @@ public abstract class BasePlayer implements
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
final AdaptiveTrackSelection.Factory trackSelectionFactory = final TrackSelection.Factory trackSelectionFactory =
new AdaptiveTrackSelection.Factory(bandwidthMeter); PlayerHelper.getQualitySelector(context, bandwidthMeter);
trackSelector = new CustomTrackSelector(trackSelectionFactory); trackSelector = new CustomTrackSelector(trackSelectionFactory);
final LoadControl loadControl = new LoadController(context); final LoadControl loadControl = new LoadController(context);
final RenderersFactory renderFactory = new DefaultRenderersFactory(context); final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
simpleExoPlayer.addListener(this); simpleExoPlayer.addListener(this);
simpleExoPlayer.setPlayWhenReady(true); simpleExoPlayer.setPlayWhenReady(playOnReady);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
audioReactor = new AudioReactor(context, simpleExoPlayer); audioReactor = new AudioReactor(context, simpleExoPlayer);
@ -237,15 +237,16 @@ public abstract class BasePlayer implements
final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch());
// Good to go... // Good to go...
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch); initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, /*playOnInit=*/true);
} }
protected void initPlayback(@NonNull final PlayQueue queue, protected void initPlayback(@NonNull final PlayQueue queue,
@Player.RepeatMode final int repeatMode, @Player.RepeatMode final int repeatMode,
final float playbackSpeed, final float playbackSpeed,
final float playbackPitch) { final float playbackPitch,
final boolean playOnReady) {
destroyPlayer(); destroyPlayer();
initPlayer(); initPlayer(playOnReady);
setRepeatMode(repeatMode); setRepeatMode(repeatMode);
setPlaybackParameters(playbackSpeed, playbackPitch); setPlaybackParameters(playbackSpeed, playbackPitch);
@ -518,15 +519,16 @@ public abstract class BasePlayer implements
} }
public void triggerProgressUpdate() { public void triggerProgressUpdate() {
if (simpleExoPlayer == null) return;
onUpdateProgress( onUpdateProgress(
(int) simpleExoPlayer.getCurrentPosition(), Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
(int) simpleExoPlayer.getDuration(), (int) simpleExoPlayer.getDuration(),
simpleExoPlayer.getBufferedPercentage() simpleExoPlayer.getBufferedPercentage()
); );
} }
private Disposable getProgressReactor() { private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> triggerProgressUpdate()); .subscribe(ignored -> triggerProgressUpdate());
} }
@ -553,8 +555,8 @@ public abstract class BasePlayer implements
// Ensure dynamic/livestream timeline changes does not cause negative position // Ensure dynamic/livestream timeline changes does not cause negative position
if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) { if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) {
if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " + if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " +
"clamping position to 0ms."); "clamping to default position.");
seekTo(/*clampToTime=*/0); seekToDefault();
} }
break; break;
} }
@ -640,12 +642,12 @@ public abstract class BasePlayer implements
seekTo(recoveryPositionMillis); seekTo(recoveryPositionMillis);
playQueue.unsetRecovery(currentSourceIndex); playQueue.unsetRecovery(currentSourceIndex);
} else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) { } else if (isSynchronizing && isLive()) {
if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
// Is still synchronizing? // Is still synchronizing?
seekToDefault(); seekToDefault();
} else if (isSynchronizing && presetStartPositionMillis != 0L) { } else if (isSynchronizing && presetStartPositionMillis > 0L) {
if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " +
"position=[" + presetStartPositionMillis + "]"); "position=[" + presetStartPositionMillis + "]");
// Has another start position? // Has another start position?
@ -700,41 +702,23 @@ public abstract class BasePlayer implements
} }
} }
/**
* Processes {@link ExoPlaybackException} tagged with {@link ExoPlaybackException#TYPE_SOURCE}.
* <br><br>
* If the current {@link com.google.android.exoplayer2.Timeline.Window window} is valid,
* then we know the error is produced by transitioning into a bad window, therefore we report
* an error to the play queue based on if the current error can be skipped.
* <br><br>
* This is done because ExoPlayer reports the source exceptions before window is
* transitioned on seamless playback. Because player error causes ExoPlayer to go
* back to {@link Player#STATE_IDLE STATE_IDLE}, we reset and prepare the media source
* again to resume playback.
* <br><br>
* In the event that this error is produced during a valid stream playback, we save the
* current position so the playback may be recovered and resumed manually by the user. This
* happens only if the playback is {@link #RECOVERY_SKIP_THRESHOLD} milliseconds until complete.
* <br><br>
* In the event of livestreaming being lagged behind for any reason, most notably pausing for
* too long, a {@link BehindLiveWindowException} will be produced. This will trigger a reload
* instead of skipping or removal.
* */
private void processSourceError(final IOException error) { private void processSourceError(final IOException error) {
if (simpleExoPlayer == null || playQueue == null) return; if (simpleExoPlayer == null || playQueue == null) return;
if (simpleExoPlayer.getCurrentPosition() <
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) {
setRecovery(); setRecovery();
}
final Throwable cause = error.getCause(); final Throwable cause = error.getCause();
if (cause instanceof BehindLiveWindowException) { if (cause instanceof BehindLiveWindowException) {
reload(); reload();
} else if (cause instanceof UnknownHostException) { } else if (cause instanceof UnknownHostException) {
playQueue.error(/*isNetworkProblem=*/true); playQueue.error(/*isNetworkProblem=*/true);
} else if (isCurrentWindowValid()) {
playQueue.error(/*isTransitioningToBadStream=*/true);
} else if (cause instanceof FailedMediaSource.MediaSourceResolutionException) {
playQueue.error(/*recoverableWithNoAvailableStream=*/false);
} else if (cause instanceof FailedMediaSource.StreamInfoLoadException) {
playQueue.error(/*recoverableIfLoadFailsWhenNetworkIsFine=*/false);
} else { } else {
playQueue.error(isCurrentWindowValid()); playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true);
} }
} }
@ -787,9 +771,10 @@ public abstract class BasePlayer implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public boolean isNearPlaybackEdge(final long timeToEndMillis) { public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
// If live, then not near playback edge // If live, then not near playback edge
if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false; // If not playing, then not approaching playback edge
if (simpleExoPlayer == null || isLive() || !isPlaying()) return false;
final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); final long currentPositionMillis = simpleExoPlayer.getCurrentPosition();
final long currentDurationMillis = simpleExoPlayer.getDuration(); final long currentDurationMillis = simpleExoPlayer.getDuration();
@ -985,22 +970,22 @@ public abstract class BasePlayer implements
public void onFastRewind() { public void onFastRewind() {
if (DEBUG) Log.d(TAG, "onFastRewind() called"); if (DEBUG) Log.d(TAG, "onFastRewind() called");
seekBy(-FAST_FORWARD_REWIND_AMOUNT); seekBy(-FAST_FORWARD_REWIND_AMOUNT_MILLIS);
} }
public void onFastForward() { public void onFastForward() {
if (DEBUG) Log.d(TAG, "onFastForward() called"); if (DEBUG) Log.d(TAG, "onFastForward() called");
seekBy(FAST_FORWARD_REWIND_AMOUNT); seekBy(FAST_FORWARD_REWIND_AMOUNT_MILLIS);
} }
public void onPlayPrevious() { public void onPlayPrevious() {
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");
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds,
* restart current track. Also restart the track if the current track * restart current track. Also restart the track if the current track
* is the first in a queue.*/ * is the first in a queue.*/
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS ||
playQueue.getIndex() == 0) { playQueue.getIndex() == 0) {
seekToDefault(); seekToDefault();
playQueue.offsetIndex(0); playQueue.offsetIndex(0);
@ -1050,7 +1035,9 @@ public abstract class BasePlayer implements
} }
public void seekToDefault() { public void seekToDefault() {
if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition(); if (simpleExoPlayer != null) {
simpleExoPlayer.seekToDefaultPosition();
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -1091,9 +1078,9 @@ public abstract class BasePlayer implements
private void savePlaybackState() { private void savePlaybackState() {
if (simpleExoPlayer == null || currentInfo == null) return; if (simpleExoPlayer == null || currentInfo == null) return;
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD && if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS &&
simpleExoPlayer.getCurrentPosition() < simpleExoPlayer.getCurrentPosition() <
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) {
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
} }
} }
@ -1127,9 +1114,7 @@ public abstract class BasePlayer implements
/** Checks if the current playback is a livestream AND is playing at or beyond the live edge */ /** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
public boolean isLiveEdge() { public boolean isLiveEdge() {
if (simpleExoPlayer == null) return false; if (simpleExoPlayer == null || !isLive()) return false;
final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic();
if (!isLive) return false;
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline(); final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
@ -1143,6 +1128,16 @@ public abstract class BasePlayer implements
return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
} }
public boolean isLive() {
if (simpleExoPlayer == null) return false;
try {
return simpleExoPlayer.isCurrentWindowDynamic();
} catch (@NonNull IndexOutOfBoundsException ignored) {
// Why would this even happen =(
return false;
}
}
public boolean isPlaying() { public boolean isPlaying() {
final int state = simpleExoPlayer.getPlaybackState(); final int state = simpleExoPlayer.getPlaybackState();
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
@ -1170,10 +1165,6 @@ public abstract class BasePlayer implements
setPlaybackParameters(speed, getPlaybackPitch()); setPlaybackParameters(speed, getPlaybackPitch());
} }
public void setPlaybackPitch(float pitch) {
setPlaybackParameters(getPlaybackSpeed(), pitch);
}
public PlaybackParameters getPlaybackParameters() { public PlaybackParameters getPlaybackParameters() {
final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f); final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f);
if (simpleExoPlayer == null) return defaultParameters; if (simpleExoPlayer == null) return defaultParameters;

View File

@ -30,8 +30,10 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.provider.Settings; import android.provider.Settings;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper; import android.support.v7.widget.helper.ItemTouchHelper;
@ -50,6 +52,7 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.ui.SubtitleView;
@ -59,7 +62,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
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;
@ -95,12 +97,12 @@ public final class MainVideoPlayer extends AppCompatActivity
private GestureDetector gestureDetector; private GestureDetector gestureDetector;
private boolean activityPaused;
private VideoPlayerImpl playerImpl; private VideoPlayerImpl playerImpl;
private SharedPreferences defaultPreferences; private SharedPreferences defaultPreferences;
@Nullable private StateSaver.SavedState savedState; @Nullable private PlayerState playerState;
private boolean isInMultiWindow;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Activity LifeCycle // Activity LifeCycle
@ -135,8 +137,9 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override @Override
protected void onRestoreInstanceState(@NonNull Bundle bundle) { protected void onRestoreInstanceState(@NonNull Bundle bundle) {
if (DEBUG) Log.d(TAG, "onRestoreInstanceState() called");
super.onRestoreInstanceState(bundle); super.onRestoreInstanceState(bundle);
savedState = StateSaver.tryToRestore(bundle, this); StateSaver.tryToRestore(bundle, this);
} }
@Override @Override
@ -148,26 +151,28 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume();
if (DEBUG) Log.d(TAG, "onResume() called"); if (DEBUG) Log.d(TAG, "onResume() called");
if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() super.onResume();
&& !playerImpl.isPlaying()) {
playerImpl.onPlay();
}
activityPaused = false;
if(globalScreenOrientationLocked()) { if (globalScreenOrientationLocked()) {
boolean lastOrientationWasLandscape boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
= defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false); getString(R.string.last_orientation_landscape_key), false);
setLandscape(lastOrientationWasLandscape); setLandscape(lastOrientationWasLandscape);
} }
}
@Override // Upon going in or out of multiwindow mode, isInMultiWindow will always be false,
public void onBackPressed() { // since the first onResume needs to restore the player.
if (DEBUG) Log.d(TAG, "onBackPressed() called"); // Subsequent onResume calls while multiwindow mode remains the same and the player is
super.onBackPressed(); // prepared should be ignored.
if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); if (isInMultiWindow) return;
isInMultiWindow = isInMultiWindow();
if (playerState != null) {
playerImpl.setPlaybackQuality(playerState.getPlaybackQuality());
playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(),
playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(),
playerState.wasPlaying());
}
} }
@Override @Override
@ -180,33 +185,24 @@ public final class MainVideoPlayer extends AppCompatActivity
} }
} }
@Override
protected void onPause() {
super.onPause();
if (DEBUG) Log.d(TAG, "onPause() called");
if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) {
playerImpl.wasPlaying = playerImpl.isPlaying();
playerImpl.onPause();
}
activityPaused = true;
}
@Override @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called");
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
if (playerImpl == null) return; if (playerImpl == null) return;
playerImpl.setRecovery(); playerImpl.setRecovery();
savedState = StateSaver.tryToSave(isChangingConfigurations(), savedState, playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
outState, this); playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
playerImpl.getPlaybackQuality(), playerImpl.isPlaying());
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
} }
@Override @Override
protected void onDestroy() { protected void onStop() {
super.onDestroy(); if (DEBUG) Log.d(TAG, "onStop() called");
if (DEBUG) Log.d(TAG, "onDestroy() called"); super.onStop();
if (playerImpl != null) playerImpl.destroy(); playerImpl.destroy();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -221,48 +217,19 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override @Override
public void writeTo(Queue<Object> objectsToSave) { public void writeTo(Queue<Object> objectsToSave) {
if (objectsToSave == null) return; if (objectsToSave == null) return;
objectsToSave.add(playerImpl.getPlayQueue()); objectsToSave.add(playerState);
objectsToSave.add(playerImpl.getRepeatMode());
objectsToSave.add(playerImpl.getPlaybackSpeed());
objectsToSave.add(playerImpl.getPlaybackPitch());
objectsToSave.add(playerImpl.getPlaybackQuality());
} }
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception { public void readFrom(@NonNull Queue<Object> savedObjects) {
@NonNull final PlayQueue queue = (PlayQueue) savedObjects.poll(); playerState = (PlayerState) savedObjects.poll();
final int repeatMode = (int) savedObjects.poll();
final float playbackSpeed = (float) savedObjects.poll();
final float playbackPitch = (float) savedObjects.poll();
@NonNull final String playbackQuality = (String) savedObjects.poll();
playerImpl.setPlaybackQuality(playbackQuality);
playerImpl.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch);
StateSaver.onDestroy(savedState);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// View // View
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
/**
* Prior to Kitkat, hiding system ui causes the player view to be overlaid and require two
* clicks to get rid of that invisible overlay. By showing the system UI on actions/events,
* that overlay is removed and the player view is put to the foreground.
*
* Post Kitkat, navbar and status bar can be pulled out by swiping the edge of
* screen, therefore, we can do nothing or hide the UI on actions/events.
* */
private void changeSystemUi() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
showSystemUi();
} else {
hideSystemUi();
}
}
private void showSystemUi() { private void showSystemUi() {
if (DEBUG) Log.d(TAG, "showSystemUi() called"); if (DEBUG) Log.d(TAG, "showSystemUi() called");
if (playerImpl != null && playerImpl.queueVisible) return; if (playerImpl != null && playerImpl.queueVisible) return;
@ -275,6 +242,14 @@ public final class MainVideoPlayer extends AppCompatActivity
} else { } else {
visibility = View.STATUS_BAR_VISIBLE; visibility = View.STATUS_BAR_VISIBLE;
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@ColorInt final int systenUiColor =
ActivityCompat.getColor(getApplicationContext(), R.color.video_overlay_color);
getWindow().setStatusBarColor(systenUiColor);
getWindow().setNavigationBarColor(systenUiColor);
}
getWindow().getDecorView().setSystemUiVisibility(visibility); getWindow().getDecorView().setSystemUiVisibility(visibility);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
} }
@ -342,6 +317,10 @@ public final class MainVideoPlayer extends AppCompatActivity
} }
} }
private boolean isInMultiWindow() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode();
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Playback Parameters Listener // Playback Parameters Listener
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -411,15 +390,6 @@ public final class MainVideoPlayer extends AppCompatActivity
this.itemsListCloseButton = findViewById(R.id.playQueueClose); this.itemsListCloseButton = findViewById(R.id.playQueueClose);
this.itemsList = findViewById(R.id.playQueue); this.itemsList = findViewById(R.id.playQueue);
this.windowRootLayout = rootView.findViewById(R.id.playbackWindowRoot);
// Prior to Kitkat, there is no way of setting translucent navbar programmatically.
// Thus, fit system windows is opted instead.
// See https://stackoverflow.com/questions/29069070/completely-transparent-status-bar-and-navigation-bar-on-lollipop
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
windowRootLayout.setFitsSystemWindows(false);
windowRootLayout.invalidate();
}
titleTextView.setSelected(true); titleTextView.setSelected(true);
channelTextView.setSelected(true); channelTextView.setSelected(true);
@ -428,20 +398,15 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override @Override
protected void setupSubtitleView(@NonNull SubtitleView view, protected void setupSubtitleView(@NonNull SubtitleView view,
@NonNull String captionSizeKey) { final float captionScale,
final float captionRatioInverse; @NonNull final CaptionStyleCompat captionStyle) {
if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) {
captionRatioInverse = 22f;
} else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) {
captionRatioInverse = 18f;
} else {
captionRatioInverse = 20f;
}
final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
final float captionRatioInverse = 20f + 4f * (1f - captionScale);
view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX,
(float) minimumLength / captionRatioInverse); (float) minimumLength / captionRatioInverse);
view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT));
view.setStyle(captionStyle);
} }
@Override @Override
@ -727,7 +692,7 @@ public final class MainVideoPlayer extends AppCompatActivity
animatePlayButtons(true, 200); animatePlayButtons(true, 200);
}); });
changeSystemUi(); showSystemUi();
getRootView().setKeepScreenOn(false); getRootView().setKeepScreenOn(false);
} }
@ -900,7 +865,7 @@ public final class MainVideoPlayer extends AppCompatActivity
playerImpl.hideControls(150, 0); playerImpl.hideControls(150, 0);
} else { } else {
playerImpl.showControlsThenHide(); playerImpl.showControlsThenHide();
changeSystemUi(); showSystemUi();
} }
return true; return true;
} }

View File

@ -0,0 +1,88 @@
package org.schabi.newpipe.player;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import org.schabi.newpipe.playlist.PlayQueue;
import java.io.Serializable;
public class PlayerState implements Serializable {
private final static String TAG = "PlayerState";
@NonNull private final PlayQueue playQueue;
private final int repeatMode;
private final float playbackSpeed;
private final float playbackPitch;
@Nullable private final String playbackQuality;
private final boolean wasPlaying;
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) {
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying);
}
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch,
@Nullable final String playbackQuality, final boolean wasPlaying) {
this.playQueue = playQueue;
this.repeatMode = repeatMode;
this.playbackSpeed = playbackSpeed;
this.playbackPitch = playbackPitch;
this.playbackQuality = playbackQuality;
this.wasPlaying = wasPlaying;
}
/*//////////////////////////////////////////////////////////////////////////
// Serdes
//////////////////////////////////////////////////////////////////////////*/
@Nullable
public static PlayerState fromJson(@NonNull final String json) {
try {
return new Gson().fromJson(json, PlayerState.class);
} catch (JsonSyntaxException error) {
Log.e(TAG, "Failed to deserialize PlayerState from json=[" + json + "]", error);
return null;
}
}
@NonNull
public String toJson() {
return new Gson().toJson(this);
}
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
@NonNull
public PlayQueue getPlayQueue() {
return playQueue;
}
public int getRepeatMode() {
return repeatMode;
}
public float getPlaybackSpeed() {
return playbackSpeed;
}
public float getPlaybackPitch() {
return playbackPitch;
}
@Nullable
public String getPlaybackQuality() {
return playbackQuality;
}
public boolean wasPlaying() {
return wasPlaying;
}
}

View File

@ -52,6 +52,7 @@ import android.widget.TextView;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.ui.SubtitleView;
@ -397,14 +398,12 @@ public final class PopupVideoPlayer extends Service {
@Override @Override
protected void setupSubtitleView(@NonNull SubtitleView view, protected void setupSubtitleView(@NonNull SubtitleView view,
@NonNull String captionSizeKey) { final float captionScale,
float captionRatio = SubtitleView.DEFAULT_TEXT_SIZE_FRACTION; @NonNull final CaptionStyleCompat captionStyle) {
if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) { float captionRatio = (captionScale - 1f) / 5f + 1f;
captionRatio *= 0.9; view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
} else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) { view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT));
captionRatio *= 1.1; view.setStyle(captionStyle);
}
view.setFractionalTextSize(captionRatio);
} }
@Override @Override

View File

@ -32,6 +32,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.playlist.PlayQueueAdapter;
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;
@ -40,6 +41,9 @@ 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;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch; import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
@ -151,7 +155,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
finish(); finish();
return true; return true;
case R.id.action_append_playlist: case R.id.action_append_playlist:
appendToPlaylist(); appendAllToPlaylist();
return true; return true;
case R.id.action_settings: case R.id.action_settings:
NavigationHelper.openSettings(this); NavigationHelper.openSettings(this);
@ -187,13 +191,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} }
private void appendToPlaylist() {
if (this.player != null && this.player.getPlayQueue() != null) {
PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams())
.show(getSupportFragmentManager(), getTag());
}
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Service Connection // Service Connection
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -319,7 +316,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void buildItemPopupMenu(final PlayQueueItem item, final View view) { private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu menu = new PopupMenu(this, view); final PopupMenu menu = new PopupMenu(this, view);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, R.string.play_queue_remove); final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/0,
Menu.NONE, R.string.play_queue_remove);
remove.setOnMenuItemClickListener(menuItem -> { remove.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false; if (player == null) return false;
@ -328,12 +326,20 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true; return true;
}); });
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, R.string.play_queue_stream_detail); final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/1,
Menu.NONE, R.string.play_queue_stream_detail);
detail.setOnMenuItemClickListener(menuItem -> { detail.setOnMenuItemClickListener(menuItem -> {
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle()); onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
return true; return true;
}); });
final MenuItem append = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/2,
Menu.NONE, R.string.append_playlist);
append.setOnMenuItemClickListener(menuItem -> {
openPlaylistAppendDialog(Collections.singletonList(item));
return true;
});
menu.show(); menu.show();
} }
@ -488,6 +494,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
seeking = false; seeking = false;
} }
////////////////////////////////////////////////////////////////////////////
// Playlist append
////////////////////////////////////////////////////////////////////////////
private void appendAllToPlaylist() {
if (player != null && player.getPlayQueue() != null) {
openPlaylistAppendDialog(player.getPlayQueue().getStreams());
}
}
private void openPlaylistAppendDialog(final List<PlayQueueItem> playlist) {
PlaylistAppendDialog.fromPlayQueueItems(playlist)
.show(getSupportFragmentManager(), getTag());
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Binding Service Listener // Binding Service Listener
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -497,6 +518,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
onStateChanged(state); onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled); onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters); onPlaybackParameterChanged(parameters);
onMaybePlaybackAdapterChanged();
} }
@Override @Override
@ -609,4 +631,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
playbackPitchButton.setText(formatPitch(parameters.pitch)); playbackPitchButton.setText(formatPitch(parameters.pitch));
} }
} }
private void onMaybePlaybackAdapterChanged() {
if (itemsList == null || player == null) return;
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) {
itemsList.setAdapter(maybeNewAdapter);
}
}
} }

View File

@ -32,7 +32,6 @@ import android.graphics.PorterDuff;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
@ -55,6 +54,7 @@ import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.ui.SubtitleView;
@ -189,10 +189,10 @@ public abstract class VideoPlayer extends BasePlayer
this.qualityTextView = rootView.findViewById(R.id.qualityTextView); this.qualityTextView = rootView.findViewById(R.id.qualityTextView);
this.subtitleView = rootView.findViewById(R.id.subtitleView); this.subtitleView = rootView.findViewById(R.id.subtitleView);
final String captionSizeKey = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.caption_size_key), final float captionScale = PlayerHelper.getCaptionScale(context);
context.getString(R.string.caption_size_default)); final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context);
setupSubtitleView(subtitleView, captionSizeKey); setupSubtitleView(subtitleView, captionScale, captionStyle);
this.resizeView = rootView.findViewById(R.id.resizeTextView); this.resizeView = rootView.findViewById(R.id.resizeTextView);
resizeView.setText(PlayerHelper.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode())); resizeView.setText(PlayerHelper.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode()));
@ -214,7 +214,8 @@ public abstract class VideoPlayer extends BasePlayer
} }
protected abstract void setupSubtitleView(@NonNull SubtitleView view, protected abstract void setupSubtitleView(@NonNull SubtitleView view,
@NonNull String captionSizeKey); final float captionScale,
@NonNull final CaptionStyleCompat captionStyle);
@Override @Override
public void initListeners() { public void initListeners() {
@ -228,8 +229,8 @@ public abstract class VideoPlayer extends BasePlayer
} }
@Override @Override
public void initPlayer() { public void initPlayer(final boolean playOnReady) {
super.initPlayer(); super.initPlayer(playOnReady);
// Setup video view // Setup video view
simpleExoPlayer.setVideoSurfaceView(surfaceView); simpleExoPlayer.setVideoSurfaceView(surfaceView);

View File

@ -1,8 +1,12 @@
package org.schabi.newpipe.player.helper; package org.schabi.newpipe.player.helper;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.media.session.MediaButtonReceiver;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
import android.view.KeyEvent;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
@ -15,8 +19,8 @@ import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
public class MediaSessionManager { public class MediaSessionManager {
private static final String TAG = "MediaSessionManager"; private static final String TAG = "MediaSessionManager";
private final MediaSessionCompat mediaSession; @NonNull private final MediaSessionCompat mediaSession;
private final MediaSessionConnector sessionConnector; @NonNull private final MediaSessionConnector sessionConnector;
public MediaSessionManager(@NonNull final Context context, public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player, @NonNull final Player player,
@ -28,11 +32,9 @@ public class MediaSessionManager {
this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer()); this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer());
} }
public MediaSessionCompat getMediaSession() { @Nullable
return mediaSession; @SuppressWarnings("UnusedReturnValue")
} public KeyEvent handleMediaButtonIntent(final Intent intent) {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
public MediaSessionConnector getSessionConnector() {
return sessionConnector;
} }
} }

View File

@ -2,12 +2,18 @@ package org.schabi.newpipe.player.helper;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.view.accessibility.CaptioningManager;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -203,6 +209,16 @@ public class PlayerHelper {
return 60000; return 60000;
} }
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
@NonNull final BandwidthMeter meter) {
return new AdaptiveTrackSelection.Factory(meter,
AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE,
/*bufferDurationRequiredForQualityIncrease=*/1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION);
}
public static boolean isUsingDSP(@NonNull final Context context) { public static boolean isUsingDSP(@NonNull final Context context) {
return true; return true;
} }
@ -215,6 +231,35 @@ public class PlayerHelper {
return 2500; return 2500;
} }
@NonNull
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return CaptionStyleCompat.DEFAULT;
final CaptioningManager captioningManager = (CaptioningManager)
context.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager == null || !captioningManager.isEnabled()) {
return CaptionStyleCompat.DEFAULT;
}
return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
}
/**
* System font scaling:
* Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f
* */
@NonNull
public static float getCaptionScale(@NonNull final Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1f;
final CaptioningManager captioningManager = (CaptioningManager)
context.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager == null || !captioningManager.isEnabled()) {
return 1f;
}
return captioningManager.getFontScale();
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Private helpers // Private helpers
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View File

@ -14,13 +14,35 @@ import java.io.IOException;
public class FailedMediaSource implements ManagedMediaSource { public class FailedMediaSource implements ManagedMediaSource {
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
public static class FailedMediaSourceException extends Exception {
FailedMediaSourceException(String message) {
super(message);
}
FailedMediaSourceException(Throwable cause) {
super(cause);
}
}
public static final class MediaSourceResolutionException extends FailedMediaSourceException {
public MediaSourceResolutionException(String message) {
super(message);
}
}
public static final class StreamInfoLoadException extends FailedMediaSourceException {
public StreamInfoLoadException(Throwable cause) {
super(cause);
}
}
private final PlayQueueItem playQueueItem; private final PlayQueueItem playQueueItem;
private final Throwable error; private final FailedMediaSourceException error;
private final long retryTimestamp; private final long retryTimestamp;
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final Throwable error, @NonNull final FailedMediaSourceException error,
final long retryTimestamp) { final long retryTimestamp) {
this.playQueueItem = playQueueItem; this.playQueueItem = playQueueItem;
this.error = error; this.error = error;
@ -32,7 +54,7 @@ public class FailedMediaSource implements ManagedMediaSource {
* The error will always be propagated to ExoPlayer. * The error will always be propagated to ExoPlayer.
* */ * */
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final Throwable error) { @NonNull final FailedMediaSourceException error) {
this.playQueueItem = playQueueItem; this.playQueueItem = playQueueItem;
this.error = error; this.error = error;
this.retryTimestamp = Long.MAX_VALUE; this.retryTimestamp = Long.MAX_VALUE;
@ -42,7 +64,7 @@ public class FailedMediaSource implements ManagedMediaSource {
return playQueueItem; return playQueueItem;
} }
public Throwable getError() { public FailedMediaSourceException getError() {
return error; return error;
} }

View File

@ -0,0 +1,135 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
public class ManagedMediaSourcePlaylist {
@NonNull private final DynamicConcatenatingMediaSource internalSource;
public ManagedMediaSourcePlaylist() {
internalSource = new DynamicConcatenatingMediaSource(/*isPlaylistAtomic=*/false,
new ShuffleOrder.UnshuffledShuffleOrder(0));
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Delegations
//////////////////////////////////////////////////////////////////////////*/
public int size() {
return internalSource.getSize();
}
/**
* Returns the {@link ManagedMediaSource} at the given index of the playlist.
* If the index is invalid, then null is returned.
* */
@Nullable
public ManagedMediaSource get(final int index) {
return (index < 0 || index >= size()) ?
null : (ManagedMediaSource) internalSource.getMediaSource(index);
}
public void dispose() {
internalSource.releaseSource();
}
@NonNull
public DynamicConcatenatingMediaSource getParentMediaSource() {
return internalSource;
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist Manipulation
//////////////////////////////////////////////////////////////////////////*/
/**
* Expands the {@link DynamicConcatenatingMediaSource} by appending it with a
* {@link PlaceholderMediaSource}.
*
* @see #append(ManagedMediaSource)
* */
public synchronized void expand() {
append(new PlaceholderMediaSource());
}
/**
* Appends a {@link ManagedMediaSource} to the end of {@link DynamicConcatenatingMediaSource}.
* @see DynamicConcatenatingMediaSource#addMediaSource
* */
public synchronized void append(@NonNull final ManagedMediaSource source) {
internalSource.addMediaSource(source);
}
/**
* Removes a {@link ManagedMediaSource} from {@link DynamicConcatenatingMediaSource}
* at the given index. If this index is out of bound, then the removal is ignored.
* @see DynamicConcatenatingMediaSource#removeMediaSource(int)
* */
public synchronized void remove(final int index) {
if (index < 0 || index > internalSource.getSize()) return;
internalSource.removeMediaSource(index);
}
/**
* Moves a {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
* from the given source index to the target index. If either index is out of bound,
* then the call is ignored.
* @see DynamicConcatenatingMediaSource#moveMediaSource(int, int)
* */
public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= internalSource.getSize() || target >= internalSource.getSize()) return;
internalSource.moveMediaSource(source, target);
}
/**
* Invalidates the {@link ManagedMediaSource} at the given index by replacing it
* with a {@link PlaceholderMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable)
* */
public synchronized void invalidate(final int index,
@Nullable final Runnable finalizingAction) {
if (get(index) instanceof PlaceholderMediaSource) return;
update(index, new PlaceholderMediaSource(), finalizingAction);
}
/**
* Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable)
* */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source) {
update(index, source, /*doNothing=*/null);
}
/**
* Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
* then the replacement is ignored.
* @see DynamicConcatenatingMediaSource#addMediaSource
* @see DynamicConcatenatingMediaSource#removeMediaSource(int, Runnable)
* */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source,
@Nullable final Runnable finalizingAction) {
if (index < 0 || index >= internalSource.getSize()) return;
// Add and remove are sequential on the same thread, therefore here, the exoplayer
// message queue must receive and process add before remove, effectively treating them
// as atomic.
// Since the finalizing action occurs strictly after the timeline has completed
// all its changes on the playback thread, thus, it is possible, in the meantime,
// other calls that modifies the playlist media source occur in between. This makes
// it unsafe to call remove as the finalizing action of add.
internalSource.addMediaSource(index + 1, source);
// Because of the above race condition, it is thus only safe to synchronize the player
// in the finalizing action AFTER the removal is complete and the timeline has changed.
internalSource.removeMediaSource(index, finalizingAction);
}
}

View File

@ -2,11 +2,11 @@ package org.schabi.newpipe.player.playback;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.util.ArraySet;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
@ -14,6 +14,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.mediasource.LoadedMediaSource; import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSource; import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource; import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
@ -23,8 +24,10 @@ 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 org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
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;
@ -37,8 +40,11 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.SerialDisposable; import io.reactivex.disposables.SerialDisposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.internal.subscriptions.EmptySubscription;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager { public class MediaSourceManager {
@ -52,7 +58,6 @@ 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, Runnable)
* */ * */
private final static int WINDOW_SIZE = 1; private final static int WINDOW_SIZE = 1;
@ -103,7 +108,7 @@ public class MediaSourceManager {
@NonNull private final AtomicBoolean isBlocked; @NonNull private final AtomicBoolean isBlocked;
@NonNull private DynamicConcatenatingMediaSource sources; @NonNull private ManagedMediaSourcePlaylist playlist;
public MediaSourceManager(@NonNull final PlaybackListener listener, public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) { @NonNull final PlayQueue playQueue) {
@ -143,9 +148,9 @@ public class MediaSourceManager {
this.isBlocked = new AtomicBoolean(false); this.isBlocked = new AtomicBoolean(false);
this.sources = new DynamicConcatenatingMediaSource(); this.playlist = new ManagedMediaSourcePlaylist();
this.loadingItems = Collections.synchronizedSet(new HashSet<>()); this.loadingItems = Collections.synchronizedSet(new ArraySet<>());
playQueue.getBroadcastReceiver() playQueue.getBroadcastReceiver()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -167,7 +172,7 @@ public class MediaSourceManager {
playQueueReactor.cancel(); playQueueReactor.cancel();
loaderReactor.dispose(); loaderReactor.dispose();
syncReactor.dispose(); syncReactor.dispose();
sources.releaseSource(); playlist.dispose();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -215,17 +220,18 @@ public class MediaSourceManager {
break; break;
case REMOVE: case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event; final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex()); playlist.remove(removeEvent.getRemoveIndex());
break; break;
case MOVE: case MOVE:
final MoveEvent moveEvent = (MoveEvent) event; final MoveEvent moveEvent = (MoveEvent) event;
move(moveEvent.getFromIndex(), moveEvent.getToIndex()); playlist.move(moveEvent.getFromIndex(), moveEvent.getToIndex());
break; break;
case REORDER: case REORDER:
// Need to move to ensure the playing index from play queue matches that of // Need to move to ensure the playing index from play queue matches that of
// the source timeline, and then window correction can take care of the rest // the source timeline, and then window correction can take care of the rest
final ReorderEvent reorderEvent = (ReorderEvent) event; final ReorderEvent reorderEvent = (ReorderEvent) event;
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); playlist.move(reorderEvent.getFromSelectedIndex(),
reorderEvent.getToSelectedIndex());
break; break;
case RECOVERY: case RECOVERY:
default: default:
@ -266,10 +272,11 @@ public class MediaSourceManager {
} }
private boolean isPlaybackReady() { private boolean isPlaybackReady() {
if (sources.getSize() != playQueue.size()) return false; if (playlist.size() != playQueue.size()) return false;
final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex());
if (mediaSource == null) return false;
final ManagedMediaSource mediaSource =
(ManagedMediaSource) sources.getMediaSource(playQueue.getIndex());
final PlayQueueItem playQueueItem = playQueue.getItem(); final PlayQueueItem playQueueItem = playQueue.getItem();
return mediaSource.isStreamEqual(playQueueItem); return mediaSource.isStreamEqual(playQueueItem);
} }
@ -288,9 +295,9 @@ public class MediaSourceManager {
private void maybeUnblock() { private void maybeUnblock() {
if (DEBUG) Log.d(TAG, "maybeUnblock() called."); if (DEBUG) Log.d(TAG, "maybeUnblock() called.");
if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) { if (isBlocked.get()) {
isBlocked.set(false); isBlocked.set(false);
playbackListener.onPlaybackUnblock(sources); playbackListener.onPlaybackUnblock(playlist.getParentMediaSource());
} }
} }
@ -299,10 +306,10 @@ public class MediaSourceManager {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void maybeSync() { private void maybeSync() {
if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); if (DEBUG) Log.d(TAG, "maybeSync() called.");
final PlayQueueItem currentItem = playQueue.getItem(); final PlayQueueItem currentItem = playQueue.getItem();
if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return; if (isBlocked.get() || currentItem == null) return;
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null); final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
@ -321,10 +328,12 @@ public class MediaSourceManager {
} }
} }
private void maybeSynchronizePlayer() { private synchronized void maybeSynchronizePlayer() {
if (isPlayQueueReady() && isPlaybackReady()) {
maybeUnblock(); maybeUnblock();
maybeSync(); maybeSync();
} }
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// MediaSource Loading // MediaSource Loading
@ -332,12 +341,14 @@ public class MediaSourceManager {
private Observable<Long> getEdgeIntervalSignal() { private Observable<Long> getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
.filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis)); .filter(ignored ->
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
} }
private Disposable getDebouncedLoader() { private Disposable getDebouncedLoader() {
return debouncedSignal.mergeWith(nearEndIntervalSignal) return debouncedSignal.mergeWith(nearEndIntervalSignal)
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate()); .subscribe(timestamp -> loadImmediate());
} }
@ -348,42 +359,21 @@ public class MediaSourceManager {
private void loadImmediate() { private void loadImmediate() {
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called"); if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
// The current item has higher priority final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue, WINDOW_SIZE);
final int currentIndex = playQueue.getIndex(); if (itemsToLoad == null) return;
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return;
// Evict the items being loaded to free up memory // Evict the previous items being loaded to free up memory, before start loading new ones
if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) { maybeClearLoaders();
loaderReactor.clear();
loadingItems.clear();
}
maybeLoadItem(currentItem);
// The rest are just for seamless playback maybeLoadItem(itemsToLoad.center);
// Although timeline is not updated prior to the current index, these sources are still for (final PlayQueueItem item : itemsToLoad.neighbors) {
// loaded into the cache for faster retrieval at a potentially later time.
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 Set<PlayQueueItem> items = new HashSet<>(
playQueue.getStreams().subList(leftBound,rightBound));
// Do a round robin
final int excess = rightLimit - playQueue.size();
if (excess >= 0) {
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
}
items.remove(currentItem);
for (final PlayQueueItem item : items) {
maybeLoadItem(item); maybeLoadItem(item);
} }
} }
private void maybeLoadItem(@NonNull final PlayQueueItem item) { private void maybeLoadItem(@NonNull final PlayQueueItem item) {
if (DEBUG) Log.d(TAG, "maybeLoadItem() called."); if (DEBUG) Log.d(TAG, "maybeLoadItem() called.");
if (playQueue.indexOf(item) >= sources.getSize()) return; if (playQueue.indexOf(item) >= playlist.size()) return;
if (!loadingItems.contains(item) && isCorrectionNeeded(item)) { if (!loadingItems.contains(item) && isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() + if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() +
@ -402,19 +392,19 @@ public class MediaSourceManager {
return stream.getStream().map(streamInfo -> { return stream.getStream().map(streamInfo -> {
final MediaSource source = playbackListener.sourceOf(stream, streamInfo); final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
if (source == null) { if (source == null) {
final Exception exception = new IllegalStateException( final String message = "Unable to resolve source from stream info." +
"Unable to resolve source from stream info." +
" URL: " + stream.getUrl() + " URL: " + stream.getUrl() +
", audio count: " + streamInfo.getAudioStreams().size() + ", audio count: " + streamInfo.getAudioStreams().size() +
", video count: " + streamInfo.getVideoOnlyStreams().size() + ", video count: " + streamInfo.getVideoOnlyStreams().size() +
streamInfo.getVideoStreams().size()); streamInfo.getVideoStreams().size();
return new FailedMediaSource(stream, exception); return new FailedMediaSource(stream, new MediaSourceResolutionException(message));
} }
final long expiration = System.currentTimeMillis() + final long expiration = System.currentTimeMillis() +
ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); 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,
new StreamInfoLoadException(throwable)));
} }
private void onMediaSourceReceived(@NonNull final PlayQueueItem item, private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
@ -426,10 +416,10 @@ public class MediaSourceManager {
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 (isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " + if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " +
"title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]"); "title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
update(itemIndex, mediaSource, this::maybeSynchronizePlayer); playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
} }
} }
@ -445,10 +435,8 @@ public class MediaSourceManager {
* */ * */
private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) {
final int index = playQueue.indexOf(item); final int index = playQueue.indexOf(item);
if (index == -1 || index >= sources.getSize()) return false; final ManagedMediaSource mediaSource = playlist.get(index);
return mediaSource != null && mediaSource.shouldBeReplacedWith(item,
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
return mediaSource.shouldBeReplacedWith(item,
/*mightBeInProgress=*/index != playQueue.getIndex()); /*mightBeInProgress=*/index != playQueue.getIndex());
} }
@ -465,10 +453,9 @@ public class MediaSourceManager {
* */ * */
private void maybeRenewCurrentIndex() { private void maybeRenewCurrentIndex() {
final int currentIndex = playQueue.getIndex(); final int currentIndex = playQueue.getIndex();
if (sources.getSize() <= currentIndex) return; final ManagedMediaSource currentSource = playlist.get(currentIndex);
if (currentSource == null) return;
final ManagedMediaSource currentSource =
(ManagedMediaSource) sources.getMediaSource(currentIndex);
final PlayQueueItem currentItem = playQueue.getItem(); final PlayQueueItem currentItem = playQueue.getItem();
if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) { if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) {
maybeSynchronizePlayer(); maybeSynchronizePlayer();
@ -477,7 +464,16 @@ public class MediaSourceManager {
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " + if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate); playlist.invalidate(currentIndex, this::loadImmediate);
}
private void maybeClearLoaders() {
if (DEBUG) Log.d(TAG, "MediaSource - maybeClearLoaders() called.");
if (!loadingItems.contains(playQueue.getItem()) &&
loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
loaderReactor.clear();
loadingItems.clear();
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Helpers // MediaSource Playlist Helpers
@ -486,72 +482,55 @@ public class MediaSourceManager {
private void resetSources() { private void resetSources() {
if (DEBUG) Log.d(TAG, "resetSources() called."); if (DEBUG) Log.d(TAG, "resetSources() called.");
this.sources.releaseSource(); playlist.dispose();
this.sources = new DynamicConcatenatingMediaSource(false, playlist = new ManagedMediaSourcePlaylist();
// Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
new ShuffleOrder.UnshuffledShuffleOrder(0));
} }
private void populateSources() { private void populateSources() {
if (DEBUG) Log.d(TAG, "populateSources() called."); if (DEBUG) Log.d(TAG, "populateSources() called.");
if (sources.getSize() >= playQueue.size()) return; while (playlist.size() < playQueue.size()) {
playlist.expand();
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
emplace(index, new PlaceholderMediaSource());
} }
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Manipulation // Manager Helpers
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Nullable
private static ItemsToLoad getItemsToLoad(@NonNull final PlayQueue playQueue,
final int windowSize) {
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return null;
/** // The rest are just for seamless playback
* Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource} // Although timeline is not updated prior to the current index, these sources are still
* with position in respect to the play queue only if no {@link MediaSource} // loaded into the cache for faster retrieval at a potentially later time.
* already exists at the given index. final int leftBound = Math.max(0, currentIndex - windowSize);
* */ final int rightLimit = currentIndex + windowSize + 1;
private synchronized void emplace(final int index, @NonNull final MediaSource source) { final int rightBound = Math.min(playQueue.size(), rightLimit);
if (index < sources.getSize()) return; final Set<PlayQueueItem> neighbors = new ArraySet<>(
playQueue.getStreams().subList(leftBound,rightBound));
sources.addMediaSource(index, source); // Do a round robin
final int excess = rightLimit - playQueue.size();
if (excess >= 0) {
neighbors.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
}
neighbors.remove(currentItem);
return new ItemsToLoad(currentItem, neighbors);
} }
/** private static class ItemsToLoad {
* Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource} @NonNull final private PlayQueueItem center;
* at the given index. If this index is out of bound, then the removal is ignored. @NonNull final private Collection<PlayQueueItem> neighbors;
* */
private synchronized void remove(final int index) {
if (index < 0 || index > sources.getSize()) return;
sources.removeMediaSource(index); ItemsToLoad(@NonNull final PlayQueueItem center,
@NonNull final Collection<PlayQueueItem> neighbors) {
this.center = center;
this.neighbors = neighbors;
} }
/**
* Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
* from the given source index to the target index. If either index is out of bound,
* then the call is ignored.
* */
private synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= sources.getSize() || target >= sources.getSize()) return;
sources.moveMediaSource(source, target);
}
/**
* Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
* at the given index with a given {@link MediaSource}. If the index is out of bound,
* then the replacement is ignored.
* <br><br>
* Not recommended to use on indices LESS THAN the currently playing index, since
* this will modify the playback timeline prior to the index and may cause desynchronization
* on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}.
* */
private synchronized void update(final int index, @NonNull final MediaSource source,
@Nullable final Runnable finalizingAction) {
if (index < 0 || index >= sources.getSize()) return;
sources.addMediaSource(index + 1, source, () ->
sources.removeMediaSource(index, finalizingAction));
} }
} }

View File

@ -13,13 +13,13 @@ 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. * Called to check if the currently playing stream is approaching the end of its playback.
* Implementation should return true when the current playback position is within * Implementation should return true when the current playback position is progressing within
* timeToEndMillis or less until its playback completes or transitions. * timeToEndMillis or less to its playback during.
* *
* May be called at any time. * May be called at any time.
* */ * */
boolean isNearPlaybackEdge(final long timeToEndMillis); boolean isApproachingPlaybackEdge(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.

View File

@ -1,17 +1,25 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
import android.util.Log;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
public class AppearanceSettingsFragment extends BasePreferenceFragment { public class AppearanceSettingsFragment extends BasePreferenceFragment {
private final static boolean CAPTIONING_SETTINGS_ACCESSIBLE =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
/** /**
* Theme that was applied when the settings was opened (or recreated after a theme change) * Theme that was applied when the settings was opened (or recreated after a theme change)
*/ */
private String startThemeKey; private String startThemeKey;
private String captionSettingsKey;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
@ -19,6 +27,12 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
String themeKey = getString(R.string.theme_key); String themeKey = getString(R.string.theme_key);
startThemeKey = defaultPreferences.getString(themeKey, getString(R.string.default_theme_value)); startThemeKey = defaultPreferences.getString(themeKey, getString(R.string.default_theme_value));
findPreference(themeKey).setOnPreferenceChangeListener(themePreferenceChange); findPreference(themeKey).setOnPreferenceChangeListener(themePreferenceChange);
captionSettingsKey = getString(R.string.caption_settings_key);
if (!CAPTIONING_SETTINGS_ACCESSIBLE) {
final Preference captionSettings = findPreference(captionSettingsKey);
getPreferenceScreen().removePreference(captionSettings);
}
} }
@Override @Override
@ -26,13 +40,23 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
addPreferencesFromResource(R.xml.appearance_settings); addPreferencesFromResource(R.xml.appearance_settings);
} }
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) {
startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS));
}
return super.onPreferenceTreeClick(preference);
}
private Preference.OnPreferenceChangeListener themePreferenceChange = new Preference.OnPreferenceChangeListener() { private Preference.OnPreferenceChangeListener themePreferenceChange = new Preference.OnPreferenceChangeListener() {
@Override @Override
public boolean onPreferenceChange(Preference preference, Object newValue) { public boolean onPreferenceChange(Preference preference, Object newValue) {
defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply(); defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply();
defaultPreferences.edit().putString(getString(R.string.theme_key), newValue.toString()).apply(); defaultPreferences.edit().putString(getString(R.string.theme_key), newValue.toString()).apply();
if (!newValue.equals(startThemeKey)) { // If it's not the current theme if (!newValue.equals(startThemeKey) && getActivity() != null) {
// If it's not the current theme
getActivity().recreate(); getActivity().recreate();
} }

View File

@ -3,5 +3,5 @@
<gradient <gradient
android:angle="90" android:angle="90"
android:endColor="#00000000" android:endColor="#00000000"
android:startColor="#8c000000"/> android:startColor="@color/video_overlay_color"/>
</shape> </shape>

View File

@ -3,5 +3,5 @@
<gradient <gradient
android:angle="-90" android:angle="-90"
android:endColor="#00000000" android:endColor="#00000000"
android:startColor="#8c000000"/> android:startColor="@color/video_overlay_color"/>
</shape> </shape>

View File

@ -304,7 +304,7 @@
android:paddingLeft="4dp" android:paddingLeft="4dp"
android:paddingRight="4dp" android:paddingRight="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/duration_live" android:text="@string/duration_live_button"
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
android:maxLength="4" android:maxLength="4"

View File

@ -129,7 +129,7 @@
android:id="@+id/playbackControlRoot" android:id="@+id/playbackControlRoot"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#64000000" android:background="@color/video_overlay_color"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
@ -406,7 +406,7 @@
android:paddingLeft="4dp" android:paddingLeft="4dp"
android:paddingRight="4dp" android:paddingRight="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/duration_live" android:text="@string/duration_live_button"
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:maxLength="4" android:maxLength="4"

View File

@ -154,7 +154,7 @@
android:paddingLeft="4dp" android:paddingLeft="4dp"
android:paddingRight="4dp" android:paddingRight="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/duration_live" android:text="@string/duration_live_button"
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
android:maxLength="4" android:maxLength="4"

View File

@ -198,7 +198,7 @@
android:paddingLeft="4dp" android:paddingLeft="4dp"
android:paddingRight="4dp" android:paddingRight="4dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:text="@string/duration_live" android:text="@string/duration_live_button"
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:maxLength="4" android:maxLength="4"

View File

@ -40,7 +40,7 @@
<color name="playlist_stream_count_background_color">#e6000000</color> <color name="playlist_stream_count_background_color">#e6000000</color>
<color name="duration_text_color">#EEFFFFFF</color> <color name="duration_text_color">#EEFFFFFF</color>
<color name="playlist_stream_count_text_color">#ffffff</color> <color name="playlist_stream_count_text_color">#ffffff</color>
<color name="video_overlay_color">#66000000</color> <color name="video_overlay_color">#64000000</color>
<color name="background_notification_color">#323232</color> <color name="background_notification_color">#323232</color>
<color name="background_title_color">#ffffff</color> <color name="background_title_color">#ffffff</color>

View File

@ -109,23 +109,7 @@
</string-array> </string-array>
<!-- Caption Size --> <!-- Caption Size -->
<string name="caption_size_key" translatable="false">caption_size_key</string> <string name="caption_settings_key" translatable="false">caption_settings_key</string>
<string name="caption_size_default" translatable="false">@string/normal_caption_size_key</string>
<string name="smaller_caption_size_key" translatable="false">smaller_caption_size</string>
<string name="normal_caption_size_key" translatable="false">normal_caption_size</string>
<string name="larger_caption_size_key" translatable="false">larger_caption_size</string>
<string-array name="caption_size_descriptions_list" translatable="false">
<item>@string/smaller_caption_font_size</item>
<item>@string/normal_caption_font_size</item>
<item>@string/larger_caption_font_size</item>
</string-array>
<string-array name="caption_size_values_list" translatable="false">
<item>@string/smaller_caption_size_key</item>
<item>@string/normal_caption_size_key</item>
<item>@string/larger_caption_size_key</item>
</string-array>
<!-- Content & History --> <!-- Content & History -->
<string name="show_search_suggestions_key" translatable="false">show_search_suggestions</string> <string name="show_search_suggestions_key" translatable="false">show_search_suggestions</string>

View File

@ -119,6 +119,7 @@
<string name="show_age_restricted_content_title">Show age restricted content</string> <string name="show_age_restricted_content_title">Show age restricted content</string>
<string name="video_is_age_restricted">Age Restricted Video. Allowing such material is possible from Settings.</string> <string name="video_is_age_restricted">Age Restricted Video. Allowing such material is possible from Settings.</string>
<string name="duration_live">live</string> <string name="duration_live">live</string>
<string name="duration_live_button" translatable="false">LIVE</string>
<string name="downloads">Downloads</string> <string name="downloads">Downloads</string>
<string name="downloads_title">Downloads</string> <string name="downloads_title">Downloads</string>
<string name="error_report_title">Error report</string> <string name="error_report_title">Error report</string>
@ -425,10 +426,10 @@
<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="smaller_caption_font_size">Smaller font</string> <!-- Caption Settings -->
<string name="normal_caption_font_size">Normal font</string> <string name="caption_setting_title">Caption</string>
<string name="larger_caption_font_size">Larger font</string> <string name="caption_setting_description">Modify player caption text scale and background styles. Require player restart to take effect.</string>
<!-- Debug Settings --> <!-- Debug Settings -->
<string name="enable_leak_canary_title">Enable LeakCanary</string> <string name="enable_leak_canary_title">Enable LeakCanary</string>

View File

@ -22,11 +22,8 @@
android:title="@string/show_hold_to_append_title" android:title="@string/show_hold_to_append_title"
android:summary="@string/show_hold_to_append_summary"/> android:summary="@string/show_hold_to_append_summary"/>
<ListPreference <Preference
android:defaultValue="@string/caption_size_default" android:key="@string/caption_settings_key"
android:entries="@array/caption_size_descriptions_list" android:title="@string/caption_setting_title"
android:entryValues="@array/caption_size_values_list" android:summary="@string/caption_setting_description"/>
android:key="@string/caption_size_key"
android:summary="%s"
android:title="@string/caption_font_size_settings_title"/>
</PreferenceScreen> </PreferenceScreen>

View File

@ -6,7 +6,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.0' classpath 'com.android.tools.build:gradle:3.1.1'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files