diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 1a1c07fd61..38ac324e69 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -47,7 +47,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { @Override public void setUp() throws Exception { super.setUp(); - mediaSource = new DynamicConcatenatingMediaSource(new FakeShuffleOrder(0)); + mediaSource = + new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); testRunner = new MediaSourceTestRunner(mediaSource, null); } @@ -627,6 +628,92 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); } + public void testAtomicTimelineWindowOrder() throws IOException { + // Release default test runner with non-atomic media source and replace with new test runner. + testRunner.release(); + DynamicConcatenatingMediaSource mediaSource = + new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null); + mediaSource.addMediaSources(Arrays.asList(createMediaSources(3))); + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 2, 0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(2); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); + } + + public void testNestedTimeline() throws IOException { + DynamicConcatenatingMediaSource nestedSource1 = + new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); + DynamicConcatenatingMediaSource nestedSource2 = + new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + mediaSource.addMediaSource(nestedSource1); + mediaSource.addMediaSource(nestedSource2); + testRunner.prepareSource(); + FakeMediaSource[] childSources = createMediaSources(4); + nestedSource1.addMediaSource(childSources[0]); + testRunner.assertTimelineChangeBlocking(); + nestedSource1.addMediaSource(childSources[1]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[2]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[3]); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); + + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1); + } + public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { FakeMediaSource childSource = createFakeMediaSource(); mediaSource.addMediaSource(childSource); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 35234753b0..696a6f6fad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -27,14 +27,18 @@ import com.google.android.exoplayer2.Timeline; private final int childCount; private final ShuffleOrder shuffleOrder; + private final boolean isAtomic; /** * Sets up a concatenated timeline with a shuffle order of child timelines. * + * @param isAtomic Whether the child timelines shall be treated as atomic, i.e., treated as a + * single item for repeating and shuffling. * @param shuffleOrder A shuffle order of child timelines. The number of child timelines must * match the number of elements in the shuffle order. */ - public AbstractConcatenatedTimeline(ShuffleOrder shuffleOrder) { + public AbstractConcatenatedTimeline(boolean isAtomic, ShuffleOrder shuffleOrder) { + this.isAtomic = isAtomic; this.shuffleOrder = shuffleOrder; this.childCount = shuffleOrder.getLength(); } @@ -42,6 +46,11 @@ import com.google.android.exoplayer2.Timeline; @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + if (isAtomic) { + // Adapt repeat and shuffle mode to atomic concatenation. + repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; + shuffleModeEnabled = false; + } // Find next window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); @@ -71,6 +80,11 @@ import com.google.android.exoplayer2.Timeline; @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + if (isAtomic) { + // Adapt repeat and shuffle mode to atomic concatenation. + repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; + shuffleModeEnabled = false; + } // Find previous window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); @@ -103,6 +117,9 @@ import com.google.android.exoplayer2.Timeline; if (childCount == 0) { return C.INDEX_UNSET; } + if (isAtomic) { + shuffleModeEnabled = false; + } // Find last non-empty child. int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1; while (getTimelineByChildIndex(lastChildIndex).isEmpty()) { @@ -121,6 +138,9 @@ import com.google.android.exoplayer2.Timeline; if (childCount == 0) { return C.INDEX_UNSET; } + if (isAtomic) { + shuffleModeEnabled = false; + } // Find first non-empty child. int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; while (getTimelineByChildIndex(firstChildIndex).isEmpty()) { 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 48de7de364..c29367e109 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 @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; @@ -173,10 +172,9 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceByMediaPeriod; private final List deferredMediaPeriods; + private final boolean isAtomic; private ExoPlayer player; private Listener listener; @@ -70,22 +71,35 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< * Creates a new dynamic concatenating media source. */ public DynamicConcatenatingMediaSource() { - this(new DefaultShuffleOrder(0)); + this(/* isAtomic= */ false, new DefaultShuffleOrder(0)); + } + + /** + * Creates a new dynamic concatenating media source. + * + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + */ + public DynamicConcatenatingMediaSource(boolean isAtomic) { + this(isAtomic, new DefaultShuffleOrder(0)); } /** * Creates a new dynamic concatenating media source with a custom shuffle order. * + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. * This shuffle order must be empty. */ - public DynamicConcatenatingMediaSource(ShuffleOrder shuffleOrder) { + public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { this.shuffleOrder = shuffleOrder; this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); this.mediaSourcesPublic = new ArrayList<>(); this.mediaSourceHolders = new ArrayList<>(); this.deferredMediaPeriods = new ArrayList<>(1); this.query = new MediaSourceHolder(null, null, -1, -1, -1); + this.isAtomic = isAtomic; } /** @@ -446,8 +460,10 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< private void maybeNotifyListener(@Nullable EventDispatcher actionOnCompletion) { if (!preventListenerNotification) { - listener.onSourceInfoRefreshed(this, - new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), + listener.onSourceInfoRefreshed( + this, + new ConcatenatedTimeline( + mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), null); if (actionOnCompletion != null) { player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionOnCompletion).send(); @@ -652,9 +668,13 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< private final int[] uids; private final SparseIntArray childIndexByUid; - public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, - int periodCount, ShuffleOrder shuffleOrder) { - super(shuffleOrder); + public ConcatenatedTimeline( + Collection mediaSourceHolders, + int windowCount, + int periodCount, + ShuffleOrder shuffleOrder, + boolean isAtomic) { + super(isAtomic, shuffleOrder); this.windowCount = windowCount; this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); @@ -728,61 +748,39 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< * Timeline used as placeholder for an unprepared media source. After preparation, a copy of the * DeferredTimeline is used to keep the originally assigned first period ID. */ - private static final class DeferredTimeline extends Timeline { + private static final class DeferredTimeline extends ForwardingTimeline { private static final Object DUMMY_ID = new Object(); private static final Period period = new Period(); + private static final DummyTimeline dummyTimeline = new DummyTimeline(); - private final Timeline timeline; - private final Object replacedID; + private final Object replacedId; public DeferredTimeline() { - timeline = null; - replacedID = null; + this(dummyTimeline, /* replacedId= */ null); } - private DeferredTimeline(Timeline timeline, Object replacedID) { - this.timeline = timeline; - this.replacedID = replacedID; + private DeferredTimeline(Timeline timeline, Object replacedId) { + super(timeline); + this.replacedId = replacedId; } public DeferredTimeline cloneWithNewTimeline(Timeline timeline) { - return new DeferredTimeline(timeline, replacedID == null && timeline.getPeriodCount() > 0 - ? timeline.getPeriod(0, period, true).uid : replacedID); + return new DeferredTimeline( + timeline, + replacedId == null && timeline.getPeriodCount() > 0 + ? timeline.getPeriod(0, period, true).uid + : replacedId); } public Timeline getTimeline() { return timeline; } - @Override - public int getWindowCount() { - return timeline == null ? 1 : timeline.getWindowCount(); - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - return timeline == null - // Dynamic window to indicate pending timeline updates. - ? window.set(setIds ? DUMMY_ID : null, C.TIME_UNSET, C.TIME_UNSET, false, true, 0, - C.TIME_UNSET, 0, 0, 0) - : timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); - } - - @Override - public int getPeriodCount() { - return timeline == null ? 1 : timeline.getPeriodCount(); - } - @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - if (timeline == null) { - return period.set(setIds ? DUMMY_ID : null, setIds ? DUMMY_ID : null, 0, C.TIME_UNSET, - C.TIME_UNSET); - } timeline.getPeriod(periodIndex, period, setIds); - if (period.uid == replacedID) { + if (period.uid == replacedId) { period.uid = DUMMY_ID; } return period; @@ -790,11 +788,54 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< @Override public int getIndexOfPeriod(Object uid) { - return timeline == null ? (uid == DUMMY_ID ? 0 : C.INDEX_UNSET) - : timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedID : uid); + return timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedId : uid); } - } + /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ + private static final class DummyTimeline extends Timeline { + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + // Dynamic window to indicate pending timeline updates. + return window.set( + /* id= */ null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* defaultPositionUs= */ 0, + /* durationUs= */ C.TIME_UNSET, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, + /* positionInFirstPeriodUs= */ 0); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return period.set( + /* id= */ null, + /* uid= */ null, + /* windowIndex= */ 0, + /* durationUs = */ C.TIME_UNSET, + /* positionInWindowUs= */ C.TIME_UNSET); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid == null ? 0 : C.INDEX_UNSET; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 5eea1aa1cc..e2ef4eb5fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -106,7 +106,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { private final int loopCount; public LoopingTimeline(Timeline childTimeline, int loopCount) { - super(new UnshuffledShuffleOrder(loopCount)); + super(/* isAtomic= */ false, new UnshuffledShuffleOrder(loopCount)); this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount();