Use MaskingMediaSource for AdsMediaSource content source

This means the content source is 'prepared' instantly with a
placeholder, enabling all further preparation steps (e.g. loading
preroll ads) while the actual content is still preparing. This
improvement can speed up the start time for prerolls in  manifest-based
content that doesn't have a zero-time preparation step like progressive
media.

Issue: androidx/media#1358
PiperOrigin-RevId: 633640746
This commit is contained in:
tonihei 2024-05-14 10:59:31 -07:00 committed by Copybara-Service
parent bf7b4e0d8c
commit d27c36ac9e
3 changed files with 29 additions and 10 deletions

View File

@ -30,6 +30,9 @@
* Add new error code * Add new error code
`PlaybackException.ERROR_CODE_DECODING_RESOURCES_RECLAIMED` that is used `PlaybackException.ERROR_CODE_DECODING_RESOURCES_RECLAIMED` that is used
when codec resources are reclaimed for higher priority tasks. 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: * Transformer:
* Work around a decoder bug where the number of audio channels was capped * Work around a decoder bug where the number of audio channels was capped
at stereo when handling PCM input. at stereo when handling PCM input.

View File

@ -38,6 +38,7 @@ import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.source.CompositeMediaSource; import androidx.media3.exoplayer.source.CompositeMediaSource;
import androidx.media3.exoplayer.source.LoadEventInfo; import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MaskingMediaPeriod; import androidx.media3.exoplayer.source.MaskingMediaPeriod;
import androidx.media3.exoplayer.source.MaskingMediaSource;
import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource;
@ -134,7 +135,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
private static final MediaPeriodId CHILD_SOURCE_MEDIA_PERIOD_ID = private static final MediaPeriodId CHILD_SOURCE_MEDIA_PERIOD_ID =
new MediaPeriodId(/* periodUid= */ new Object()); new MediaPeriodId(/* periodUid= */ new Object());
private final MediaSource contentMediaSource; private final MaskingMediaSource contentMediaSource;
@Nullable final MediaItem.DrmConfiguration contentDrmConfiguration; @Nullable final MediaItem.DrmConfiguration contentDrmConfiguration;
private final MediaSource.Factory adMediaSourceFactory; private final MediaSource.Factory adMediaSourceFactory;
private final AdsLoader adsLoader; private final AdsLoader adsLoader;
@ -171,7 +172,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
MediaSource.Factory adMediaSourceFactory, MediaSource.Factory adMediaSourceFactory,
AdsLoader adsLoader, AdsLoader adsLoader,
AdViewProvider adViewProvider) { AdViewProvider adViewProvider) {
this.contentMediaSource = contentMediaSource; this.contentMediaSource =
new MaskingMediaSource(contentMediaSource, /* useLazyPreparation= */ true);
this.contentDrmConfiguration = this.contentDrmConfiguration =
checkNotNull(contentMediaSource.getMediaItem().localConfiguration).drmConfiguration; checkNotNull(contentMediaSource.getMediaItem().localConfiguration).drmConfiguration;
this.adMediaSourceFactory = adMediaSourceFactory; this.adMediaSourceFactory = adMediaSourceFactory;
@ -206,6 +208,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
super.prepareSourceInternal(mediaTransferListener); super.prepareSourceInternal(mediaTransferListener);
ComponentListener componentListener = new ComponentListener(); ComponentListener componentListener = new ComponentListener();
this.componentListener = componentListener; this.componentListener = componentListener;
contentTimeline = contentMediaSource.getTimeline();
prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource); prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource);
mainHandler.post( mainHandler.post(
() -> () ->

View File

@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf; import static org.robolectric.Shadows.shadowOf;
@ -35,6 +36,7 @@ import androidx.media3.common.Timeline;
import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MaskingMediaSource;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
@ -81,7 +83,9 @@ public final class AdsMediaSourceTest {
/* isDynamic= */ false, /* isDynamic= */ false,
/* useLiveConfiguration= */ false, /* useLiveConfiguration= */ false,
/* manifest= */ null, /* 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 = private static final Object CONTENT_PERIOD_UID =
CONTENT_TIMELINE.getUidOfPeriod(/* periodIndex= */ 0); CONTENT_TIMELINE.getUidOfPeriod(/* periodIndex= */ 0);
@ -147,7 +151,8 @@ public final class AdsMediaSourceTest {
} }
@Test @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); contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE);
adsMediaSource.createPeriod( adsMediaSource.createPeriod(
new MediaPeriodId( new MediaPeriodId(
@ -162,11 +167,14 @@ public final class AdsMediaSourceTest {
assertThat(prerollAdMediaSource.isPrepared()).isTrue(); assertThat(prerollAdMediaSource.isPrepared()).isTrue();
verify(mockMediaSourceCaller) verify(mockMediaSourceCaller)
.onSourceInfoRefreshed( .onSourceInfoRefreshed(
adsMediaSource, new SinglePeriodAdTimeline(CONTENT_TIMELINE, AD_PLAYBACK_STATE)); adsMediaSource,
new SinglePeriodAdTimeline(PLACEHOLDER_CONTENT_TIMELINE, AD_PLAYBACK_STATE));
} }
@Test @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); contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE);
adsMediaSource.createPeriod( adsMediaSource.createPeriod(
new MediaPeriodId( new MediaPeriodId(
@ -183,13 +191,12 @@ public final class AdsMediaSourceTest {
.onSourceInfoRefreshed( .onSourceInfoRefreshed(
adsMediaSource, adsMediaSource,
new SinglePeriodAdTimeline( new SinglePeriodAdTimeline(
CONTENT_TIMELINE, PLACEHOLDER_CONTENT_TIMELINE,
AD_PLAYBACK_STATE.withAdDurationsUs(new long[][] {{PREROLL_AD_DURATION_US}}))); AD_PLAYBACK_STATE.withAdDurationsUs(new long[][] {{PREROLL_AD_DURATION_US}})));
} }
@Test @Test
public void createPeriod_createsChildPrerollAdMediaPeriod() { public void createPeriod_forPreroll_createsChildPrerollAdMediaPeriod() {
contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE);
adsMediaSource.createPeriod( adsMediaSource.createPeriod(
new MediaPeriodId( new MediaPeriodId(
CONTENT_PERIOD_UID, CONTENT_PERIOD_UID,
@ -206,7 +213,7 @@ public final class AdsMediaSourceTest {
} }
@Test @Test
public void createPeriod_createsChildContentMediaPeriod() { public void createPeriod_forContent_createsChildContentMediaPeriodAndLoadsContentTimeline() {
contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE);
shadowOf(Looper.getMainLooper()).idle(); shadowOf(Looper.getMainLooper()).idle();
adsMediaSource.createPeriod( adsMediaSource.createPeriod(
@ -216,6 +223,12 @@ public final class AdsMediaSourceTest {
contentMediaSource.assertMediaPeriodCreated( contentMediaSource.assertMediaPeriodCreated(
new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0)); new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0));
ArgumentCaptor<Timeline> 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 @Test