diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java
index bb6f1b1a1f..f94a3198cd 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java
@@ -601,7 +601,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
mediaItem.mediaId, mediaItem.localConfiguration.uri, adsConfiguration.adTagUri),
/* adMediaSourceFactory= */ this,
adsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
}
/** Loads media source factories lazily. */
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsLoader.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsLoader.java
index d95b9d4ef5..776f27caec 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsLoader.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsLoader.java
@@ -21,6 +21,7 @@ import androidx.media3.common.AdViewProvider;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
+import androidx.media3.common.Timeline;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.source.MediaSource;
@@ -157,6 +158,24 @@ public interface AdsLoader {
@UnstableApi
void stop(AdsMediaSource adsMediaSource, EventListener eventListener);
+ /**
+ * Notifies the ads loader when the content source has changed its timeline. Called on the main
+ * thread by {@link AdsMediaSource}.
+ *
+ *
If you override this callback for the purpose of reading ad data from the timeline to
+ * populate the {@link AdPlaybackState} with, you need to pass true to the constructor of {@link
+ * AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, Object, MediaSource.Factory, AdsLoader,
+ * AdViewProvider, boolean) AdsMediaSource} to indicate the content source needs to be prepared
+ * upfront.
+ *
+ * @param mediaItem The {@link MediaItem} of the source that produced the timeline.
+ * @param timeline The timeline of the content source.
+ */
+ @UnstableApi
+ default void handleContentTimelineChanged(MediaItem mediaItem, Timeline timeline) {
+ // Do nothing.
+ }
+
/**
* Notifies the ads loader that preparation of an ad media period is complete. Called on the main
* thread by {@link AdsMediaSource}.
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java
index 0df30cb9e8..23ead8fffb 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java
@@ -126,7 +126,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
* #TYPE_UNEXPECTED}.
*/
public RuntimeException getRuntimeExceptionForUnexpected() {
- Assertions.checkState(type == TYPE_UNEXPECTED);
+ checkState(type == TYPE_UNEXPECTED);
return (RuntimeException) checkNotNull(getCause());
}
}
@@ -155,6 +155,10 @@ public final class AdsMediaSource extends CompositeMediaSource {
* Constructs a new source that inserts ads linearly with the content specified by {@code
* contentMediaSource}.
*
+ * This is equivalent to passing true as param {@code useLazyContentSourcePreparation} when
+ * calling {@link AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, Object,
+ * MediaSource.Factory, AdsLoader, AdViewProvider, boolean)}.
+ *
* @param contentMediaSource The {@link MediaSource} providing the content to play.
* @param adTagDataSpec The data specification of the ad tag to load.
* @param adsId An opaque identifier for ad playback state associated with this instance. Ad
@@ -169,11 +173,49 @@ public final class AdsMediaSource extends CompositeMediaSource {
MediaSource contentMediaSource,
DataSpec adTagDataSpec,
Object adsId,
- MediaSource.Factory adMediaSourceFactory,
+ Factory adMediaSourceFactory,
AdsLoader adsLoader,
AdViewProvider adViewProvider) {
+ this(
+ contentMediaSource,
+ adTagDataSpec,
+ adsId,
+ adMediaSourceFactory,
+ adsLoader,
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
+ }
+
+ /**
+ * Constructs a new source that inserts ads linearly with the content specified by {@code
+ * contentMediaSource}.
+ *
+ * @param contentMediaSource The {@link MediaSource} providing the content to play.
+ * @param adTagDataSpec The data specification of the ad tag to load.
+ * @param adsId An opaque identifier for ad playback state associated with this instance. Ad
+ * loading and playback state is shared among all playlist items that have the same ads id (by
+ * {@link Object#equals(Object) equality}), so it is important to pass the same identifiers
+ * when constructing playlist items each time the player returns to the foreground.
+ * @param adMediaSourceFactory Factory for media sources used to load ad media.
+ * @param adsLoader The loader for ads.
+ * @param adViewProvider Provider of views for the ad UI.
+ * @param useLazyContentSourcePreparation True if the content source should be prepared lazily and
+ * wait for an {@link AdPlaybackState} to be set before preparing. False if the timeline is
+ * required {@linkplain AdsLoader#handleContentTimelineChanged(MediaItem, Timeline) to read ad
+ * data from it} to populate the {@link AdPlaybackState} (for instance from HLS
+ * interstitials).
+ */
+ public AdsMediaSource(
+ MediaSource contentMediaSource,
+ DataSpec adTagDataSpec,
+ Object adsId,
+ Factory adMediaSourceFactory,
+ AdsLoader adsLoader,
+ AdViewProvider adViewProvider,
+ boolean useLazyContentSourcePreparation) {
this.contentMediaSource =
- new MaskingMediaSource(contentMediaSource, /* useLazyPreparation= */ true);
+ new MaskingMediaSource(
+ contentMediaSource, /* useLazyPreparation= */ useLazyContentSourcePreparation);
this.contentDrmConfiguration =
checkNotNull(contentMediaSource.getMediaItem().localConfiguration).drmConfiguration;
this.adMediaSourceFactory = adMediaSourceFactory;
@@ -288,6 +330,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
} else {
Assertions.checkArgument(newTimeline.getPeriodCount() == 1);
contentTimeline = newTimeline;
+ mainHandler.post(() -> adsLoader.handleContentTimelineChanged(getMediaItem(), newTimeline));
}
maybeUpdateSourceInfo();
}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/AdsMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/AdsMediaSourceTest.java
index cb502b5c8d..590951a838 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/AdsMediaSourceTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/AdsMediaSourceTest.java
@@ -16,6 +16,7 @@
package androidx.media3.exoplayer.source.ads;
import static com.google.common.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
@@ -28,12 +29,16 @@ import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
+import android.util.Pair;
+import androidx.annotation.Nullable;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.AdViewProvider;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
+import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.datasource.DataSpec;
+import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MaskingMediaSource;
@@ -49,6 +54,12 @@ import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Rule;
@@ -102,6 +113,7 @@ public final class AdsMediaSourceTest {
private static final DataSpec TEST_ADS_DATA_SPEC = new DataSpec(Uri.EMPTY);
private static final Object TEST_ADS_ID = new Object();
+ private static final long TIMEOUT_MS = 5_000L;
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@@ -132,7 +144,8 @@ public final class AdsMediaSourceTest {
TEST_ADS_ID,
adMediaSourceFactory,
mockAdsLoader,
- mockAdViewProvider);
+ mockAdViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
adsMediaSource.prepareSource(
mockMediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET);
shadowOf(Looper.getMainLooper()).idle();
@@ -325,6 +338,395 @@ public final class AdsMediaSourceTest {
.isEqualTo(updatedMediaItem);
}
+ @Test
+ public void
+ prepare_withPrerollUsingLazyContentSourcePreparationFalse_allExternalTimelinesWithAds()
+ throws InterruptedException {
+ AtomicBoolean contentMediaPeriodCreated = new AtomicBoolean();
+ MediaSource fakeContentMediaSource =
+ new FakeMediaSource() {
+ @Override
+ public MediaPeriod createPeriod(
+ MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ contentMediaPeriodCreated.set(true);
+ return super.createPeriod(id, allocator, startPositionUs);
+ }
+ };
+ CountDownLatch adSourcePreparedLatch = new CountDownLatch(1);
+ AtomicInteger adSourcePreparedCounter = new AtomicInteger();
+ List createdAdMediaPeriodIds = new ArrayList<>();
+ MediaSource fakeAdMediaSource =
+ new FakeMediaSource() {
+ @Override
+ public synchronized void prepareSourceInternal(
+ @Nullable TransferListener mediaTransferListener) {
+ adSourcePreparedLatch.countDown();
+ adSourcePreparedCounter.incrementAndGet();
+ super.prepareSourceInternal(mediaTransferListener);
+ }
+
+ @Override
+ public MediaPeriod createPeriod(
+ MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ createdAdMediaPeriodIds.add(id);
+ return super.createPeriod(id, allocator, startPositionUs);
+ }
+ };
+ CountDownLatch contentTimelineChangedCalledLatch = new CountDownLatch(1);
+ AtomicReference eventListenerRef = new AtomicReference<>();
+ AdsLoader fakeAdsLoader =
+ new NoOpAdsLoader() {
+ @Override
+ public void start(
+ AdsMediaSource adsMediaSource,
+ DataSpec adTagDataSpec,
+ Object adsId,
+ AdViewProvider adViewProvider,
+ EventListener eventListener) {
+ eventListenerRef.set(eventListener);
+ }
+
+ @Override
+ public void handleContentTimelineChanged(MediaItem mediaItem, Timeline timeline) {
+ contentTimelineChangedCalledLatch.countDown();
+ }
+ };
+ MediaSource.Factory adMediaSourceFactory = mock(MediaSource.Factory.class);
+ when(adMediaSourceFactory.createMediaSource(any(MediaItem.class)))
+ .thenReturn(fakeAdMediaSource);
+ // Prepare the AdsMediaSource and capture the event listener the ads loader receives.
+ AdsMediaSource adsMediaSource =
+ new AdsMediaSource(
+ fakeContentMediaSource,
+ TEST_ADS_DATA_SPEC,
+ TEST_ADS_ID,
+ adMediaSourceFactory,
+ fakeAdsLoader,
+ mock(AdViewProvider.class),
+ /* useLazyContentSourcePreparation= */ false);
+ AtomicInteger mediaSourceCallerCallCounter = new AtomicInteger();
+ List externallyReceivedTimelines = new ArrayList<>();
+ List externallyRequestedPeriods = new ArrayList<>();
+ MediaSource.MediaSourceCaller fakeMediaSourceCaller =
+ (source, timeline) -> {
+ // The caller creates a media period at position 0 according to the timeline.
+ mediaSourceCallerCallCounter.incrementAndGet();
+ externallyReceivedTimelines.add(timeline);
+ Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
+ Timeline.Period period =
+ timeline.getPeriod(
+ window.firstPeriodIndex, new Timeline.Period(), /* setIds= */ true);
+ // Search for pre roll ad group if any.
+ int adGroupIndex =
+ period.adPlaybackState.getAdGroupIndexForPositionUs(
+ window.positionInFirstPeriodUs, period.durationUs);
+ MediaPeriodId mediaPeriodId =
+ adGroupIndex == C.INDEX_UNSET
+ ? new MediaPeriodId(period.uid, /* windowSequenceNumber= */ 0L)
+ : new MediaPeriodId(
+ 123L,
+ /* adGroupIndex= */ adGroupIndex,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0L);
+ externallyRequestedPeriods.add(mediaPeriodId);
+ // Create a media period immediately regardless whether it is the same as before.
+ source.createPeriod(mediaPeriodId, mock(Allocator.class), /* startPositionUs= */ 0L);
+ };
+
+ // Prepare the source which must not notify the caller with a timeline yet.
+ adsMediaSource.prepareSource(
+ fakeMediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // Verify ads loader was called with the content timeline to allow populating the ads.
+ assertThat(contentTimelineChangedCalledLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
+ // Verify external caller not yet notified even when content timeline available.
+ assertThat(mediaSourceCallerCallCounter.get()).isEqualTo(0);
+ // Verify no content media period has been created.
+ assertThat(contentMediaPeriodCreated.get()).isFalse();
+ // Verify ad source not yet prepared.
+ assertThat(adSourcePreparedCounter.get()).isEqualTo(0);
+
+ // Setting the ad playback state allows the outer AdsMediaSource to complete
+ // preparation of the AdsMediaSource that makes the external caller create the first period
+ // according to the timeline.
+ eventListenerRef
+ .get()
+ .onAdPlaybackState(
+ new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 0)
+ .withContentDurationUs(CONTENT_DURATION_US)
+ .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
+ .withAvailableAdMediaItem(
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ MediaItem.fromUri("https://google.com/ad"))
+ .withAdResumePositionUs(/* adResumePositionUs= */ 0)
+ .withAdDurationsUs(/* adGroupIndex= */ 0, 10_000_000L));
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // Ad source was prepared once.
+ assertThat(adSourcePreparedCounter.get()).isEqualTo(1);
+ // Verify that no content period was created. Content source prepared only to get the playlist.
+ assertThat(contentMediaPeriodCreated.get()).isFalse();
+ // Verify the caller got two timeline updates.
+ assertThat(mediaSourceCallerCallCounter.get()).isEqualTo(2);
+ // Verify whether every externally exposed timeline was augmented with ad data.
+ assertThat(externallyRequestedPeriods)
+ .containsExactly(
+ new MediaPeriodId(
+ 123L,
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0L),
+ new MediaPeriodId(
+ 123L,
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0L))
+ .inOrder();
+ // Verify the requested media ID in the child ad sources without ad data.
+ assertThat(createdAdMediaPeriodIds)
+ .containsExactly(
+ new MediaPeriodId(
+ new Pair<>(0, 0),
+ /* adGroupIndex= */ -1,
+ /* adIndexInAdGroup= */ -1,
+ /* windowSequenceNumber= */ 0L),
+ new MediaPeriodId(
+ new Pair<>(0, 0),
+ /* adGroupIndex= */ -1,
+ /* adIndexInAdGroup= */ -1,
+ /* windowSequenceNumber= */ 0L))
+ .inOrder();
+ // Verify all external exposed timelines contained ad data with the duration updated according
+ // to the actual duration of the ad sources.
+ assertThat(externallyReceivedTimelines).hasSize(2);
+ assertThat(
+ externallyReceivedTimelines
+ .get(0)
+ .getPeriod(0, new Timeline.Period())
+ .getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
+ .isEqualTo(C.TIME_UNSET); // Overridden by AdsMediaSource before the source was prepared.
+ assertThat(
+ externallyReceivedTimelines
+ .get(1)
+ .getPeriod(0, new Timeline.Period())
+ .getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
+ .isEqualTo(133_000_000); // Overridden by AdsMediaSource with the actual source duration.
+ }
+
+ @Test
+ public void prepare_withPrerollUsingLazyContentSourcePreparationTrue_allExternalTimelinesWithAds()
+ throws InterruptedException {
+ AtomicBoolean contentMediaPeriodCreated = new AtomicBoolean();
+ MediaSource fakeContentMediaSource =
+ new FakeMediaSource() {
+ @Override
+ public MediaPeriod createPeriod(
+ MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ contentMediaPeriodCreated.set(true);
+ return super.createPeriod(id, allocator, startPositionUs);
+ }
+ };
+ CountDownLatch adSourcePreparedLatch = new CountDownLatch(1);
+ AtomicInteger adSourcePreparedCounter = new AtomicInteger();
+ List createdAdMediaPeriodIds = new ArrayList<>();
+ MediaSource fakeAdMediaSource =
+ new FakeMediaSource() {
+ @Override
+ public synchronized void prepareSourceInternal(
+ @Nullable TransferListener mediaTransferListener) {
+ adSourcePreparedLatch.countDown();
+ adSourcePreparedCounter.incrementAndGet();
+ super.prepareSourceInternal(mediaTransferListener);
+ }
+
+ @Override
+ public MediaPeriod createPeriod(
+ MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ createdAdMediaPeriodIds.add(id);
+ return super.createPeriod(id, allocator, startPositionUs);
+ }
+ };
+ AtomicInteger contentTimelineChangedCallCount = new AtomicInteger();
+ AtomicReference eventListenerRef = new AtomicReference<>();
+ AdsLoader fakeAdsLoader =
+ new NoOpAdsLoader() {
+ @Override
+ public void start(
+ AdsMediaSource adsMediaSource,
+ DataSpec adTagDataSpec,
+ Object adsId,
+ AdViewProvider adViewProvider,
+ EventListener eventListener) {
+ eventListenerRef.set(eventListener);
+ }
+
+ @Override
+ public void handleContentTimelineChanged(MediaItem mediaItem, Timeline timeline) {
+ contentTimelineChangedCallCount.incrementAndGet();
+ }
+ };
+ MediaSource.Factory adMediaSourceFactory = mock(MediaSource.Factory.class);
+ when(adMediaSourceFactory.createMediaSource(any(MediaItem.class)))
+ .thenReturn(fakeAdMediaSource);
+ // Prepare the AdsMediaSource and capture the event listener the ads loader receives.
+ AdsMediaSource adsMediaSource =
+ new AdsMediaSource(
+ fakeContentMediaSource,
+ TEST_ADS_DATA_SPEC,
+ TEST_ADS_ID,
+ adMediaSourceFactory,
+ fakeAdsLoader,
+ mock(AdViewProvider.class),
+ /* useLazyContentSourcePreparation= */ true);
+ AtomicInteger mediaSourceCallerCallCounter = new AtomicInteger();
+ List externallyReceivedTimelines = new ArrayList<>();
+ List externallyRequestedPeriods = new ArrayList<>();
+ MediaSource.MediaSourceCaller fakeMediaSourceCaller =
+ (source, timeline) -> {
+ mediaSourceCallerCallCounter.incrementAndGet();
+ externallyReceivedTimelines.add(timeline);
+ Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
+ Timeline.Period period =
+ timeline.getPeriod(
+ window.firstPeriodIndex, new Timeline.Period(), /* setIds= */ true);
+ // Search for the preroll ad group.
+ int adGroupIndex =
+ period.adPlaybackState.getAdGroupIndexForPositionUs(
+ window.positionInFirstPeriodUs, period.durationUs);
+ MediaPeriodId mediaPeriodId =
+ adGroupIndex == C.INDEX_UNSET
+ ? new MediaPeriodId(period.uid, /* windowSequenceNumber= */ 0L)
+ : new MediaPeriodId(
+ 123L,
+ /* adGroupIndex= */ adGroupIndex,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0L);
+ externallyRequestedPeriods.add(mediaPeriodId);
+ // Create a media period immediately.
+ source.createPeriod(mediaPeriodId, mock(Allocator.class), /* startPositionUs= */ 0L);
+ };
+
+ // Prepare the source that must not result in an external timeline without ad data.
+ adsMediaSource.prepareSource(
+ fakeMediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // External caller not yet notified.
+ assertThat(mediaSourceCallerCallCounter.get()).isEqualTo(0);
+ // Verify that the content source is not prepared. Must never happen.
+ assertThat(contentTimelineChangedCallCount.get()).isEqualTo(0);
+ // Verify that th ad source is not yet prepared.
+ assertThat(adSourcePreparedCounter.get()).isEqualTo(0);
+
+ // Setting the ad playback state allows the outer AdsMediaSource to complete
+ // preparation of the AdsMediaSource that makes the external caller create the first period
+ // according to the timeline.
+ eventListenerRef
+ .get()
+ .onAdPlaybackState(
+ new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 0)
+ .withContentDurationUs(CONTENT_DURATION_US)
+ .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
+ .withAvailableAdMediaItem(
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ MediaItem.fromUri("https://google.com/ad"))
+ .withAdResumePositionUs(/* adResumePositionUs= */ 0)
+ .withAdDurationsUs(/* adGroupIndex= */ 0, 10_000_000L));
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // Content source not prepared.
+ assertThat(contentTimelineChangedCallCount.get()).isEqualTo(0);
+ // Verify that the ad source was prepared once.
+ assertThat(adSourcePreparedCounter.get()).isEqualTo(1);
+ // Verify that no content period was created.
+ assertThat(contentMediaPeriodCreated.get()).isFalse();
+ // Verify the caller got two timeline updates.
+ assertThat(mediaSourceCallerCallCounter.get()).isEqualTo(2);
+ // Verify whether every externally exposed timeline was augmented with ad data.
+ assertThat(externallyRequestedPeriods)
+ .containsExactly(
+ new MediaPeriodId(
+ 123L,
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0L),
+ new MediaPeriodId(
+ 123L,
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0L))
+ .inOrder();
+ // Verify the requested media ID in the child ad sources without ad data.
+ assertThat(createdAdMediaPeriodIds)
+ .containsExactly(
+ new MediaPeriodId(
+ new Pair<>(0, 0),
+ /* adGroupIndex= */ -1,
+ /* adIndexInAdGroup= */ -1,
+ /* windowSequenceNumber= */ 0L),
+ new MediaPeriodId(
+ new Pair<>(0, 0),
+ /* adGroupIndex= */ -1,
+ /* adIndexInAdGroup= */ -1,
+ /* windowSequenceNumber= */ 0L))
+ .inOrder();
+ // Verify all external exposed timeline contained ad data with the duration updated according
+ // to the actual duration of the ad sources.
+ assertThat(externallyReceivedTimelines).hasSize(2);
+ assertThat(
+ externallyReceivedTimelines
+ .get(0)
+ .getPeriod(0, new Timeline.Period())
+ .getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
+ .isEqualTo(C.TIME_UNSET); // Overridden by AdsMediaSource before the source was prepared.
+ assertThat(
+ externallyReceivedTimelines
+ .get(1)
+ .getPeriod(0, new Timeline.Period())
+ .getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
+ .isEqualTo(133_000_000); // Overridden by AdsMediaSource with the actual source duration.
+ }
+
+ private static class NoOpAdsLoader implements AdsLoader {
+
+ @Override
+ public void setPlayer(@Nullable Player player) {}
+
+ @Override
+ public void release() {}
+
+ @Override
+ public void setSupportedContentTypes(@C.ContentType int... contentTypes) {}
+
+ @Override
+ public void start(
+ AdsMediaSource adsMediaSource,
+ DataSpec adTagDataSpec,
+ Object adsId,
+ AdViewProvider adViewProvider,
+ EventListener eventListener) {}
+
+ @Override
+ public void stop(AdsMediaSource adsMediaSource, EventListener eventListener) {}
+
+ @Override
+ public void handlePrepareComplete(
+ AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup) {}
+
+ @Override
+ public void handlePrepareError(
+ AdsMediaSource adsMediaSource,
+ int adGroupIndex,
+ int adIndexInAdGroup,
+ IOException exception) {}
+
+ @Override
+ public void handleContentTimelineChanged(MediaItem mediaItem, Timeline timeline) {}
+ }
+
private static MediaSource buildMediaSource(MediaItem mediaItem) {
FakeMediaSource fakeMediaSource = new FakeMediaSource();
fakeMediaSource.setCanUpdateMediaItems(true);
@@ -344,6 +746,7 @@ public final class AdsMediaSourceTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()),
adsLoader,
- /* adViewProvider= */ () -> null);
+ /* adViewProvider= */ () -> null,
+ /* useLazyContentSourcePreparation= */ true);
}
}
diff --git a/libraries/exoplayer_ima/src/androidTest/java/androidx/media3/exoplayer/ima/ImaPlaybackTest.java b/libraries/exoplayer_ima/src/androidTest/java/androidx/media3/exoplayer/ima/ImaPlaybackTest.java
index cee2106100..d2ef7e5f25 100644
--- a/libraries/exoplayer_ima/src/androidTest/java/androidx/media3/exoplayer/ima/ImaPlaybackTest.java
+++ b/libraries/exoplayer_ima/src/androidTest/java/androidx/media3/exoplayer/ima/ImaPlaybackTest.java
@@ -241,7 +241,8 @@ public final class ImaPlaybackTest {
/* adsId= */ adTagDataSpec.uri,
new DefaultMediaSourceFactory(context),
Assertions.checkNotNull(imaAdsLoader),
- () -> overlayFrameLayout);
+ () -> overlayFrameLayout,
+ /* useLazyContentSourcePreparation= */ true);
}
@Override
diff --git a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java
index 8523ebad3c..3191c12121 100644
--- a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java
+++ b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java
@@ -177,7 +177,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
timelineWindowDefinitions =
new TimelineWindowDefinition[] {getInitialTimelineWindowDefinition(TEST_ADS_ID)};
adsLoaderListener = new TestAdsLoaderListener(/* periodIndex= */ 0);
@@ -760,7 +761,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs =
midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
@@ -802,7 +804,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs =
midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
@@ -843,7 +846,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs =
midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
@@ -882,7 +886,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs =
firstMidrollWindowTimeUs
@@ -931,7 +936,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs =
firstMidrollWindowTimeUs
@@ -1021,7 +1027,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
when(mockAdsManager.getAdCuePoints()).thenReturn(PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.setSupportedContentTypes(C.CONTENT_TYPE_OTHER);
@@ -1106,7 +1113,8 @@ public final class ImaAdsLoaderTest {
secondAdsId,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
timelineWindowDefinitions =
new TimelineWindowDefinition[] {
getInitialTimelineWindowDefinition(TEST_ADS_ID),
@@ -1166,7 +1174,8 @@ public final class ImaAdsLoaderTest {
secondAdsId,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
timelineWindowDefinitions =
new TimelineWindowDefinition[] {
getInitialTimelineWindowDefinition(TEST_ADS_ID),
@@ -1233,7 +1242,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
timelineWindowDefinitions =
new TimelineWindowDefinition[] {
getInitialTimelineWindowDefinition(TEST_ADS_ID),
@@ -1285,7 +1295,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
when(mockAdsManager.getAdCuePoints()).thenReturn(PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.setSupportedContentTypes(C.CONTENT_TYPE_OTHER);
@@ -1311,7 +1322,8 @@ public final class ImaAdsLoaderTest {
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
- adViewProvider);
+ adViewProvider,
+ /* useLazyContentSourcePreparation= */ true);
when(mockAdsManager.getAdCuePoints()).thenReturn(PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.setSupportedContentTypes(C.CONTENT_TYPE_OTHER);