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 bc72ebc060..9f172dc802 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 @@ -15,6 +15,7 @@ */ 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.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -24,6 +25,7 @@ import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; +import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import java.util.concurrent.CountDownLatch; @@ -234,4 +236,27 @@ public final class ExoPlayerTest extends TestCase { assertTrue(renderer.isEnded); } + public void testShuffleModeEnabledChanges() throws Exception { + Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); + MediaSource[] fakeMediaSources = { + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + }; + ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false, + new FakeShuffleOrder(3), fakeMediaSources); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testShuffleModeEnabled") + .setRepeatMode(Player.REPEAT_MODE_ALL).waitForPositionDiscontinuity() // 0 -> 1 + .setShuffleModeEnabled(true).waitForPositionDiscontinuity() // 1 -> 0 + .waitForPositionDiscontinuity().waitForPositionDiscontinuity() // 0 -> 2 -> 1 + .setShuffleModeEnabled(false).setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2 -> end + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource).setRenderers(renderer).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2); + assertTrue(renderer.isEnded); + } + } 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 7035ed637e..5d55652f61 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 @@ -488,7 +488,7 @@ import java.io.IOException; } while (true) { int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex, - period, window, repeatMode, false); + period, window, repeatMode, shuffleModeEnabled); while (lastValidPeriodHolder.next != null && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { lastValidPeriodHolder = lastValidPeriodHolder.next; @@ -686,13 +686,15 @@ 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. - playbackInfo = new PlaybackInfo(0, 0); + playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); - // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't - // ignored. - playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to + // (firstPeriodIndex,0) isn't ignored. + playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); @@ -1029,7 +1031,8 @@ import java.io.IOException; if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(manifest); } else { - Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); + Pair defaultPosition = getPeriodPosition( + timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); int periodIndex = defaultPosition.first; long startPositionUs = defaultPosition.second; MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, @@ -1122,7 +1125,8 @@ import java.io.IOException; while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; - periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, false); + periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, + shuffleModeEnabled); if (periodIndex != C.INDEX_UNSET && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { // The holder is consistent with the new timeline. Update its index and continue. @@ -1170,11 +1174,14 @@ import java.io.IOException; private void handleSourceInfoRefreshEndedPlayback(Object manifest, int processedInitialSeekCount) { - // Set the playback position to (0,0) for notifying the eventHandler. - playbackInfo = new PlaybackInfo(0, 0); + int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( + timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; + // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. + playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); notifySourceInfoRefresh(manifest, processedInitialSeekCount); - // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored. - playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to + // (firstPeriodIndex,0) isn't ignored. + playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); @@ -1205,7 +1212,7 @@ import java.io.IOException; int maxIterations = oldTimeline.getPeriodCount(); for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode, - false); + shuffleModeEnabled); if (oldPeriodIndex == C.INDEX_UNSET) { // We've reached the end of the old timeline. break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java index d7821ed705..6fd0d48e57 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java @@ -162,7 +162,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; // timeline is updated, to avoid repeatedly checking the same timeline. if (currentMediaPeriodInfo.isLastInTimelinePeriod) { int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex, - period, window, repeatMode, false); + period, window, repeatMode, shuffleModeEnabled); if (nextPeriodIndex == C.INDEX_UNSET) { // We can't create a next period yet. return null; @@ -353,7 +353,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex; return !timeline.getWindow(windowIndex, window).isDynamic - && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, false) + && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, shuffleModeEnabled) && isLastMediaPeriodInPeriod; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index ab1f448afd..bc16e105da 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -284,6 +284,29 @@ public abstract class Action { } + /** + * Calls {@link Player#setShuffleModeEnabled(boolean)}. + */ + public static final class SetShuffleModeEnabled extends Action { + + private final boolean shuffleModeEnabled; + + /** + * @param tag A tag to use for logging. + */ + public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) { + super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled); + this.shuffleModeEnabled = shuffleModeEnabled; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.setShuffleModeEnabled(shuffleModeEnabled); + } + + } + /** * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object)}. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 4392dd9d3f..c9ae02c957 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; +import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; @@ -217,6 +218,15 @@ public final class ActionSchedule { return apply(new SetRepeatMode(tag, repeatMode)); } + /** + * Schedules a shuffle setting action to be executed. + * + * @return The builder, for convenience. + */ + public Builder setShuffleModeEnabled(boolean shuffleModeEnabled) { + return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); + } + /** * Schedules a delay until the timeline changed to a specified expected timeline. *