From 30b31b56792664ebe181bc497282f075f6add1e5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 Aug 2017 08:37:38 -0700 Subject: [PATCH] Support empty concatenations and empty timelines in concatenations. Both cases were not supported so far. Added tests which all failed in the previous code version and adapted the concatenated media sources to cope with empty timelines and empty concatenations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166480344 --- .../source/ConcatenatingMediaSourceTest.java | 67 ++++++++++- .../DynamicConcatenatingMediaSourceTest.java | 110 +++++++++++++++++- .../source/LoopingMediaSourceTest.java | 17 ++- .../google/android/exoplayer2/Timeline.java | 10 +- .../source/AbstractConcatenatedTimeline.java | 55 ++++++++- .../source/ConcatenatingMediaSource.java | 26 +++-- .../DynamicConcatenatingMediaSource.java | 13 ++- .../exoplayer2/source/LoopingMediaSource.java | 6 +- .../exoplayer2/testutil/TimelineAsserts.java | 7 +- 9 files changed, 275 insertions(+), 36 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index fd0acf2ab3..53111e83ac 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -31,11 +31,24 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { + public void testEmptyConcatenation() { + for (boolean atomic : new boolean[] {false, true}) { + Timeline timeline = getConcatenatedTimeline(atomic); + TimelineAsserts.assertEmpty(timeline); + + timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY); + TimelineAsserts.assertEmpty(timeline); + + timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY, Timeline.EMPTY, Timeline.EMPTY); + TimelineAsserts.assertEmpty(timeline); + } + } + public void testSingleMediaSource() { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); @@ -49,7 +62,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); @@ -91,7 +104,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { timeline = getConcatenatedTimeline(true, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -135,6 +148,54 @@ public final class ConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); } + public void testEmptyTimelineMediaSources() { + // Empty timelines in the front, back, and the middle (single and multiple in a row). + Timeline[] timelines = { Timeline.EMPTY, createFakeTimeline(1, 111), Timeline.EMPTY, + Timeline.EMPTY, createFakeTimeline(2, 222), Timeline.EMPTY, createFakeTimeline(3, 333), + Timeline.EMPTY }; + Timeline timeline = getConcatenatedTimeline(false, timelines); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(2, timeline.getLastWindowIndex(false)); + assertEquals(2, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); + + timeline = getConcatenatedTimeline(true, timelines); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + for (boolean shuffled : new boolean[] {false, true}) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); + assertEquals(0, timeline.getFirstWindowIndex(shuffled)); + assertEquals(2, timeline.getLastWindowIndex(shuffled)); + } + } + /** * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns * the concatenated timeline. 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 0e07e99978..86c03d1ce8 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -25,7 +26,10 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -53,13 +57,13 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( new FakeShuffleOrder(0)); prepareAndListenToTimelineUpdates(mediaSource); + assertNotNull(timeline); waitForTimelineUpdate(); TimelineAsserts.assertEmpty(timeline); // Add first source. mediaSource.addMediaSource(childSources[0]); waitForTimelineUpdate(); - assertNotNull(timeline); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); @@ -143,6 +147,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(timeline.getWindowCount() - 1, timeline.getFirstWindowIndex(true)); assertEquals(0, timeline.getLastWindowIndex(true)); + // Assert all periods can be prepared. + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + // Remove at front of queue. mediaSource.removeMediaSource(0); waitForTimelineUpdate(); @@ -192,6 +199,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); @@ -225,8 +233,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false); + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); - //Add lazy sources after preparation + //Add lazy sources after preparation (and also try to prepare media period from lazy source). mediaSource.addMediaSource(1, lazySources[2]); waitForTimelineUpdate(); mediaSource.addMediaSource(2, childSources[1]); @@ -239,17 +248,90 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); + MediaPeriod lazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); + assertNotNull(lazyPeriod); + final ConditionVariable lazyPeriodPrepared = new ConditionVariable(); + lazyPeriod.prepare(new Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + lazyPeriodPrepared.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }, 0); + assertFalse(lazyPeriodPrepared.block(1)); + MediaPeriod secondLazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); + assertNotNull(secondLazyPeriod); + mediaSource.releasePeriod(secondLazyPeriod); + lazySources[3].triggerTimelineUpdate(createFakeTimeline(7)); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); + assertTrue(lazyPeriodPrepared.block(TIMEOUT_MS)); + mediaSource.releasePeriod(lazyPeriod); mediaSource.releaseSource(); childSources[0].assertReleased(); childSources[1].assertReleased(); } + public void testEmptyTimelineMediaSource() throws InterruptedException { + timeline = null; + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( + new FakeShuffleOrder(0)); + prepareAndListenToTimelineUpdates(mediaSource); + assertNotNull(timeline); + waitForTimelineUpdate(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); + waitForTimelineUpdate(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSources(Arrays.asList(new MediaSource[] { + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) + })); + waitForTimelineUpdate(); + TimelineAsserts.assertEmpty(timeline); + + // Insert non-empty media source to leave empty sources at the start, the end, and the middle + // (with single and multiple empty sources in a row). + MediaSource[] mediaSources = createMediaSources(3); + mediaSource.addMediaSource(1, mediaSources[0]); + waitForTimelineUpdate(); + mediaSource.addMediaSource(4, mediaSources[1]); + waitForTimelineUpdate(); + mediaSource.addMediaSource(6, mediaSources[2]); + waitForTimelineUpdate(); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(2, timeline.getLastWindowIndex(false)); + assertEquals(2, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + } + public void testIllegalArguments() { DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); @@ -325,6 +407,28 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } + private static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource, + int periodCount) { + for (int i = 0; i < periodCount; i++) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(i), null); + assertNotNull(mediaPeriod); + final ConditionVariable mediaPeriodPrepared = new ConditionVariable(); + mediaPeriod.prepare(new Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + mediaPeriodPrepared.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }, 0); + assertTrue(mediaPeriodPrepared.block(TIMEOUT_MS)); + MediaPeriod secondMediaPeriod = mediaSource.createPeriod(new MediaPeriodId(i), null); + assertNotNull(secondMediaPeriod); + mediaSource.releasePeriod(secondMediaPeriod); + mediaSource.releasePeriod(mediaPeriod); + } + } + private static class LazyMediaSource implements MediaSource { private Listener listener; @@ -344,7 +448,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return null; + return new FakeMediaPeriod(TrackGroupArray.EMPTY); } @Override diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 52c313ed47..2c8deb74b4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -42,7 +42,7 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -60,7 +60,7 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -80,7 +80,7 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, 2, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -93,6 +93,17 @@ public class LoopingMediaSourceTest extends TestCase { } } + public void testEmptyTimelineLoop() { + Timeline timeline = getLoopingTimeline(Timeline.EMPTY, 1); + TimelineAsserts.assertEmpty(timeline); + + timeline = getLoopingTimeline(Timeline.EMPTY, 3); + TimelineAsserts.assertEmpty(timeline); + + timeline = getLoopingTimeline(Timeline.EMPTY, Integer.MAX_VALUE); + TimelineAsserts.assertEmpty(timeline); + } + /** * Wraps the specified timeline in a {@link LoopingMediaSource} and returns * the looping timeline. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 8a1d7964ee..b83a99295a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -606,10 +606,11 @@ public abstract class Timeline { * enabled. * * @param shuffleModeEnabled Whether shuffling is enabled. - * @return The index of the last window in the playback order. + * @return The index of the last window in the playback order, or {@link C#INDEX_UNSET} if the + * timeline is empty. */ public int getLastWindowIndex(boolean shuffleModeEnabled) { - return getWindowCount() - 1; + return isEmpty() ? C.INDEX_UNSET : getWindowCount() - 1; } /** @@ -617,10 +618,11 @@ public abstract class Timeline { * enabled. * * @param shuffleModeEnabled Whether shuffling is enabled. - * @return The index of the first window in the playback order. + * @return The index of the first window in the playback order, or {@link C#INDEX_UNSET} if the + * timeline is empty. */ public int getFirstWindowIndex(boolean shuffleModeEnabled) { - return 0; + return isEmpty() ? C.INDEX_UNSET : 0; } /** 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 07813ff046..35234753b0 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 @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.Timeline; @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + // Find next window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( @@ -51,12 +52,16 @@ import com.google.android.exoplayer2.Timeline; if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } - int nextChildIndex = shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) - : childIndex + 1; - if (nextChildIndex != C.INDEX_UNSET && nextChildIndex < childCount) { + // If not found, find first window of next non-empty child. + int nextChildIndex = getNextChildIndex(childIndex, shuffleModeEnabled); + while (nextChildIndex != C.INDEX_UNSET && getTimelineByChildIndex(nextChildIndex).isEmpty()) { + nextChildIndex = getNextChildIndex(nextChildIndex, shuffleModeEnabled); + } + if (nextChildIndex != C.INDEX_UNSET) { return getFirstWindowIndexByChildIndex(nextChildIndex) + getTimelineByChildIndex(nextChildIndex).getFirstWindowIndex(shuffleModeEnabled); } + // If not found, this is the last window. if (repeatMode == Player.REPEAT_MODE_ALL) { return getFirstWindowIndex(shuffleModeEnabled); } @@ -66,6 +71,7 @@ import com.google.android.exoplayer2.Timeline; @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + // Find previous window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( @@ -75,12 +81,17 @@ import com.google.android.exoplayer2.Timeline; if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; } - int previousChildIndex = shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) - : childIndex - 1; - if (previousChildIndex != C.INDEX_UNSET && previousChildIndex >= 0) { + // If not found, find last window of previous non-empty child. + int previousChildIndex = getPreviousChildIndex(childIndex, shuffleModeEnabled); + while (previousChildIndex != C.INDEX_UNSET + && getTimelineByChildIndex(previousChildIndex).isEmpty()) { + previousChildIndex = getPreviousChildIndex(previousChildIndex, shuffleModeEnabled); + } + if (previousChildIndex != C.INDEX_UNSET) { return getFirstWindowIndexByChildIndex(previousChildIndex) + getTimelineByChildIndex(previousChildIndex).getLastWindowIndex(shuffleModeEnabled); } + // If not found, this is the first window. if (repeatMode == Player.REPEAT_MODE_ALL) { return getLastWindowIndex(shuffleModeEnabled); } @@ -89,14 +100,36 @@ import com.google.android.exoplayer2.Timeline; @Override public int getLastWindowIndex(boolean shuffleModeEnabled) { + if (childCount == 0) { + return C.INDEX_UNSET; + } + // Find last non-empty child. int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1; + while (getTimelineByChildIndex(lastChildIndex).isEmpty()) { + lastChildIndex = getPreviousChildIndex(lastChildIndex, shuffleModeEnabled); + if (lastChildIndex == C.INDEX_UNSET) { + // All children are empty. + return C.INDEX_UNSET; + } + } return getFirstWindowIndexByChildIndex(lastChildIndex) + getTimelineByChildIndex(lastChildIndex).getLastWindowIndex(shuffleModeEnabled); } @Override public int getFirstWindowIndex(boolean shuffleModeEnabled) { + if (childCount == 0) { + return C.INDEX_UNSET; + } + // Find first non-empty child. int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; + while (getTimelineByChildIndex(firstChildIndex).isEmpty()) { + firstChildIndex = getNextChildIndex(firstChildIndex, shuffleModeEnabled); + if (firstChildIndex == C.INDEX_UNSET) { + // All children are empty. + return C.INDEX_UNSET; + } + } return getFirstWindowIndexByChildIndex(firstChildIndex) + getTimelineByChildIndex(firstChildIndex).getFirstWindowIndex(shuffleModeEnabled); } @@ -196,4 +229,14 @@ import com.google.android.exoplayer2.Timeline; */ protected abstract Object getChildUidByChildIndex(int childIndex); + private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) + : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET; + } + + private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) + : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET; + } + } 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 0c7bcece68..4cf3843ea1 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 @@ -90,15 +90,19 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { this.listener = listener; - for (int i = 0; i < mediaSources.length; i++) { - if (!duplicateFlags[i]) { - final int index = i; - mediaSources[i].prepareSource(player, false, new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - handleSourceInfoRefreshed(index, timeline, manifest); - } - }); + if (mediaSources.length == 0) { + listener.onSourceInfoRefreshed(Timeline.EMPTY, null); + } else { + for (int i = 0; i < mediaSources.length; i++) { + if (!duplicateFlags[i]) { + final int index = i; + mediaSources[i].prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + handleSourceInfoRefreshed(index, timeline, manifest); + } + }); + } } } } @@ -245,12 +249,12 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1; + return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex + 1, false, false) + 1; } @Override protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; + return Util.binarySearchFloor(sourceWindowOffsets, windowIndex + 1, false, false) + 1; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 9c1e7ec1ba..8614cf9c85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -395,7 +395,14 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private int findMediaSourceHolderByPeriodIndex(int periodIndex) { query.firstPeriodIndexInChild = periodIndex; int index = Collections.binarySearch(mediaSourceHolders, query); - return index >= 0 ? index : -index - 2; + if (index < 0) { + return -index - 2; + } + while (index < mediaSourceHolders.size() - 1 + && mediaSourceHolders.get(index + 1).firstPeriodIndexInChild == periodIndex) { + index++; + } + return index; } private static final class MediaSourceHolder implements Comparable { @@ -456,12 +463,12 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl @Override protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false); + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); } @Override protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false); + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); } @Override 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 00e3c50506..b23b36dcf3 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 @@ -107,8 +107,10 @@ public final class LoopingMediaSource implements MediaSource { childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); this.loopCount = loopCount; - Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, - "LoopingMediaSource contains too many periods"); + if (childPeriodCount > 0) { + Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, + "LoopingMediaSource contains too many periods"); + } } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 74129a0e69..c61aac708c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -36,6 +36,10 @@ public final class TimelineAsserts { public static void assertEmpty(Timeline timeline) { assertWindowIds(timeline); assertPeriodCounts(timeline); + for (boolean shuffled : new boolean[] {false, true}) { + assertEquals(C.INDEX_UNSET, timeline.getFirstWindowIndex(shuffled)); + assertEquals(C.INDEX_UNSET, timeline.getLastWindowIndex(shuffled)); + } } /** @@ -119,8 +123,9 @@ public final class TimelineAsserts { expectedWindowIndex++; } assertEquals(expectedWindowIndex, period.windowIndex); + assertEquals(i, timeline.getIndexOfPeriod(period.uid)); for (@Player.RepeatMode int repeatMode - : new int[] { Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL }) { + : new int[] {Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL}) { if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, repeatMode, false)); } else {