diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2a8f91ae20..c1bb8ca798 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -98,6 +98,8 @@ ([#3972](https://github.com/google/ExoPlayer/issues/3972)). * Support range removal with `removeMediaSourceRange` methods ([#4542](https://github.com/google/ExoPlayer/issues/4542)). + * Support setting a new shuffle order with `setShuffleOrder` + ([#4791](https://github.com/google/ExoPlayer/issues/4791)). * MPEG-TS: Support CEA-608/708 in H262 ([#2565](https://github.com/google/ExoPlayer/issues/2565)). * Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 3a474facd3..f883fbc96b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -53,8 +53,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourcesPublic; @@ -433,6 +434,47 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(/* index= */ 0, shuffleOrder, actionOnCompletion)) + .send(); + } else { + this.shuffleOrder = + shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; + if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + } + @Override public final synchronized void prepareSourceInternal( ExoPlayer player, @@ -579,6 +621,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource shuffleOrderMessage = (MessageData) message; + shuffleOrder = shuffleOrderMessage.customData; + scheduleListenerNotification(shuffleOrderMessage.actionOnCompletion); + break; case MSG_NOTIFY_LISTENER: notifyListener(); break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java index f5f98e4d8a..750c42bbd0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java @@ -21,6 +21,8 @@ import java.util.Random; /** * Shuffled order of indices. + * + *

The shuffle order must be immutable to ensure thread safety. */ public interface ShuffleOrder { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 507b718e8f..d3d3b39ea4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -925,6 +925,54 @@ public final class ConcatenatingMediaSourceTest { testRunner.createPeriod(mediaPeriodId); } + @Test + public void testSetShuffleOrderBeforePreparation() throws Exception { + mediaSource.setShuffleOrder(new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0)); + mediaSource.addMediaSources( + Arrays.asList(createFakeMediaSource(), createFakeMediaSource(), createFakeMediaSource())); + Timeline timeline = testRunner.prepareSource(); + + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); + } + + @Test + public void testSetShuffleOrderAfterPreparation() throws Exception { + mediaSource.addMediaSources( + Arrays.asList(createFakeMediaSource(), createFakeMediaSource(), createFakeMediaSource())); + testRunner.prepareSource(); + mediaSource.setShuffleOrder(new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 3)); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); + + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); + } + + @Test + public void testCustomCallbackBeforePreparationSetShuffleOrder() throws Exception { + Runnable runnable = Mockito.mock(Runnable.class); + mediaSource.setShuffleOrder(new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0), runnable); + + verify(runnable).run(); + } + + @Test + public void testCustomCallbackAfterPreparationSetShuffleOrder() throws Exception { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + mediaSource.addMediaSources( + Arrays.asList(createFakeMediaSource(), createFakeMediaSource(), createFakeMediaSource())); + testRunner.prepareSource(); + TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + () -> + mediaSource.setShuffleOrder( + new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 3), timelineGrabber)); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); + } finally { + dummyMainThread.release(); + } + } + private void assertCompletedAllMediaPeriodLoads(Timeline timeline) { Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window();