From 784e8a634477542dab41e6f90a05bf67965a4c7c Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Feb 2018 07:34:31 -0800 Subject: [PATCH] Support isAtomic flag in DynamicConcatenatingMediaSource. This feature is supported in the ConcatenatingMediaSource and is easily copied to this media source. Also adding tests to check whether the atomic property works in normal concatenation and in also in nested use. Also fixes a bug where timeline methods of the DeferredTimeline were not correctly forwarded. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184526881 --- .../DynamicConcatenatingMediaSourceTest.java | 89 +++++++++++- .../source/AbstractConcatenatedTimeline.java | 22 ++- .../source/ConcatenatingMediaSource.java | 33 +---- .../DynamicConcatenatingMediaSource.java | 131 ++++++++++++------ .../exoplayer2/source/LoopingMediaSource.java | 2 +- 5 files changed, 197 insertions(+), 80 deletions(-) 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();