mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-24 17:10:33 +00:00
Merge branch 'qol-follow-ups' of https://github.com/karyogamy/NewPipe into test
This commit is contained in:
commit
3cbd2057e3
@ -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"
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
@ -59,7 +61,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 +96,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 +136,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 +150,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 +184,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 +216,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 +241,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 +316,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 +389,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);
|
||||||
|
|
||||||
@ -727,7 +696,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||||||
animatePlayButtons(true, 200);
|
animatePlayButtons(true, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
changeSystemUi();
|
showSystemUi();
|
||||||
getRootView().setKeepScreenOn(false);
|
getRootView().setKeepScreenOn(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -900,7 +869,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;
|
||||||
}
|
}
|
||||||
|
88
app/src/main/java/org/schabi/newpipe/player/PlayerState.java
Normal file
88
app/src/main/java/org/schabi/newpipe/player/PlayerState.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,8 +228,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);
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,11 @@ import android.support.annotation.NonNull;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
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.ui.AspectRatioFrameLayout;
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||||
|
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||||
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
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 +207,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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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>
|
@ -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>
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user