diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7e5b46ea26..4c9126eaa3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -30,6 +30,9 @@ * Add new error code `PlaybackException.ERROR_CODE_DECODING_RESOURCES_RECLAIMED` that is used when codec resources are reclaimed for higher priority tasks. + * Let `AdsMediaSource` load preroll ads before initial content media + preparation completes + ([#1358](https://github.com/androidx/media/issues/1358)). * Transformer: * Work around a decoder bug where the number of audio channels was capped at stereo when handling PCM input. 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 64c16b199f..0df30cb9e8 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 @@ -38,6 +38,7 @@ import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.source.CompositeMediaSource; import androidx.media3.exoplayer.source.LoadEventInfo; import androidx.media3.exoplayer.source.MaskingMediaPeriod; +import androidx.media3.exoplayer.source.MaskingMediaSource; import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource; @@ -134,7 +135,7 @@ public final class AdsMediaSource extends CompositeMediaSource { private static final MediaPeriodId CHILD_SOURCE_MEDIA_PERIOD_ID = new MediaPeriodId(/* periodUid= */ new Object()); - private final MediaSource contentMediaSource; + private final MaskingMediaSource contentMediaSource; @Nullable final MediaItem.DrmConfiguration contentDrmConfiguration; private final MediaSource.Factory adMediaSourceFactory; private final AdsLoader adsLoader; @@ -171,7 +172,8 @@ public final class AdsMediaSource extends CompositeMediaSource { MediaSource.Factory adMediaSourceFactory, AdsLoader adsLoader, AdViewProvider adViewProvider) { - this.contentMediaSource = contentMediaSource; + this.contentMediaSource = + new MaskingMediaSource(contentMediaSource, /* useLazyPreparation= */ true); this.contentDrmConfiguration = checkNotNull(contentMediaSource.getMediaItem().localConfiguration).drmConfiguration; this.adMediaSourceFactory = adMediaSourceFactory; @@ -206,6 +208,7 @@ public final class AdsMediaSource extends CompositeMediaSource { super.prepareSourceInternal(mediaTransferListener); ComponentListener componentListener = new ComponentListener(); this.componentListener = componentListener; + contentTimeline = contentMediaSource.getTimeline(); prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource); mainHandler.post( () -> 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 b81679ad5d..cb502b5c8d 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 @@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; @@ -35,6 +36,7 @@ import androidx.media3.common.Timeline; import androidx.media3.datasource.DataSpec; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; +import androidx.media3.exoplayer.source.MaskingMediaSource; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; @@ -81,7 +83,9 @@ public final class AdsMediaSourceTest { /* isDynamic= */ false, /* useLiveConfiguration= */ false, /* manifest= */ null, - MediaItem.fromUri(Uri.parse("https://google.com/empty"))); + FakeMediaSource.FAKE_MEDIA_ITEM); + private static final Timeline PLACEHOLDER_CONTENT_TIMELINE = + new MaskingMediaSource.PlaceholderTimeline(FakeMediaSource.FAKE_MEDIA_ITEM); private static final Object CONTENT_PERIOD_UID = CONTENT_TIMELINE.getUidOfPeriod(/* periodIndex= */ 0); @@ -147,7 +151,8 @@ public final class AdsMediaSourceTest { } @Test - public void createPeriod_preparesChildAdMediaSourceAndRefreshesSourceInfo() { + public void createPeriod_forPreroll_preparesChildAdMediaSourceAndRefreshesSourceInfo() { + // This should be unused if we only create the preroll period. contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); adsMediaSource.createPeriod( new MediaPeriodId( @@ -162,11 +167,14 @@ public final class AdsMediaSourceTest { assertThat(prerollAdMediaSource.isPrepared()).isTrue(); verify(mockMediaSourceCaller) .onSourceInfoRefreshed( - adsMediaSource, new SinglePeriodAdTimeline(CONTENT_TIMELINE, AD_PLAYBACK_STATE)); + adsMediaSource, + new SinglePeriodAdTimeline(PLACEHOLDER_CONTENT_TIMELINE, AD_PLAYBACK_STATE)); } @Test - public void createPeriod_preparesChildAdMediaSourceAndRefreshesSourceInfoWithAdMediaSourceInfo() { + public void + createPeriod_forPreroll_preparesChildAdMediaSourceAndRefreshesSourceInfoWithAdMediaSourceInfo() { + // This should be unused if we only create the preroll period. contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); adsMediaSource.createPeriod( new MediaPeriodId( @@ -183,13 +191,12 @@ public final class AdsMediaSourceTest { .onSourceInfoRefreshed( adsMediaSource, new SinglePeriodAdTimeline( - CONTENT_TIMELINE, + PLACEHOLDER_CONTENT_TIMELINE, AD_PLAYBACK_STATE.withAdDurationsUs(new long[][] {{PREROLL_AD_DURATION_US}}))); } @Test - public void createPeriod_createsChildPrerollAdMediaPeriod() { - contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); + public void createPeriod_forPreroll_createsChildPrerollAdMediaPeriod() { adsMediaSource.createPeriod( new MediaPeriodId( CONTENT_PERIOD_UID, @@ -206,7 +213,7 @@ public final class AdsMediaSourceTest { } @Test - public void createPeriod_createsChildContentMediaPeriod() { + public void createPeriod_forContent_createsChildContentMediaPeriodAndLoadsContentTimeline() { contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); shadowOf(Looper.getMainLooper()).idle(); adsMediaSource.createPeriod( @@ -216,6 +223,12 @@ public final class AdsMediaSourceTest { contentMediaSource.assertMediaPeriodCreated( new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0)); + ArgumentCaptor adsTimelineCaptor = ArgumentCaptor.forClass(Timeline.class); + verify(mockMediaSourceCaller, times(2)) + .onSourceInfoRefreshed(eq(adsMediaSource), adsTimelineCaptor.capture()); + TestUtil.timelinesAreSame( + adsTimelineCaptor.getValue(), + new SinglePeriodAdTimeline(CONTENT_TIMELINE, AD_PLAYBACK_STATE)); } @Test