diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 59a58a4912..2392c32e0a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; @@ -541,4 +540,30 @@ public final class ExoPlayerTest extends TestCase { Player.TIMELINE_CHANGE_REASON_DYNAMIC); } + public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { + Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(/* isSeekable= */ true, + /* isDynamic= */ false, /* durationUs= */ 100000)); + ConcatenatingMediaSource firstMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, + new FakeShuffleOrder(/* length= */ 2), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + ); + ConcatenatingMediaSource secondMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, + new FakeShuffleOrder(/* length= */ 2), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + ); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationWithShuffle") + // Wait for first preparation and enable shuffling. Plays period 0. + .waitForPlaybackState(Player.STATE_READY).setShuffleModeEnabled(true) + // Reprepare with second media source (keeping state, but with position reset). + // Plays period 1 and 0 because of the reversed fake shuffle order. + .prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(firstMediaSource).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPlayedPeriodIndices(0, 1, 0); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 1732026540..668d52425e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -393,17 +393,10 @@ import java.io.IOException; private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { pendingPrepareCount++; - resetInternal(true); + resetInternal(/* releaseMediaSource= */ true, resetPosition); loadControl.onPrepared(); - if (resetPosition) { - playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET); - } else { - // The new start position is the current playback position. - playbackInfo = new PlaybackInfo(null, null, playbackInfo.periodId, playbackInfo.positionUs, - playbackInfo.contentPositionUs); - } this.mediaSource = mediaSource; - mediaSource.prepareSource(player, true, this); + mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); setState(Player.STATE_BUFFERING); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -638,18 +631,16 @@ import java.io.IOException; Pair periodPosition = resolveSeekPosition(seekPosition); if (periodPosition == null) { - int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( - timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to - // (firstPeriodIndex,0) isn't ignored. - playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); setState(Player.STATE_ENDED); - eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, - playbackInfo.fromNewPosition(firstPeriodIndex, 0, C.TIME_UNSET)).sendToTarget(); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false); + resetInternal(false, true); + // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). + eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted = */ 1, 0, + playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, /* startPositionUs = */ 0, + /* contentPositionUs= */ C.TIME_UNSET)) + .sendToTarget(); return; } @@ -768,13 +759,13 @@ import java.io.IOException; } private void stopInternal() { - resetInternal(true); + resetInternal(/* releaseMediaSource= */ false, /* resetPosition= */ false); loadControl.onStopped(); setState(Player.STATE_IDLE); } private void releaseInternal() { - resetInternal(true); + resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ true); loadControl.onReleased(); setState(Player.STATE_IDLE); internalPlaybackThread.quit(); @@ -784,7 +775,7 @@ import java.io.IOException; } } - private void resetInternal(boolean releaseMediaSource) { + private void resetInternal(boolean releaseMediaSource, boolean resetPosition) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; mediaClock.stop(); @@ -804,6 +795,20 @@ import java.io.IOException; readingPeriodHolder = null; playingPeriodHolder = null; setIsLoading(false); + if (resetPosition) { + // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to + // (firstPeriodIndex,0) isn't ignored. + Timeline timeline = playbackInfo.timeline; + int firstPeriodIndex = timeline == null || timeline.isEmpty() + ? 0 + : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) + .firstPeriodIndex; + playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); + } else { + // The new start position is the current playback position. + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, playbackInfo.positionUs, + playbackInfo.contentPositionUs); + } if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(); @@ -1129,18 +1134,12 @@ import java.io.IOException; } private void handleSourceInfoRefreshEndedPlayback(int prepareAcks, int seekAcks) { - Timeline timeline = playbackInfo.timeline; - int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( - timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; - // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to - // (firstPeriodIndex,0) isn't ignored. - playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); setState(Player.STATE_ENDED); - // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. - notifySourceInfoRefresh(prepareAcks, seekAcks, - playbackInfo.fromNewPosition(firstPeriodIndex, 0, C.TIME_UNSET)); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false); + resetInternal(false, true); + // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). + notifySourceInfoRefresh(prepareAcks, seekAcks, + playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET)); } private void notifySourceInfoRefresh() {