diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d6fa51daf5..6034ef3e8e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,9 @@ deprecated `DynamicConcatenatingMediaSource`. * Allow clipping of child media sources where the period and window have a non-zero offset with `ClippingMediaSource`. + * Allow adding and removing `MediaSourceEventListener`s to MediaSources after + they have been created. Listening to events is now supported for all + media sources including composite sources. * Audio: Factor out `AudioTrack` position tracking from `DefaultAudioSink`. * Caching: * Add release method to Cache interface. diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 358d0256e3..0219b62789 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -342,7 +342,6 @@ public class PlayerActivity extends Activity MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { mediaSources[i] = buildMediaSource(uris[i], extensions[i]); - mediaSources[i].addEventListener(mainHandler, eventLogger); } mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); @@ -362,6 +361,7 @@ public class PlayerActivity extends Activity } else { releaseAdsLoader(); } + mediaSource.addEventListener(mainHandler, eventLogger); } boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; if (haveResumePosition) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 4cde78296e..7c5e0e52ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -190,6 +190,19 @@ public final class ClippingMediaSource extends CompositeMediaSource { } } + @Override + protected long getMediaTimeForChildMediaTime(Void id, long mediaTimeMs) { + if (mediaTimeMs == C.TIME_UNSET) { + return C.TIME_UNSET; + } + long startMs = C.usToMs(startUs); + long clippedTimeMs = Math.max(0, mediaTimeMs - startMs); + if (endUs != C.TIME_END_OF_SOURCE) { + clippedTimeMs = Math.min(C.usToMs(endUs) - startMs, clippedTimeMs); + } + return clippedTimeMs; + } + /** * Provides a clipped view of a specified timeline. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index f43010dcd4..c73640341e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -15,10 +15,12 @@ */ package com.google.android.exoplayer2.source; +import android.os.Handler; import android.support.annotation.CallSuper; import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.HashMap; @@ -31,7 +33,9 @@ import java.util.HashMap; public abstract class CompositeMediaSource extends BaseMediaSource { private final HashMap childSources; + private ExoPlayer player; + private Handler eventHandler; /** Create composite media source without child sources. */ protected CompositeMediaSource() { @@ -42,6 +46,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @CallSuper public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { this.player = player; + eventHandler = new Handler(); } @Override @@ -57,6 +62,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { public void releaseSourceInternal() { for (MediaSourceAndListener childSource : childSources.values()) { childSource.mediaSource.releaseSource(childSource.listener); + childSource.mediaSource.removeEventListener(childSource.eventListener); } childSources.clear(); player = null; @@ -96,7 +102,9 @@ public abstract class CompositeMediaSource extends BaseMediaSource { onChildSourceInfoRefreshed(id, source, timeline, manifest); } }; - childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener)); + MediaSourceEventListener eventListener = new ForwardingEventListener(id); + childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener)); + mediaSource.addEventListener(eventHandler, eventListener); mediaSource.prepareSource(player, /* isTopLevelSource= */ false, sourceListener); } @@ -108,16 +116,148 @@ public abstract class CompositeMediaSource extends BaseMediaSource { protected final void releaseChildSource(@Nullable T id) { MediaSourceAndListener removedChild = childSources.remove(id); removedChild.mediaSource.releaseSource(removedChild.listener); + removedChild.mediaSource.removeEventListener(removedChild.eventListener); + } + + /** + * Returns the window index in the composite source corresponding to the specified window index in + * a child source. The default implementation does not change the window index. + * + * @param id The unique id used to prepare the child source. + * @param windowIndex A window index of the child source. + * @return The corresponding window index in the composite source. + */ + protected int getWindowIndexForChildWindowIndex(@Nullable T id, int windowIndex) { + return windowIndex; + } + + /** + * Returns the {@link MediaPeriodId} in the composite source corresponding to the specified {@link + * MediaPeriodId} in a child source. The default implementation does not change the media period + * id. + * + * @param id The unique id used to prepare the child source. + * @param mediaPeriodId A {@link MediaPeriodId} of the child source. + * @return The corresponding {@link MediaPeriodId} in the composite source. Null if no + * corresponding media period id can be determined. + */ + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + @Nullable T id, MediaPeriodId mediaPeriodId) { + return mediaPeriodId; + } + + /** + * Returns the media time in the composite source corresponding to the specified media time in a + * child source. The default implementation does not change the media time. + * + * @param id The unique id used to prepare the child source. + * @param mediaTimeMs A media time of the child source, in milliseconds. + * @return The corresponding media time in the composite source, in milliseconds. + */ + protected long getMediaTimeForChildMediaTime(@Nullable T id, long mediaTimeMs) { + return mediaTimeMs; } private static final class MediaSourceAndListener { public final MediaSource mediaSource; public final SourceInfoRefreshListener listener; + public final MediaSourceEventListener eventListener; - public MediaSourceAndListener(MediaSource mediaSource, SourceInfoRefreshListener listener) { + public MediaSourceAndListener( + MediaSource mediaSource, + SourceInfoRefreshListener listener, + MediaSourceEventListener eventListener) { this.mediaSource = mediaSource; this.listener = listener; + this.eventListener = eventListener; + } + } + + private final class ForwardingEventListener implements MediaSourceEventListener { + + private final EventDispatcher eventDispatcher; + private final @Nullable T id; + + public ForwardingEventListener(@Nullable T id) { + this.eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); + this.id = id; + } + + @Override + public void onLoadStarted(LoadEventInfo loadEventData, MediaLoadData mediaLoadData) { + MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData); + if (correctedMediaLoadData != null) { + eventDispatcher.loadStarted(loadEventData, correctedMediaLoadData); + } + } + + @Override + public void onLoadCompleted(LoadEventInfo loadEventData, MediaLoadData mediaLoadData) { + MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData); + if (correctedMediaLoadData != null) { + eventDispatcher.loadCompleted(loadEventData, correctedMediaLoadData); + } + } + + @Override + public void onLoadCanceled(LoadEventInfo loadEventData, MediaLoadData mediaLoadData) { + MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData); + if (correctedMediaLoadData != null) { + eventDispatcher.loadCanceled(loadEventData, correctedMediaLoadData); + } + } + + @Override + public void onLoadError( + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData); + if (correctedMediaLoadData != null) { + eventDispatcher.loadError(loadEventData, correctedMediaLoadData, error, wasCanceled); + } + } + + @Override + public void onUpstreamDiscarded(MediaLoadData mediaLoadData) { + MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData); + if (correctedMediaLoadData != null) { + eventDispatcher.upstreamDiscarded(correctedMediaLoadData); + } + } + + @Override + public void onDownstreamFormatChanged(MediaLoadData mediaLoadData) { + MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData); + if (correctedMediaLoadData != null) { + eventDispatcher.downstreamFormatChanged(correctedMediaLoadData); + } + } + + private @Nullable MediaLoadData correctMediaLoadData(MediaLoadData mediaLoadData) { + MediaPeriodId mediaPeriodId = null; + if (mediaLoadData.mediaPeriodId != null) { + mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, mediaLoadData.mediaPeriodId); + if (mediaPeriodId == null) { + // Media period not found. Ignore event. + return null; + } + } + int windowIndex = getWindowIndexForChildWindowIndex(id, mediaLoadData.windowIndex); + long mediaStartTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaStartTimeMs); + long mediaEndTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaEndTimeMs); + return new MediaLoadData( + windowIndex, + mediaPeriodId, + mediaLoadData.dataType, + mediaLoadData.trackType, + mediaLoadData.trackFormat, + mediaLoadData.trackSelectionReason, + mediaLoadData.trackSelectionData, + mediaStartTimeMs, + mediaEndTimeMs); } } } 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 181efe0092..535413c4b4 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 @@ -435,6 +435,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { } } + @Override + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + MediaPeriodId childId, MediaPeriodId mediaPeriodId) { + // The child id for the content period is just a dummy without window sequence number. That's + // why we need to forward the reported mediaPeriodId in this case. + return childId.isAd() ? childId : mediaPeriodId; + } + // Internal methods. private void onAdPlaybackState(AdPlaybackState adPlaybackState) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 589b75ee50..20239edcc4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -18,18 +18,24 @@ package com.google.android.exoplayer2.source; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ClippingMediaSource.IllegalClippingException; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; import org.junit.Before; import org.junit.Test; @@ -187,13 +193,134 @@ public final class ClippingMediaSourceTest { TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0); } + @Test + public void testEventTimeWithinClippedRange() throws IOException { + MediaLoadData mediaLoadData = + getClippingMediaSourceMediaLoadData( + /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, + /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + /* eventStartUs= */ TEST_CLIP_AMOUNT_US + 1000, + /* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US - 1000); + assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(1000); + assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)) + .isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US - 1000); + } + + @Test + public void testEventTimeOutsideClippedRange() throws IOException { + MediaLoadData mediaLoadData = + getClippingMediaSourceMediaLoadData( + /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, + /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + /* eventStartUs= */ TEST_CLIP_AMOUNT_US - 1000, + /* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US + 1000); + assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0); + assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)) + .isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US); + } + + @Test + public void testUnsetEventTime() throws IOException { + MediaLoadData mediaLoadData = + getClippingMediaSourceMediaLoadData( + /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, + /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + /* eventStartUs= */ C.TIME_UNSET, + /* eventEndUs= */ C.TIME_UNSET); + assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(C.TIME_UNSET); + assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(C.TIME_UNSET); + } + + @Test + public void testEventTimeWithUnsetDuration() throws IOException { + MediaLoadData mediaLoadData = + getClippingMediaSourceMediaLoadData( + /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, + /* clippingEndUs= */ C.TIME_END_OF_SOURCE, + /* eventStartUs= */ TEST_CLIP_AMOUNT_US, + /* eventEndUs= */ TEST_CLIP_AMOUNT_US + 1_000_000); + assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0); + assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(1_000_000); + } + + /** + * Wraps a timeline of duration {@link #TEST_PERIOD_DURATION_US} in a {@link ClippingMediaSource}, + * sends a media source event from the child source and returns the reported {@link MediaLoadData} + * for the clipping media source. + * + * @param clippingStartUs The start time of the media source clipping, in microseconds. + * @param clippingEndUs The end time of the media source clipping, in microseconds. + * @param eventStartUs The start time of the media source event (before clipping), in + * microseconds. + * @param eventEndUs The end time of the media source event (before clipping), in microseconds. + * @return The reported {@link MediaLoadData} for that event. + */ + private static MediaLoadData getClippingMediaSourceMediaLoadData( + long clippingStartUs, long clippingEndUs, final long eventStartUs, final long eventEndUs) + throws IOException { + FakeMediaSource fakeMediaSource = + new FakeMediaSource( + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false), + /* manifest= */ null) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher) { + eventDispatcher.downstreamFormatChanged( + new MediaLoadData( + /* windowIndex= */ 0, + id, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + C.usToMs(eventStartUs), + C.usToMs(eventEndUs))); + return super.createFakeMediaPeriod(id, trackGroupArray, allocator, eventDispatcher); + } + }; + final ClippingMediaSource clippingMediaSource = + new ClippingMediaSource(fakeMediaSource, clippingStartUs, clippingEndUs); + MediaSourceTestRunner testRunner = + new MediaSourceTestRunner(clippingMediaSource, /* allocator= */ null); + final MediaLoadData[] reportedMediaLoadData = new MediaLoadData[1]; + try { + testRunner.runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + clippingMediaSource.addEventListener( + new Handler(), + new DefaultMediaSourceEventListener() { + @Override + public void onDownstreamFormatChanged(MediaLoadData mediaLoadData) { + reportedMediaLoadData[0] = mediaLoadData; + } + }); + } + }); + testRunner.prepareSource(); + // Create period to send the test event configured above. + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + assertThat(reportedMediaLoadData[0]).isNotNull(); + } finally { + testRunner.release(); + } + return reportedMediaLoadData[0]; + } + /** * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ - private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) + private static Timeline getClippedTimeline(Timeline timeline, long startUs, long endUs) throws IOException { FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); - ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs); + ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startUs, endUs); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { Timeline clippedTimeline = testRunner.prepareSource(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index f866c98171..89ebaac071 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.CountDownLatch; import org.junit.After; @@ -134,6 +135,10 @@ public final class ConcatenatingMediaSourceTest { childSources[i].assertReleased(); } + // Assert the correct child source preparation load events have been returned (with the + // respective window index at the time of preparation). + testRunner.assertCompletedManifestLoads(0, 0, 2, 1, 3, 4, 5); + // Assert correct next and previous indices behavior after some insertions and removals. TimelineAsserts.assertNextWindowIndices( timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); @@ -156,8 +161,9 @@ public final class ConcatenatingMediaSourceTest { assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1); assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); - // Assert all periods can be prepared. + // Assert all periods can be prepared and the respective load events are returned. testRunner.assertPrepareAndReleaseAllPeriods(); + assertCompletedAllMediaPeriodLoads(timeline); // Remove at front of queue. mediaSource.removeMediaSource(0); @@ -205,6 +211,8 @@ public final class ConcatenatingMediaSourceTest { timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); testRunner.assertPrepareAndReleaseAllPeriods(); + testRunner.assertCompletedManifestLoads(0, 1, 2); + assertCompletedAllMediaPeriodLoads(timeline); testRunner.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); @@ -250,6 +258,8 @@ public final class ConcatenatingMediaSourceTest { TimelineAsserts.assertWindowIds(timeline, 111, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false); testRunner.assertPrepareAndReleaseAllPeriods(); + testRunner.assertCompletedManifestLoads(0, 1); + assertCompletedAllMediaPeriodLoads(timeline); // Add further lazy and normal sources after preparation. Also remove one lazy source again to // check it doesn't throw or change the result. @@ -325,6 +335,7 @@ public final class ConcatenatingMediaSourceTest { })); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); + testRunner.assertCompletedManifestLoads(/* empty */ ); // 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). @@ -358,6 +369,8 @@ public final class ConcatenatingMediaSourceTest { assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2); assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); testRunner.assertPrepareAndReleaseAllPeriods(); + testRunner.assertCompletedManifestLoads(0, 1, 2); + assertCompletedAllMediaPeriodLoads(timeline); } @Test @@ -659,6 +672,8 @@ public final class ConcatenatingMediaSourceTest { /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* windowSequenceNumber= */ 1)); + testRunner.assertCompletedManifestLoads(0, 1); + assertCompletedAllMediaPeriodLoads(timeline); } @Test @@ -787,6 +802,9 @@ public final class ConcatenatingMediaSourceTest { new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3), new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5), new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 7)); + // Assert that only one manifest load is reported because the source is reused. + testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0); + assertCompletedAllMediaPeriodLoads(timeline); testRunner.releaseSource(); childSource.assertReleased(); @@ -816,6 +834,9 @@ public final class ConcatenatingMediaSourceTest { new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3), new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4)); + // Assert that only one manifest load is needed because the source is reused. + testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0); + assertCompletedAllMediaPeriodLoads(timeline); testRunner.releaseSource(); childSource.assertReleased(); @@ -874,6 +895,47 @@ public final class ConcatenatingMediaSourceTest { assertThat(newPeriodId2).isEqualTo(periodId0); } + @Test + public void testChildTimelineChangeWithActiveMediaPeriod() throws IOException { + FakeMediaSource[] nestedChildSources = createMediaSources(/* count= */ 2); + ConcatenatingMediaSource childSource = new ConcatenatingMediaSource(nestedChildSources); + mediaSource.addMediaSource(childSource); + + testRunner.prepareSource(); + MediaPeriod mediaPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + childSource.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 1); + testRunner.assertTimelineChangeBlocking(); + testRunner.preparePeriod(mediaPeriod, /* positionUs= */ 0); + + testRunner.assertCompletedMediaPeriodLoads( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + } + + private void assertCompletedAllMediaPeriodLoads(Timeline timeline) { + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + ArrayList expectedMediaPeriodIds = new ArrayList<>(); + for (int windowIndex = 0; windowIndex < timeline.getWindowCount(); windowIndex++) { + timeline.getWindow(windowIndex, window); + for (int periodIndex = window.firstPeriodIndex; + periodIndex <= window.lastPeriodIndex; + periodIndex++) { + timeline.getPeriod(periodIndex, period); + expectedMediaPeriodIds.add(new MediaPeriodId(periodIndex, windowIndex)); + for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { + for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { + expectedMediaPeriodIds.add( + new MediaPeriodId(periodIndex, adGroupIndex, adIndex, windowIndex)); + } + } + } + } + testRunner.assertCompletedMediaPeriodLoads( + expectedMediaPeriodIds.toArray(new MediaPeriodId[0])); + } + private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) {