mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-22 11:02:35 +01: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 {
|
||||
supportLibVersion = '27.1.0'
|
||||
exoPlayerLibVersion = '2.7.1'
|
||||
exoPlayerLibVersion = '2.7.3'
|
||||
roomDbLibVersion = '1.0.0'
|
||||
leakCanaryLibVersion = '1.5.4'
|
||||
okHttpLibVersion = '1.5.0'
|
||||
@ -73,6 +73,7 @@ dependencies {
|
||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
|
||||
implementation 'com.nononsenseapps:filepicker:4.2.1'
|
||||
|
||||
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
|
||||
|
||||
|
@ -42,7 +42,11 @@
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false"/>
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".player.BackgroundPlayerActivity"
|
||||
|
@ -56,7 +56,8 @@ public final class BookmarkFragment
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final AppDatabase database = NewPipeDatabase.getInstance(getContext());
|
||||
if (activity == null) return;
|
||||
final AppDatabase database = NewPipeDatabase.getInstance(activity);
|
||||
localPlaylistManager = new LocalPlaylistManager(database);
|
||||
remotePlaylistManager = new RemotePlaylistManager(database);
|
||||
disposables = new CompositeDisposable();
|
||||
|
@ -118,8 +118,12 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
@Override
|
||||
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);
|
||||
if (basePlayerImpl.mediaSessionManager != null) {
|
||||
basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@ -160,6 +164,11 @@ public final class BackgroundPlayer extends Service {
|
||||
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
|
||||
shouldUpdateOnProgress = on;
|
||||
basePlayerImpl.triggerProgressUpdate();
|
||||
if (on) {
|
||||
basePlayerImpl.startProgressLoop();
|
||||
} else {
|
||||
basePlayerImpl.stopProgressLoop();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@ -545,7 +554,6 @@ public final class BackgroundPlayer extends Service {
|
||||
super.onPaused();
|
||||
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
if (isProgressLoopRunning()) stopProgressLoop();
|
||||
|
||||
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.MediaSource;
|
||||
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.upstream.DefaultBandwidthMeter;
|
||||
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.PlayerDataSource;
|
||||
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.CustomTrackSelector;
|
||||
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_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f};
|
||||
|
||||
protected PlayQueue playQueue;
|
||||
protected PlayQueueAdapter playQueueAdapter;
|
||||
@ -140,10 +140,10 @@ public abstract class BasePlayer implements
|
||||
// Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected final static int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds
|
||||
protected final static int PLAY_PREV_ACTIVATION_LIMIT = 5000; // 5 seconds
|
||||
protected final static int PROGRESS_LOOP_INTERVAL = 500;
|
||||
protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds
|
||||
protected final static int FAST_FORWARD_REWIND_AMOUNT_MILLIS = 10000; // 10 Seconds
|
||||
protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
|
||||
protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
|
||||
protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds
|
||||
|
||||
protected CustomTrackSelector trackSelector;
|
||||
protected PlayerDataSource dataSource;
|
||||
@ -177,11 +177,11 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
public void setup() {
|
||||
if (simpleExoPlayer == null) initPlayer();
|
||||
if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true);
|
||||
initListeners();
|
||||
}
|
||||
|
||||
public void initPlayer() {
|
||||
public void initPlayer(final boolean playOnReady) {
|
||||
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
|
||||
|
||||
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
||||
@ -191,15 +191,15 @@ public abstract class BasePlayer implements
|
||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
|
||||
|
||||
final AdaptiveTrackSelection.Factory trackSelectionFactory =
|
||||
new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||
final TrackSelection.Factory trackSelectionFactory =
|
||||
PlayerHelper.getQualitySelector(context, bandwidthMeter);
|
||||
trackSelector = new CustomTrackSelector(trackSelectionFactory);
|
||||
|
||||
final LoadControl loadControl = new LoadController(context);
|
||||
final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
|
||||
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
|
||||
simpleExoPlayer.addListener(this);
|
||||
simpleExoPlayer.setPlayWhenReady(true);
|
||||
simpleExoPlayer.setPlayWhenReady(playOnReady);
|
||||
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
|
||||
|
||||
audioReactor = new AudioReactor(context, simpleExoPlayer);
|
||||
@ -237,15 +237,16 @@ public abstract class BasePlayer implements
|
||||
final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch());
|
||||
|
||||
// Good to go...
|
||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch);
|
||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, /*playOnInit=*/true);
|
||||
}
|
||||
|
||||
protected void initPlayback(@NonNull final PlayQueue queue,
|
||||
@Player.RepeatMode final int repeatMode,
|
||||
final float playbackSpeed,
|
||||
final float playbackPitch) {
|
||||
final float playbackPitch,
|
||||
final boolean playOnReady) {
|
||||
destroyPlayer();
|
||||
initPlayer();
|
||||
initPlayer(playOnReady);
|
||||
setRepeatMode(repeatMode);
|
||||
setPlaybackParameters(playbackSpeed, playbackPitch);
|
||||
|
||||
@ -518,15 +519,16 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
public void triggerProgressUpdate() {
|
||||
if (simpleExoPlayer == null) return;
|
||||
onUpdateProgress(
|
||||
(int) simpleExoPlayer.getCurrentPosition(),
|
||||
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
||||
(int) simpleExoPlayer.getDuration(),
|
||||
simpleExoPlayer.getBufferedPercentage()
|
||||
);
|
||||
}
|
||||
|
||||
private Disposable getProgressReactor() {
|
||||
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
|
||||
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> triggerProgressUpdate());
|
||||
}
|
||||
@ -553,8 +555,8 @@ public abstract class BasePlayer implements
|
||||
// Ensure dynamic/livestream timeline changes does not cause negative position
|
||||
if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " +
|
||||
"clamping position to 0ms.");
|
||||
seekTo(/*clampToTime=*/0);
|
||||
"clamping to default position.");
|
||||
seekToDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -640,12 +642,12 @@ public abstract class BasePlayer implements
|
||||
seekTo(recoveryPositionMillis);
|
||||
playQueue.unsetRecovery(currentSourceIndex);
|
||||
|
||||
} else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) {
|
||||
} else if (isSynchronizing && isLive()) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
|
||||
// Is still synchronizing?
|
||||
seekToDefault();
|
||||
|
||||
} else if (isSynchronizing && presetStartPositionMillis != 0L) {
|
||||
} else if (isSynchronizing && presetStartPositionMillis > 0L) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " +
|
||||
"position=[" + presetStartPositionMillis + "]");
|
||||
// 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) {
|
||||
if (simpleExoPlayer == null || playQueue == null) return;
|
||||
|
||||
if (simpleExoPlayer.getCurrentPosition() <
|
||||
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) {
|
||||
setRecovery();
|
||||
}
|
||||
setRecovery();
|
||||
|
||||
final Throwable cause = error.getCause();
|
||||
if (cause instanceof BehindLiveWindowException) {
|
||||
reload();
|
||||
} else if (cause instanceof UnknownHostException) {
|
||||
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 {
|
||||
playQueue.error(isCurrentWindowValid());
|
||||
playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -787,9 +771,10 @@ public abstract class BasePlayer implements
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public boolean isNearPlaybackEdge(final long timeToEndMillis) {
|
||||
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
|
||||
// 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 currentDurationMillis = simpleExoPlayer.getDuration();
|
||||
@ -985,22 +970,22 @@ public abstract class BasePlayer implements
|
||||
|
||||
public void onFastRewind() {
|
||||
if (DEBUG) Log.d(TAG, "onFastRewind() called");
|
||||
seekBy(-FAST_FORWARD_REWIND_AMOUNT);
|
||||
seekBy(-FAST_FORWARD_REWIND_AMOUNT_MILLIS);
|
||||
}
|
||||
|
||||
public void onFastForward() {
|
||||
if (DEBUG) Log.d(TAG, "onFastForward() called");
|
||||
seekBy(FAST_FORWARD_REWIND_AMOUNT);
|
||||
seekBy(FAST_FORWARD_REWIND_AMOUNT_MILLIS);
|
||||
}
|
||||
|
||||
public void onPlayPrevious() {
|
||||
if (simpleExoPlayer == null || playQueue == null) return;
|
||||
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
|
||||
* is the first in a queue.*/
|
||||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT ||
|
||||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS ||
|
||||
playQueue.getIndex() == 0) {
|
||||
seekToDefault();
|
||||
playQueue.offsetIndex(0);
|
||||
@ -1050,7 +1035,9 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
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() {
|
||||
if (simpleExoPlayer == null || currentInfo == null) return;
|
||||
|
||||
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD &&
|
||||
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS &&
|
||||
simpleExoPlayer.getCurrentPosition() <
|
||||
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) {
|
||||
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) {
|
||||
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 */
|
||||
public boolean isLiveEdge() {
|
||||
if (simpleExoPlayer == null) return false;
|
||||
final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic();
|
||||
if (!isLive) return false;
|
||||
if (simpleExoPlayer == null || !isLive()) return false;
|
||||
|
||||
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
|
||||
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
||||
@ -1143,6 +1128,16 @@ public abstract class BasePlayer implements
|
||||
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() {
|
||||
final int state = simpleExoPlayer.getPlaybackState();
|
||||
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
|
||||
@ -1170,10 +1165,6 @@ public abstract class BasePlayer implements
|
||||
setPlaybackParameters(speed, getPlaybackPitch());
|
||||
}
|
||||
|
||||
public void setPlaybackPitch(float pitch) {
|
||||
setPlaybackParameters(getPlaybackSpeed(), pitch);
|
||||
}
|
||||
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f);
|
||||
if (simpleExoPlayer == null) return defaultParameters;
|
||||
|
@ -30,8 +30,10 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
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.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||
@ -95,12 +96,12 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
private GestureDetector gestureDetector;
|
||||
|
||||
private boolean activityPaused;
|
||||
private VideoPlayerImpl playerImpl;
|
||||
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
@Nullable private StateSaver.SavedState savedState;
|
||||
@Nullable private PlayerState playerState;
|
||||
private boolean isInMultiWindow;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity LifeCycle
|
||||
@ -135,8 +136,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
|
||||
if (DEBUG) Log.d(TAG, "onRestoreInstanceState() called");
|
||||
super.onRestoreInstanceState(bundle);
|
||||
savedState = StateSaver.tryToRestore(bundle, this);
|
||||
StateSaver.tryToRestore(bundle, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -148,26 +150,28 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying()
|
||||
&& !playerImpl.isPlaying()) {
|
||||
playerImpl.onPlay();
|
||||
}
|
||||
activityPaused = false;
|
||||
super.onResume();
|
||||
|
||||
if(globalScreenOrientationLocked()) {
|
||||
boolean lastOrientationWasLandscape
|
||||
= defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false);
|
||||
if (globalScreenOrientationLocked()) {
|
||||
boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
|
||||
getString(R.string.last_orientation_landscape_key), false);
|
||||
setLandscape(lastOrientationWasLandscape);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
||||
super.onBackPressed();
|
||||
if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false);
|
||||
// Upon going in or out of multiwindow mode, isInMultiWindow will always be false,
|
||||
// since the first onResume needs to restore the player.
|
||||
// Subsequent onResume calls while multiwindow mode remains the same and the player is
|
||||
// prepared should be ignored.
|
||||
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
|
||||
@ -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
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called");
|
||||
super.onSaveInstanceState(outState);
|
||||
if (playerImpl == null) return;
|
||||
|
||||
playerImpl.setRecovery();
|
||||
savedState = StateSaver.tryToSave(isChangingConfigurations(), savedState,
|
||||
outState, this);
|
||||
playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
|
||||
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
|
||||
playerImpl.getPlaybackQuality(), playerImpl.isPlaying());
|
||||
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (DEBUG) Log.d(TAG, "onDestroy() called");
|
||||
if (playerImpl != null) playerImpl.destroy();
|
||||
protected void onStop() {
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
super.onStop();
|
||||
playerImpl.destroy();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@ -221,48 +216,19 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
if (objectsToSave == null) return;
|
||||
objectsToSave.add(playerImpl.getPlayQueue());
|
||||
objectsToSave.add(playerImpl.getRepeatMode());
|
||||
objectsToSave.add(playerImpl.getPlaybackSpeed());
|
||||
objectsToSave.add(playerImpl.getPlaybackPitch());
|
||||
objectsToSave.add(playerImpl.getPlaybackQuality());
|
||||
objectsToSave.add(playerState);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
@NonNull final PlayQueue queue = (PlayQueue) 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);
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) {
|
||||
playerState = (PlayerState) savedObjects.poll();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// 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() {
|
||||
if (DEBUG) Log.d(TAG, "showSystemUi() called");
|
||||
if (playerImpl != null && playerImpl.queueVisible) return;
|
||||
@ -275,6 +241,14 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
} else {
|
||||
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().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
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@ -411,15 +389,6 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
this.itemsListCloseButton = findViewById(R.id.playQueueClose);
|
||||
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);
|
||||
channelTextView.setSelected(true);
|
||||
|
||||
@ -727,7 +696,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
animatePlayButtons(true, 200);
|
||||
});
|
||||
|
||||
changeSystemUi();
|
||||
showSystemUi();
|
||||
getRootView().setKeepScreenOn(false);
|
||||
}
|
||||
|
||||
@ -900,7 +869,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
playerImpl.hideControls(150, 0);
|
||||
} else {
|
||||
playerImpl.showControlsThenHide();
|
||||
changeSystemUi();
|
||||
showSystemUi();
|
||||
}
|
||||
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.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueueAdapter;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||
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.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.formatSpeed;
|
||||
|
||||
@ -151,7 +155,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
finish();
|
||||
return true;
|
||||
case R.id.action_append_playlist:
|
||||
appendToPlaylist();
|
||||
appendAllToPlaylist();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
@ -187,13 +191,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
).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
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@ -319,7 +316,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
|
||||
private void buildItemPopupMenu(final PlayQueueItem item, final View 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 -> {
|
||||
if (player == null) return false;
|
||||
|
||||
@ -328,12 +326,20 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
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 -> {
|
||||
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
|
||||
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();
|
||||
}
|
||||
|
||||
@ -488,6 +494,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
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
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@ -497,6 +518,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
onStateChanged(state);
|
||||
onPlayModeChanged(repeatMode, shuffled);
|
||||
onPlaybackParameterChanged(parameters);
|
||||
onMaybePlaybackAdapterChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -609,4 +631,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
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
|
||||
public void initPlayer() {
|
||||
super.initPlayer();
|
||||
public void initPlayer(final boolean playOnReady) {
|
||||
super.initPlayer(playOnReady);
|
||||
|
||||
// Setup video view
|
||||
simpleExoPlayer.setVideoSurfaceView(surfaceView);
|
||||
|
@ -1,8 +1,12 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
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.view.KeyEvent;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
@ -15,8 +19,8 @@ import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
|
||||
public class MediaSessionManager {
|
||||
private static final String TAG = "MediaSessionManager";
|
||||
|
||||
private final MediaSessionCompat mediaSession;
|
||||
private final MediaSessionConnector sessionConnector;
|
||||
@NonNull private final MediaSessionCompat mediaSession;
|
||||
@NonNull private final MediaSessionConnector sessionConnector;
|
||||
|
||||
public MediaSessionManager(@NonNull final Context context,
|
||||
@NonNull final Player player,
|
||||
@ -28,11 +32,9 @@ public class MediaSessionManager {
|
||||
this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer());
|
||||
}
|
||||
|
||||
public MediaSessionCompat getMediaSession() {
|
||||
return mediaSession;
|
||||
}
|
||||
|
||||
public MediaSessionConnector getSessionConnector() {
|
||||
return sessionConnector;
|
||||
@Nullable
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public KeyEvent handleMediaButtonIntent(final Intent intent) {
|
||||
return MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,11 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
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.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.util.Clock;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@ -203,6 +207,16 @@ public class PlayerHelper {
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
|
@ -14,13 +14,35 @@ import java.io.IOException;
|
||||
public class FailedMediaSource implements ManagedMediaSource {
|
||||
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 Throwable error;
|
||||
private final FailedMediaSourceException error;
|
||||
|
||||
private final long retryTimestamp;
|
||||
|
||||
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
|
||||
@NonNull final Throwable error,
|
||||
@NonNull final FailedMediaSourceException error,
|
||||
final long retryTimestamp) {
|
||||
this.playQueueItem = playQueueItem;
|
||||
this.error = error;
|
||||
@ -32,7 +54,7 @@ public class FailedMediaSource implements ManagedMediaSource {
|
||||
* The error will always be propagated to ExoPlayer.
|
||||
* */
|
||||
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
|
||||
@NonNull final Throwable error) {
|
||||
@NonNull final FailedMediaSourceException error) {
|
||||
this.playQueueItem = playQueueItem;
|
||||
this.error = error;
|
||||
this.retryTimestamp = Long.MAX_VALUE;
|
||||
@ -42,7 +64,7 @@ public class FailedMediaSource implements ManagedMediaSource {
|
||||
return playQueueItem;
|
||||
}
|
||||
|
||||
public Throwable getError() {
|
||||
public FailedMediaSourceException getError() {
|
||||
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.Nullable;
|
||||
import android.support.v4.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
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.LoadedMediaSource;
|
||||
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.playlist.PlayQueue;
|
||||
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.util.ServiceHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@ -37,8 +40,11 @@ import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.SerialDisposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.internal.subscriptions.EmptySubscription;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
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;
|
||||
|
||||
public class MediaSourceManager {
|
||||
@ -52,7 +58,6 @@ public class MediaSourceManager {
|
||||
* streams before will only be cached for future usage.
|
||||
*
|
||||
* @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource)
|
||||
* @see #update(int, MediaSource, Runnable)
|
||||
* */
|
||||
private final static int WINDOW_SIZE = 1;
|
||||
|
||||
@ -103,7 +108,7 @@ public class MediaSourceManager {
|
||||
|
||||
@NonNull private final AtomicBoolean isBlocked;
|
||||
|
||||
@NonNull private DynamicConcatenatingMediaSource sources;
|
||||
@NonNull private ManagedMediaSourcePlaylist playlist;
|
||||
|
||||
public MediaSourceManager(@NonNull final PlaybackListener listener,
|
||||
@NonNull final PlayQueue playQueue) {
|
||||
@ -143,9 +148,9 @@ public class MediaSourceManager {
|
||||
|
||||
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()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@ -167,7 +172,7 @@ public class MediaSourceManager {
|
||||
playQueueReactor.cancel();
|
||||
loaderReactor.dispose();
|
||||
syncReactor.dispose();
|
||||
sources.releaseSource();
|
||||
playlist.dispose();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@ -215,17 +220,18 @@ public class MediaSourceManager {
|
||||
break;
|
||||
case REMOVE:
|
||||
final RemoveEvent removeEvent = (RemoveEvent) event;
|
||||
remove(removeEvent.getRemoveIndex());
|
||||
playlist.remove(removeEvent.getRemoveIndex());
|
||||
break;
|
||||
case MOVE:
|
||||
final MoveEvent moveEvent = (MoveEvent) event;
|
||||
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
||||
playlist.move(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
||||
break;
|
||||
case REORDER:
|
||||
// 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
|
||||
final ReorderEvent reorderEvent = (ReorderEvent) event;
|
||||
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex());
|
||||
playlist.move(reorderEvent.getFromSelectedIndex(),
|
||||
reorderEvent.getToSelectedIndex());
|
||||
break;
|
||||
case RECOVERY:
|
||||
default:
|
||||
@ -266,10 +272,11 @@ public class MediaSourceManager {
|
||||
}
|
||||
|
||||
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();
|
||||
return mediaSource.isStreamEqual(playQueueItem);
|
||||
}
|
||||
@ -288,9 +295,9 @@ public class MediaSourceManager {
|
||||
private void maybeUnblock() {
|
||||
if (DEBUG) Log.d(TAG, "maybeUnblock() called.");
|
||||
|
||||
if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) {
|
||||
if (isBlocked.get()) {
|
||||
isBlocked.set(false);
|
||||
playbackListener.onPlaybackUnblock(sources);
|
||||
playbackListener.onPlaybackUnblock(playlist.getParentMediaSource());
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,10 +306,10 @@ public class MediaSourceManager {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void maybeSync() {
|
||||
if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called.");
|
||||
if (DEBUG) Log.d(TAG, "maybeSync() called.");
|
||||
|
||||
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<Throwable> onError = throwable -> syncInternal(currentItem, null);
|
||||
@ -321,9 +328,11 @@ public class MediaSourceManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeSynchronizePlayer() {
|
||||
maybeUnblock();
|
||||
maybeSync();
|
||||
private synchronized void maybeSynchronizePlayer() {
|
||||
if (isPlayQueueReady() && isPlaybackReady()) {
|
||||
maybeUnblock();
|
||||
maybeSync();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@ -332,12 +341,14 @@ public class MediaSourceManager {
|
||||
|
||||
private Observable<Long> getEdgeIntervalSignal() {
|
||||
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
|
||||
.filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis));
|
||||
.filter(ignored ->
|
||||
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
|
||||
}
|
||||
|
||||
private Disposable getDebouncedLoader() {
|
||||
return debouncedSignal.mergeWith(nearEndIntervalSignal)
|
||||
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.single())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(timestamp -> loadImmediate());
|
||||
}
|
||||
@ -348,42 +359,21 @@ public class MediaSourceManager {
|
||||
|
||||
private void loadImmediate() {
|
||||
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
|
||||
// The current item has higher priority
|
||||
final int currentIndex = playQueue.getIndex();
|
||||
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
|
||||
if (currentItem == null) return;
|
||||
final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue, WINDOW_SIZE);
|
||||
if (itemsToLoad == null) return;
|
||||
|
||||
// Evict the items being loaded to free up memory
|
||||
if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
|
||||
loaderReactor.clear();
|
||||
loadingItems.clear();
|
||||
}
|
||||
maybeLoadItem(currentItem);
|
||||
// Evict the previous items being loaded to free up memory, before start loading new ones
|
||||
maybeClearLoaders();
|
||||
|
||||
// The rest are just for seamless playback
|
||||
// Although timeline is not updated prior to the current index, these sources are still
|
||||
// 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(itemsToLoad.center);
|
||||
for (final PlayQueueItem item : itemsToLoad.neighbors) {
|
||||
maybeLoadItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeLoadItem(@NonNull final PlayQueueItem item) {
|
||||
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 (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() +
|
||||
@ -402,19 +392,19 @@ public class MediaSourceManager {
|
||||
return stream.getStream().map(streamInfo -> {
|
||||
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
|
||||
if (source == null) {
|
||||
final Exception exception = new IllegalStateException(
|
||||
"Unable to resolve source from stream info." +
|
||||
" URL: " + stream.getUrl() +
|
||||
", audio count: " + streamInfo.getAudioStreams().size() +
|
||||
", video count: " + streamInfo.getVideoOnlyStreams().size() +
|
||||
streamInfo.getVideoStreams().size());
|
||||
return new FailedMediaSource(stream, exception);
|
||||
final String message = "Unable to resolve source from stream info." +
|
||||
" URL: " + stream.getUrl() +
|
||||
", audio count: " + streamInfo.getAudioStreams().size() +
|
||||
", video count: " + streamInfo.getVideoOnlyStreams().size() +
|
||||
streamInfo.getVideoStreams().size();
|
||||
return new FailedMediaSource(stream, new MediaSourceResolutionException(message));
|
||||
}
|
||||
|
||||
final long expiration = System.currentTimeMillis() +
|
||||
ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
|
||||
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,
|
||||
@ -426,10 +416,10 @@ public class MediaSourceManager {
|
||||
|
||||
final int itemIndex = playQueue.indexOf(item);
|
||||
// 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 " +
|
||||
"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) {
|
||||
final int index = playQueue.indexOf(item);
|
||||
if (index == -1 || index >= sources.getSize()) return false;
|
||||
|
||||
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
|
||||
return mediaSource.shouldBeReplacedWith(item,
|
||||
final ManagedMediaSource mediaSource = playlist.get(index);
|
||||
return mediaSource != null && mediaSource.shouldBeReplacedWith(item,
|
||||
/*mightBeInProgress=*/index != playQueue.getIndex());
|
||||
}
|
||||
|
||||
@ -465,10 +453,9 @@ public class MediaSourceManager {
|
||||
* */
|
||||
private void maybeRenewCurrentIndex() {
|
||||
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();
|
||||
if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) {
|
||||
maybeSynchronizePlayer();
|
||||
@ -477,7 +464,16 @@ public class MediaSourceManager {
|
||||
|
||||
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
|
||||
"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
|
||||
@ -486,72 +482,55 @@ public class MediaSourceManager {
|
||||
private void resetSources() {
|
||||
if (DEBUG) Log.d(TAG, "resetSources() called.");
|
||||
|
||||
this.sources.releaseSource();
|
||||
this.sources = new DynamicConcatenatingMediaSource(false,
|
||||
// Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
|
||||
new ShuffleOrder.UnshuffledShuffleOrder(0));
|
||||
playlist.dispose();
|
||||
playlist = new ManagedMediaSourcePlaylist();
|
||||
}
|
||||
|
||||
private void populateSources() {
|
||||
if (DEBUG) Log.d(TAG, "populateSources() called.");
|
||||
if (sources.getSize() >= playQueue.size()) return;
|
||||
|
||||
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
|
||||
emplace(index, new PlaceholderMediaSource());
|
||||
while (playlist.size() < playQueue.size()) {
|
||||
playlist.expand();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource}
|
||||
* with position in respect to the play queue only if no {@link MediaSource}
|
||||
* already exists at the given index.
|
||||
* */
|
||||
private synchronized void emplace(final int index, @NonNull final MediaSource source) {
|
||||
if (index < sources.getSize()) return;
|
||||
// The rest are just for seamless playback
|
||||
// Although timeline is not updated prior to the current index, these sources are still
|
||||
// loaded into the cache for faster retrieval at a potentially later time.
|
||||
final int leftBound = Math.max(0, currentIndex - windowSize);
|
||||
final int rightLimit = currentIndex + windowSize + 1;
|
||||
final int rightBound = Math.min(playQueue.size(), rightLimit);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource}
|
||||
* at the given index. If this index is out of bound, then the removal is ignored.
|
||||
* */
|
||||
private synchronized void remove(final int index) {
|
||||
if (index < 0 || index > sources.getSize()) return;
|
||||
private static class ItemsToLoad {
|
||||
@NonNull final private PlayQueueItem center;
|
||||
@NonNull final private Collection<PlayQueueItem> neighbors;
|
||||
|
||||
sources.removeMediaSource(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
ItemsToLoad(@NonNull final PlayQueueItem center,
|
||||
@NonNull final Collection<PlayQueueItem> neighbors) {
|
||||
this.center = center;
|
||||
this.neighbors = neighbors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,13 +13,13 @@ import java.util.List;
|
||||
public interface PlaybackListener {
|
||||
|
||||
/**
|
||||
* Called to check if the currently playing stream is close to the end of its playback.
|
||||
* Implementation should return true when the current playback position is within
|
||||
* timeToEndMillis or less until its playback completes or transitions.
|
||||
* 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 progressing within
|
||||
* timeToEndMillis or less to its playback during.
|
||||
*
|
||||
* 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.
|
||||
|
@ -3,5 +3,5 @@
|
||||
<gradient
|
||||
android:angle="90"
|
||||
android:endColor="#00000000"
|
||||
android:startColor="#8c000000"/>
|
||||
android:startColor="@color/video_overlay_color"/>
|
||||
</shape>
|
@ -3,5 +3,5 @@
|
||||
<gradient
|
||||
android:angle="-90"
|
||||
android:endColor="#00000000"
|
||||
android:startColor="#8c000000"/>
|
||||
android:startColor="@color/video_overlay_color"/>
|
||||
</shape>
|
@ -304,7 +304,7 @@
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/duration_live"
|
||||
android:text="@string/duration_live_button"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:maxLength="4"
|
||||
|
@ -129,7 +129,7 @@
|
||||
android:id="@+id/playbackControlRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#64000000"
|
||||
android:background="@color/video_overlay_color"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
@ -406,7 +406,7 @@
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/duration_live"
|
||||
android:text="@string/duration_live_button"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:maxLength="4"
|
||||
|
@ -154,7 +154,7 @@
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/duration_live"
|
||||
android:text="@string/duration_live_button"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:maxLength="4"
|
||||
|
@ -198,7 +198,7 @@
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/duration_live"
|
||||
android:text="@string/duration_live_button"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:maxLength="4"
|
||||
|
@ -40,7 +40,7 @@
|
||||
<color name="playlist_stream_count_background_color">#e6000000</color>
|
||||
<color name="duration_text_color">#EEFFFFFF</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_title_color">#ffffff</color>
|
||||
|
@ -119,6 +119,7 @@
|
||||
<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="duration_live">live</string>
|
||||
<string name="duration_live_button" translatable="false">LIVE</string>
|
||||
<string name="downloads">Downloads</string>
|
||||
<string name="downloads_title">Downloads</string>
|
||||
<string name="error_report_title">Error report</string>
|
||||
|
Loading…
Reference in New Issue
Block a user