From 1af0b5b432b67f6a43be00ea0ad61054bf14549c Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 15 Nov 2024 07:46:18 -0800 Subject: [PATCH] Add handleContentTimelineChanged to AdsLoader The new callback allows an app to read data from the content timeline to populate the `AdPlaybackState`. To avoid a deadlock between the `AdMediaSource` waiting for the `AdPlaybackState` and the app waiting for the content `Timeline`, a boolean flag `useLazyContentSourcePreparation` is introduced to tell the `AdsMediaSource` to prepare the content source immediately to make the `Timeline` available. A unit test verifies that in none of the cases (lazy preparation or immediate preparation) a `Timeline` without ad data is leaked to the caller. This ensures that in the case of a preroll, the player won't initially and accidentally read content media data before starting to load the preroll ad. While the content source is prepared early, no content media period must be created before the preroll starts. PiperOrigin-RevId: 696885392 --- .../source/DefaultMediaSourceFactory.java | 3 +- .../exoplayer/source/ads/AdsLoader.java | 19 + .../exoplayer/source/ads/AdsMediaSource.java | 49 ++- .../source/ads/AdsMediaSourceTest.java | 407 +++++++++++++++++- .../media3/exoplayer/ima/ImaPlaybackTest.java | 3 +- .../exoplayer/ima/ImaAdsLoaderTest.java | 36 +- 6 files changed, 498 insertions(+), 19 deletions(-) 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);