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