Support media source events for composite media sources.

This is achieved by automatically registering a listener for child sources in
compositions. The composite media source has the chance to correct the
reported window index and media period id in the MediaLoadData of the events.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=189575414
This commit is contained in:
tonihei 2018-03-19 06:33:02 -07:00 committed by Oliver Woodman
parent 6b527da462
commit e3a90a44b7
10 changed files with 402 additions and 19 deletions

View File

@ -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.

View File

@ -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) {

View File

@ -190,6 +190,19 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
}
}
@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.
*/

View File

@ -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<T> extends BaseMediaSource {
private final HashMap<T, MediaSourceAndListener> childSources;
private ExoPlayer player;
private Handler eventHandler;
/** Create composite media source without child sources. */
protected CompositeMediaSource() {
@ -42,6 +46,7 @@ public abstract class CompositeMediaSource<T> 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<T> 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<T> 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<T> 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);
}
}
}

View File

@ -435,6 +435,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
updateMediaSourceInternal(mediaSourceHolder, timeline);
}
@Override
protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) {
for (int i = 0; i < mediaSourceHolder.activeMediaPeriods.size(); i++) {
// Ensure the reported media period id has the same window sequence number as the one created
// by this media source. Otherwise it does not belong to this child source.
if (mediaSourceHolder.activeMediaPeriods.get(i).id.windowSequenceNumber
== mediaPeriodId.windowSequenceNumber) {
return mediaPeriodId.copyWithPeriodIndex(
mediaPeriodId.periodIndex + mediaSourceHolder.firstPeriodIndexInChild);
}
}
return null;
}
@Override
protected int getWindowIndexForChildWindowIndex(
MediaSourceHolder mediaSourceHolder, int windowIndex) {
return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
}
@Override
@SuppressWarnings("unchecked")
public final void handleMessage(int messageType, Object message) throws ExoPlaybackException {

View File

@ -40,8 +40,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
public final MediaSource mediaSource;
public final MediaPeriodId id;
private final MediaPeriodId id;
private final Allocator allocator;
private MediaPeriod mediaPeriod;

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.support.annotation.CheckResult;
import android.support.annotation.Nullable;
@ -322,9 +323,9 @@ public interface MediaSourceEventListener {
/** Dispatches {@link #onLoadStarted(LoadEventInfo, MediaLoadData)}. */
public void loadStarted(final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
Handler handler = listenerAndHandler.handler;
final MediaSourceEventListener listener = listenerAndHandler.listener;
handler.post(
postOrRun(
listenerAndHandler.handler,
new Runnable() {
@Override
public void run() {
@ -386,9 +387,9 @@ public interface MediaSourceEventListener {
public void loadCompleted(
final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
Handler handler = listenerAndHandler.handler;
final MediaSourceEventListener listener = listenerAndHandler.listener;
handler.post(
postOrRun(
listenerAndHandler.handler,
new Runnable() {
@Override
public void run() {
@ -449,9 +450,9 @@ public interface MediaSourceEventListener {
/** Dispatches {@link #onLoadCanceled(LoadEventInfo, MediaLoadData)}. */
public void loadCanceled(final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
Handler handler = listenerAndHandler.handler;
final MediaSourceEventListener listener = listenerAndHandler.listener;
handler.post(
postOrRun(
listenerAndHandler.handler,
new Runnable() {
@Override
public void run() {
@ -524,9 +525,9 @@ public interface MediaSourceEventListener {
final IOException error,
final boolean wasCanceled) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
Handler handler = listenerAndHandler.handler;
final MediaSourceEventListener listener = listenerAndHandler.listener;
handler.post(
postOrRun(
listenerAndHandler.handler,
new Runnable() {
@Override
public void run() {
@ -554,9 +555,9 @@ public interface MediaSourceEventListener {
/** Dispatches {@link #onUpstreamDiscarded(MediaLoadData)}. */
public void upstreamDiscarded(final MediaLoadData mediaLoadData) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
Handler handler = listenerAndHandler.handler;
final MediaSourceEventListener listener = listenerAndHandler.listener;
handler.post(
postOrRun(
listenerAndHandler.handler,
new Runnable() {
@Override
public void run() {
@ -589,9 +590,9 @@ public interface MediaSourceEventListener {
/** Dispatches {@link #onDownstreamFormatChanged(MediaLoadData)}. */
public void downstreamFormatChanged(final MediaLoadData mediaLoadData) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
Handler handler = listenerAndHandler.handler;
final MediaSourceEventListener listener = listenerAndHandler.listener;
handler.post(
postOrRun(
listenerAndHandler.handler,
new Runnable() {
@Override
public void run() {
@ -606,6 +607,14 @@ public interface MediaSourceEventListener {
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
}
private void postOrRun(Handler handler, Runnable runnable) {
if (handler.getLooper() == Looper.myLooper()) {
runnable.run();
} else {
handler.post(runnable);
}
}
private static final class ListenerAndHandler {
public final Handler handler;

View File

@ -303,6 +303,14 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
}
}
@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) {

View File

@ -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();

View File

@ -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<MediaPeriodId> 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++) {