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 {