From 5a05cb96beeb73c7586af1f2c9c249b6213c5a18 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 15 Mar 2018 20:07:38 -0700 Subject: [PATCH] -Changed start position seek to occur after media source window has been prepared. -Fixed livestream not seeking to live when started from play queue. -Fixed media source manager synchronization to only occur after timeline change has completed. -Fixed auto queue not working when last item is replayed after the auto-queued item is removed. -Updated ExoPlayer to 2.7.1. --- app/build.gradle | 2 +- .../org/schabi/newpipe/player/BasePlayer.java | 139 +++++++++--------- .../newpipe/player/MainVideoPlayer.java | 1 + .../newpipe/player/PopupVideoPlayer.java | 2 +- .../newpipe/player/ServicePlayerActivity.java | 2 +- .../schabi/newpipe/player/VideoPlayer.java | 10 +- .../player/playback/MediaSourceManager.java | 15 +- 7 files changed, 92 insertions(+), 79 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3529a37b1..952bc3067 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +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:2.7.0' + implementation 'com.google.android.exoplayer:exoplayer:2.7.1' debugImplementation 'com.facebook.stetho:stetho:1.5.0' debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 5ec61b058..de85a3704 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -149,7 +149,8 @@ public abstract class BasePlayer implements protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; - protected boolean isPrepared = false; + private boolean isPrepared = false; + private boolean isSynchronizing = false; protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; @@ -402,6 +403,7 @@ public abstract class BasePlayer implements // States Implementation //////////////////////////////////////////////////////////////////////////*/ + public static final int STATE_PREFLIGHT = -1; public static final int STATE_BLOCKED = 123; public static final int STATE_PLAYING = 124; public static final int STATE_BUFFERING = 125; @@ -409,7 +411,7 @@ public abstract class BasePlayer implements public static final int STATE_PAUSED_SEEK = 127; public static final int STATE_COMPLETED = 128; - protected int currentState = -1; + protected int currentState = STATE_PREFLIGHT; public void changeState(int state) { if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); @@ -540,11 +542,13 @@ public abstract class BasePlayer implements case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes - // ensures MediaSourceManager#update is complete + // Ensures MediaSourceManager#update is complete final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size(); // Ensure dynamic/livestream timeline changes does not cause negative position - if (isPlaylistStable && !isCurrentWindowValid()) { - simpleExoPlayer.seekTo(/*clampToMillis=*/0); + if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) { + if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " + + "clamping position to default time."); + seekTo(/*clampToTime=*/0); } break; } @@ -596,49 +600,55 @@ public abstract class BasePlayer implements } break; case Player.STATE_READY: //3 - maybeRecover(); + maybeCorrectSeekPosition(); if (!isPrepared) { isPrepared = true; onPrepared(playWhenReady); break; } - if (currentState == STATE_PAUSED_SEEK) break; changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); break; case Player.STATE_ENDED: // 4 - // Ensure the current window has actually ended - // since single windows that are still loading may produce an ended state - if (isCurrentWindowValid() && - simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { - changeState(STATE_COMPLETED); - isPrepared = false; - } + changeState(STATE_COMPLETED); + isPrepared = false; break; } } - private void maybeRecover() { + private void maybeCorrectSeekPosition() { + if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return; + final int currentSourceIndex = playQueue.getIndex(); final PlayQueueItem currentSourceItem = playQueue.getItem(); + if (currentSourceItem == null) return; - // Check if already playing correct window - final boolean isCurrentPeriodCorrect = + final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition(); + final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; + final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; - // Check if recovering - if (isCurrentPeriodCorrect && currentSourceItem != null) { - /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, - * rounding this position to the nearest second will help alleviate this.*/ - final long position = currentSourceItem.getRecoveryPosition(); - - /* Skip recovering if the recovery position is not set.*/ - if (position == PlayQueueItem.RECOVERY_UNSET) return; - - if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + - " at: " + getTimeString((int)position)); - simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition()); + if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) { + // Is recovering previous playback? + if (DEBUG) Log.d(TAG, "Playback - Rewinding to recovery time=" + + "[" + getTimeString((int)recoveryPositionMillis) + "]"); + seekTo(recoveryPositionMillis); playQueue.unsetRecovery(currentSourceIndex); + isSynchronizing = false; + + } else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) { + if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); + // Is still synchronizing? + seekToDefault(); + + } else if (isSynchronizing && presetStartPositionMillis != 0L) { + if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + + "position=[" + presetStartPositionMillis + "]"); + // Has another start position? + seekTo(presetStartPositionMillis); + currentInfo.setStartPosition(0); } + + isSynchronizing = false; } /** @@ -810,11 +820,26 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + (info != null ? "available" : "null") + " info, " + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); + if (simpleExoPlayer == null || playQueue == null) return; + final boolean onPlaybackInitial = currentItem == null; final boolean hasPlayQueueItemChanged = currentItem != item; final boolean hasStreamInfoChanged = currentInfo != info; + + final int currentPlayQueueIndex = playQueue.indexOf(item); + final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); + + // when starting playback on the last item when not repeating, maybe auto queue + if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && + getRepeatMode() == Player.REPEAT_MODE_OFF && + PlayerHelper.isAutoQueueEnabled(context)) { + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); + if (autoQueue != null) playQueue.append(autoQueue.getStreams()); + } + // If nothing to synchronize if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { - return; // Nothing to synchronize + return; } currentItem = item; @@ -824,13 +849,8 @@ public abstract class BasePlayer implements registerView(); initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); } - - final int currentPlayQueueIndex = playQueue.indexOf(item); onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); - if (simpleExoPlayer == null) return; - final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); - final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + @@ -844,22 +864,16 @@ public abstract class BasePlayer implements "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); - // If not playing correct stream, change window position - } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { - final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; + // If not playing correct stream, change window position and sets flag + // for synchronizing once window position is corrected + // @see maybeCorrectSeekPosition() + } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial || + !isPlaying()) { if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + " index=[" + currentPlayQueueIndex + "]," + - " at=[" + getTimeString((int)startPos) + "]," + " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); - simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); - } - - // when starting playback on the last item when not repeating, maybe auto queue - if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && - getRepeatMode() == Player.REPEAT_MODE_OFF && - PlayerHelper.isAutoQueueEnabled(context)) { - final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); - if (autoQueue != null) playQueue.append(autoQueue.getStreams()); + isSynchronizing = true; + simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); } } @@ -927,9 +941,6 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); if (playWhenReady) audioReactor.requestAudioFocus(); changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); - - // On live prepared - if (simpleExoPlayer.isCurrentWindowDynamic()) seekToDefault(); } public void onVideoPlayPause() { @@ -1001,16 +1012,16 @@ public abstract class BasePlayer implements playQueue.setIndex(index); } - public void seekBy(int milliSeconds) { - if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); - if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || - ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) { - return; - } + public void seekTo(long positionMillis) { + if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); + if (simpleExoPlayer == null || positionMillis < 0 || + positionMillis > simpleExoPlayer.getDuration()) return; + simpleExoPlayer.seekTo(positionMillis); + } - int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); - if (progress < 0) progress = 0; - simpleExoPlayer.seekTo(progress); + public void seekBy(long offsetMillis) { + if (DEBUG) Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); + seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis); } public boolean isCurrentWindowValid() { @@ -1094,10 +1105,6 @@ public abstract class BasePlayer implements return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); } - public boolean isCompleted() { - return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_ENDED; - } - public boolean isPlaying() { final int state = simpleExoPlayer.getPlaybackState(); return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) @@ -1148,8 +1155,8 @@ public abstract class BasePlayer implements return playQueueAdapter; } - public boolean isPlayerReady() { - return currentState == STATE_PLAYING || currentState == STATE_COMPLETED || currentState == STATE_PAUSED; + public boolean isPrepared() { + return isPrepared; } public boolean isProgressLoopRunning() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index dd7e0c71e..90a4a8c9f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -640,6 +640,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR public void onDismiss(PopupMenu menu) { super.onDismiss(menu); if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); + hideSystemUi(); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 123fbfee3..64dc03da6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -716,7 +716,7 @@ public final class PopupVideoPlayer extends Service { public boolean onDoubleTap(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); - if (playerImpl == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false; + if (playerImpl == null || !playerImpl.isPlaying()) return false; if (e.getX() > popupWidth / 2) { playerImpl.onFastForward(); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 1c3ffe911..50248891b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -509,7 +509,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity @Override public void onStopTrackingTouch(SeekBar seekBar) { - if (player != null) player.simpleExoPlayer.seekTo(seekBar.getProgress()); + if (player != null) player.seekTo(seekBar.getProgress()); seekDisplay.setVisibility(View.GONE); seeking = false; } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 48b13654c..aa896bb69 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -425,7 +425,7 @@ public abstract class VideoPlayer extends BasePlayer // Create subtitle sources for (final Subtitles subtitle : info.getSubtitles()) { final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); - if (mimeType == null || context == null) continue; + if (mimeType == null) continue; final Format textFormat = Format.createTextSampleFormat(null, mimeType, SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); @@ -599,7 +599,7 @@ public abstract class VideoPlayer extends BasePlayer @Override public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { - if (!isPrepared) return; + if (!isPrepared()) return; if (duration != playbackSeekBar.getMax()) { playbackEndTime.setText(getTimeString(duration)); @@ -624,8 +624,6 @@ public abstract class VideoPlayer extends BasePlayer } protected void onFullScreenButtonClicked() { - if (!isPlayerReady()) return; - changeState(STATE_BLOCKED); } @@ -735,7 +733,7 @@ public abstract class VideoPlayer extends BasePlayer } private void onResizeClicked() { - if (getAspectRatioFrameLayout() != null && context != null) { + if (getAspectRatioFrameLayout() != null) { final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); final int newResizeMode = nextResizeMode(currentResizeMode); getAspectRatioFrameLayout().setResizeMode(newResizeMode); @@ -772,7 +770,7 @@ public abstract class VideoPlayer extends BasePlayer public void onStopTrackingTouch(SeekBar seekBar) { if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); - simpleExoPlayer.seekTo(seekBar.getProgress()); + seekTo(seekBar.getProgress()); if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 50c069b40..170668169 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -307,7 +307,7 @@ public class MediaSourceManager { if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); final PlayQueueItem currentItem = playQueue.getItem(); - if (isBlocked.get() || currentItem == null) return; + if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return; final Consumer onSuccess = info -> syncInternal(currentItem, info); final Consumer onError = throwable -> syncInternal(currentItem, null); @@ -400,8 +400,6 @@ public class MediaSourceManager { /* No exception handling since getLoadedMediaSource guarantees nonnull return */ .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); loaderReactor.add(loader); - } else { - maybeSynchronizePlayer(); } } @@ -467,6 +465,12 @@ public class MediaSourceManager { * Checks if the current playing index contains an expired {@link ManagedMediaSource}. * If so, the expired source is replaced by a {@link PlaceholderMediaSource} and * {@link #loadImmediate()} is called to reload the current item. + *

+ * If not, then the media source at the current index is ready for playback, and + * {@link #maybeSynchronizePlayer()} is called. + *

+ * Under both cases, {@link #maybeSync()} will be called to ensure the listener + * is up-to-date. * */ private void maybeRenewCurrentIndex() { final int currentIndex = playQueue.getIndex(); @@ -475,7 +479,10 @@ public class MediaSourceManager { final ManagedMediaSource currentSource = (ManagedMediaSource) sources.getMediaSource(currentIndex); final PlayQueueItem currentItem = playQueue.getItem(); - if (!currentSource.canReplace(currentItem)) return; + if (!currentSource.canReplace(currentItem)) { + maybeSynchronizePlayer(); + return; + } if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " + "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");