diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java index e6db19a82b..0251ee2621 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java @@ -17,6 +17,7 @@ package androidx.media3.exoplayer.source.ads; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getAdCountInGroup; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getMediaPeriodPositionUs; @@ -36,7 +37,6 @@ import androidx.media3.common.StreamKey; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.TransferListener; @@ -57,6 +57,7 @@ import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.Allocator; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import java.io.IOException; @@ -75,8 +76,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * server-side inserted ad breaks and ensures that playback continues seamlessly with the wrapped * media across all transitions. * - *

The ad breaks need to be specified using {@link #setAdPlaybackState} and can be updated during - * playback. + *

The ad breaks need to be specified using {@link #setAdPlaybackStates} and can be updated + * during playback. */ @UnstableApi public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @@ -91,7 +92,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource * Called when the content source has refreshed the timeline. * *

If true is returned the source refresh publication is deferred, to wait for an {@link - * #setAdPlaybackState(AdPlaybackState) ad playback state update}. If false is returned, the + * #setAdPlaybackStates(ImmutableMap)} ad playback state update}. If false is returned, the * source refresh is immediately published. * *

Called on the playback thread. @@ -104,7 +105,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } private final MediaSource mediaSource; - private final ListMultimap mediaPeriods; + private final ListMultimap, SharedMediaPeriod> mediaPeriods; private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcherWithoutId; private final DrmSessionEventListener.EventDispatcher drmEventDispatcherWithoutId; @Nullable private final AdPlaybackStateUpdater adPlaybackStateUpdater; @@ -115,7 +116,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @Nullable private SharedMediaPeriod lastUsedMediaPeriod; @Nullable private Timeline contentTimeline; - private AdPlaybackState adPlaybackState; + private ImmutableMap adPlaybackStates; /** * Creates the media source. @@ -131,53 +132,74 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource this.mediaSource = mediaSource; this.adPlaybackStateUpdater = adPlaybackStateUpdater; mediaPeriods = ArrayListMultimap.create(); - adPlaybackState = AdPlaybackState.NONE; + adPlaybackStates = ImmutableMap.of(); mediaSourceEventDispatcherWithoutId = createEventDispatcher(/* mediaPeriodId= */ null); drmEventDispatcherWithoutId = createDrmEventDispatcher(/* mediaPeriodId= */ null); } /** - * Sets the {@link AdPlaybackState} published by this source. + * Sets the map of {@link AdPlaybackState ad playback states} published by this source. The key is + * the period UID of a period in the {@link + * AdPlaybackStateUpdater#onAdPlaybackStateUpdateRequested(Timeline)} content timeline}. + * + *

Each period has an {@link AdPlaybackState} that tells where in the period the ad groups + * start and end. Must only contain server-side inserted ad groups. The number of ad groups and + * the number of ads within an ad group may only increase. The durations of ads may change and the + * positions of future ad groups may change. Post-roll ad groups with {@link C#TIME_END_OF_SOURCE} + * must be empty and can be used as a placeholder for a future ad group. * *

May be called from any thread. * - *

Must only contain server-side inserted ad groups. The number of ad groups and the number of - * ads within an ad group may only increase. The durations of ads may change and the positions of - * future ad groups may change. Post-roll ad groups with {@link C#TIME_END_OF_SOURCE} must be - * empty and can be used as a placeholder for a future ad group. - * - * @param adPlaybackState The new {@link AdPlaybackState}. + * @param adPlaybackStates The map of {@link AdPlaybackState} keyed by their period UID. */ - public void setAdPlaybackState(AdPlaybackState adPlaybackState) { - checkArgument(adPlaybackState.adGroupCount >= this.adPlaybackState.adGroupCount); - for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { - AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(i); - checkArgument(adGroup.isServerSideInserted); - if (i < this.adPlaybackState.adGroupCount) { - checkArgument( - getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) - >= getAdCountInGroup(this.adPlaybackState, /* adGroupIndex= */ i)); - } - if (adGroup.timeUs == C.TIME_END_OF_SOURCE) { - checkArgument(getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) == 0); + public void setAdPlaybackStates(ImmutableMap adPlaybackStates) { + checkArgument(!adPlaybackStates.isEmpty()); + Object adsId = checkNotNull(adPlaybackStates.values().asList().get(0).adsId); + for (Map.Entry entry : adPlaybackStates.entrySet()) { + Object periodUid = entry.getKey(); + AdPlaybackState adPlaybackState = entry.getValue(); + checkArgument(Util.areEqual(adsId, adPlaybackState.adsId)); + @Nullable AdPlaybackState oldAdPlaybackState = this.adPlaybackStates.get(periodUid); + if (oldAdPlaybackState != null) { + for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(i); + checkArgument(adGroup.isServerSideInserted); + if (i < oldAdPlaybackState.adGroupCount) { + checkArgument( + getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) + >= getAdCountInGroup(oldAdPlaybackState, /* adGroupIndex= */ i)); + } + if (adGroup.timeUs == C.TIME_END_OF_SOURCE) { + checkArgument(getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) == 0); + } + } } } synchronized (this) { if (playbackHandler == null) { - this.adPlaybackState = adPlaybackState; + this.adPlaybackStates = adPlaybackStates; } else { playbackHandler.post( () -> { for (SharedMediaPeriod mediaPeriod : mediaPeriods.values()) { - mediaPeriod.updateAdPlaybackState(adPlaybackState); + @Nullable + AdPlaybackState adPlaybackState = adPlaybackStates.get(mediaPeriod.periodUid); + if (adPlaybackState != null) { + mediaPeriod.updateAdPlaybackState(adPlaybackState); + } } if (lastUsedMediaPeriod != null) { - lastUsedMediaPeriod.updateAdPlaybackState(adPlaybackState); + @Nullable + AdPlaybackState adPlaybackState = + adPlaybackStates.get(lastUsedMediaPeriod.periodUid); + if (adPlaybackState != null) { + lastUsedMediaPeriod.updateAdPlaybackState(adPlaybackState); + } } - this.adPlaybackState = adPlaybackState; + this.adPlaybackStates = adPlaybackStates; if (contentTimeline != null) { refreshSourceInfo( - new ServerSideAdInsertionTimeline(contentTimeline, adPlaybackState)); + new ServerSideAdInsertionTimeline(contentTimeline, adPlaybackStates)); } }); } @@ -221,8 +243,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource this.contentTimeline = timeline; if ((adPlaybackStateUpdater == null || !adPlaybackStateUpdater.onAdPlaybackStateUpdateRequested(timeline)) - && !AdPlaybackState.NONE.equals(adPlaybackState)) { - refreshSourceInfo(new ServerSideAdInsertionTimeline(timeline, adPlaybackState)); + && !adPlaybackStates.isEmpty()) { + refreshSourceInfo(new ServerSideAdInsertionTimeline(timeline, adPlaybackStates)); } } @@ -240,19 +262,26 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { - SharedMediaPeriod sharedPeriod; + @Nullable SharedMediaPeriod sharedPeriod = null; + Pair sharedMediaPeriodKey = new Pair<>(id.windowSequenceNumber, id.periodUid); if (lastUsedMediaPeriod != null) { - sharedPeriod = lastUsedMediaPeriod; + if (lastUsedMediaPeriod.periodUid.equals(id.periodUid)) { + sharedPeriod = lastUsedMediaPeriod; + mediaPeriods.put(sharedMediaPeriodKey, sharedPeriod); + } else { + lastUsedMediaPeriod.release(mediaSource); + } lastUsedMediaPeriod = null; - mediaPeriods.put(id.windowSequenceNumber, sharedPeriod); - } else { + } + if (sharedPeriod == null) { @Nullable SharedMediaPeriod lastExistingPeriod = - Iterables.getLast(mediaPeriods.get(id.windowSequenceNumber), /* defaultValue= */ null); + Iterables.getLast(mediaPeriods.get(sharedMediaPeriodKey), /* defaultValue= */ null); if (lastExistingPeriod != null && lastExistingPeriod.canReuseMediaPeriod(id, startPositionUs)) { sharedPeriod = lastExistingPeriod; } else { + AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(id.periodUid)); long streamPositionUs = getStreamPositionUs(startPositionUs, id, adPlaybackState); sharedPeriod = new SharedMediaPeriod( @@ -260,8 +289,9 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource new MediaPeriodId(id.periodUid, id.windowSequenceNumber), allocator, streamPositionUs), + id.periodUid, adPlaybackState); - mediaPeriods.put(id.windowSequenceNumber, sharedPeriod); + mediaPeriods.put(sharedMediaPeriodKey, sharedPeriod); } } MediaPeriodImpl mediaPeriod = @@ -277,7 +307,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource mediaPeriodImpl.sharedPeriod.remove(mediaPeriodImpl); if (mediaPeriodImpl.sharedPeriod.isUnused()) { mediaPeriods.remove( - mediaPeriodImpl.mediaPeriodId.windowSequenceNumber, mediaPeriodImpl.sharedPeriod); + new Pair<>( + mediaPeriodImpl.mediaPeriodId.windowSequenceNumber, + mediaPeriodImpl.mediaPeriodId.periodUid), + mediaPeriodImpl.sharedPeriod); if (mediaPeriods.isEmpty()) { // Keep until disabled. lastUsedMediaPeriod = mediaPeriodImpl.sharedPeriod; @@ -381,7 +414,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } else { mediaPeriod.sharedPeriod.onLoadStarted(loadEventInfo, mediaLoadData); mediaPeriod.mediaSourceEventDispatcher.loadStarted( - loadEventInfo, correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + loadEventInfo, + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -399,7 +436,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } else { mediaPeriod.sharedPeriod.onLoadFinished(loadEventInfo); mediaPeriod.mediaSourceEventDispatcher.loadCompleted( - loadEventInfo, correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + loadEventInfo, + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -417,7 +458,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } else { mediaPeriod.sharedPeriod.onLoadFinished(loadEventInfo); mediaPeriod.mediaSourceEventDispatcher.loadCanceled( - loadEventInfo, correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + loadEventInfo, + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -441,7 +486,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } mediaPeriod.mediaSourceEventDispatcher.loadError( loadEventInfo, - correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState), + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid))), error, wasCanceled); } @@ -457,7 +505,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource mediaSourceEventDispatcherWithoutId.upstreamDiscarded(mediaLoadData); } else { mediaPeriod.mediaSourceEventDispatcher.upstreamDiscarded( - correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -472,7 +523,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } else { mediaPeriod.sharedPeriod.onDownstreamFormatChanged(mediaPeriod, mediaLoadData); mediaPeriod.mediaSourceEventDispatcher.downstreamFormatChanged( - correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -491,7 +545,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource if (mediaPeriodId == null) { return null; } - List periods = mediaPeriods.get(mediaPeriodId.windowSequenceNumber); + List periods = + mediaPeriods.get(new Pair<>(mediaPeriodId.windowSequenceNumber, mediaPeriodId.periodUid)); if (periods.isEmpty()) { return null; } @@ -560,6 +615,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource private final MediaPeriod actualMediaPeriod; private final List mediaPeriods; private final Map> activeLoads; + private final Object periodUid; private AdPlaybackState adPlaybackState; @Nullable private MediaPeriodImpl loadingPeriod; @@ -569,8 +625,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource public @NullableType SampleStream[] sampleStreams; public @NullableType MediaLoadData[] lastDownstreamFormatChangeData; - public SharedMediaPeriod(MediaPeriod actualMediaPeriod, AdPlaybackState adPlaybackState) { + public SharedMediaPeriod( + MediaPeriod actualMediaPeriod, Object periodUid, AdPlaybackState adPlaybackState) { this.actualMediaPeriod = actualMediaPeriod; + this.periodUid = periodUid; this.adPlaybackState = adPlaybackState; mediaPeriods = new ArrayList<>(); activeLoads = new HashMap<>(); @@ -931,19 +989,27 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource private static final class ServerSideAdInsertionTimeline extends ForwardingTimeline { - private final AdPlaybackState adPlaybackState; + private final ImmutableMap adPlaybackStates; public ServerSideAdInsertionTimeline( - Timeline contentTimeline, AdPlaybackState adPlaybackState) { + Timeline contentTimeline, ImmutableMap adPlaybackStates) { super(contentTimeline); - Assertions.checkState(contentTimeline.getPeriodCount() == 1); - Assertions.checkState(contentTimeline.getWindowCount() == 1); - this.adPlaybackState = adPlaybackState; + checkState(contentTimeline.getPeriodCount() == 1); + checkState(contentTimeline.getWindowCount() == 1); + Period period = new Period(); + for (int i = 0; i < contentTimeline.getPeriodCount(); i++) { + contentTimeline.getPeriod(/* periodIndex= */ i, period, /* setIds= */ true); + checkState(adPlaybackStates.containsKey(checkNotNull(period.uid))); + } + this.adPlaybackStates = adPlaybackStates; } @Override public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { super.getWindow(windowIndex, window, defaultPositionProjectionUs); + Object firstPeriodUid = + checkNotNull(getPeriod(/* periodIndex= */ 0, new Period(), /* setIds= */ true).uid); + AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(firstPeriodUid)); long positionInPeriodUs = getMediaPeriodPositionUsForContent( window.positionInFirstPeriodUs, @@ -968,7 +1034,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - super.getPeriod(periodIndex, period, setIds); + super.getPeriod(periodIndex, period, /* setIds= */ true); + AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(period.uid)); long durationUs = period.durationUs; if (durationUs == C.TIME_UNSET) { durationUs = adPlaybackState.contentDurationUs; diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSourceTest.java index b6f7ca45eb..4db0210883 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSourceTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSourceTest.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer.source.ads; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilPosition; @@ -32,6 +33,7 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.SurfaceTexture; +import android.util.Pair; import android.view.Surface; import androidx.media3.common.AdPlaybackState; import androidx.media3.common.MediaItem; @@ -50,7 +52,9 @@ import androidx.media3.test.utils.robolectric.PlaybackOutput; import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableMap; import java.util.concurrent.atomic.AtomicReference; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -104,8 +108,8 @@ public final class ServerSideAdInsertionMediaSourceTest { .withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 400_000) .withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 200_000); AtomicReference timelineReference = new AtomicReference<>(); + mediaSource.setAdPlaybackStates(ImmutableMap.of(new Pair<>(0, 0), adPlaybackState)); - mediaSource.setAdPlaybackState(adPlaybackState); mediaSource.prepareSource( (source, timeline) -> timelineReference.set(timeline), /* mediaTransferListener= */ null, @@ -143,6 +147,26 @@ public final class ServerSideAdInsertionMediaSourceTest { assertThat(window.durationUs).isEqualTo(9_800_000); } + @Test + public void timeline_missingAdPlaybackStateByPeriodUid_isAssertedAndThrows() { + ServerSideAdInsertionMediaSource mediaSource = + new ServerSideAdInsertionMediaSource( + new FakeMediaSource(), /* adPlaybackStateUpdater= */ null); + // The map of adPlaybackStates does not contain a valid period UID as key. + mediaSource.setAdPlaybackStates( + ImmutableMap.of(new Object(), new AdPlaybackState(/* adsId= */ new Object()))); + + Assert.assertThrows( + IllegalStateException.class, + () -> + mediaSource.prepareSource( + (source, timeline) -> { + /* Do nothing. */ + }, + /* mediaTransferListener= */ null, + PlayerId.UNSET)); + } + @Test public void playbackWithPredefinedAds_playsSuccessfulWithoutRendererResets() throws Exception { Context context = ApplicationProvider.getApplicationContext(); @@ -154,10 +178,6 @@ public final class ServerSideAdInsertionMediaSourceTest { player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory); - ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), - /* adPlaybackStateUpdater= */ null); AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); adPlaybackState = addAdGroupToAdPlaybackState( @@ -171,17 +191,32 @@ public final class ServerSideAdInsertionMediaSourceTest { /* fromPositionUs= */ 400_000, /* toPositionUs= */ 700_000, /* contentResumeOffsetUs= */ 1_000_000); - adPlaybackState = + AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 900_000, /* toPositionUs= */ 1_000_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); + + AtomicReference mediaSourceRef = new AtomicReference<>(); + mediaSourceRef.set( + new ServerSideAdInsertionMediaSource( + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + contentTimeline -> { + Object periodUid = + checkNotNull( + contentTimeline.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState)); + return true; + })); AnalyticsListener listener = mock(AnalyticsListener.class); player.addAnalyticsListener(listener); - player.setMediaSource(mediaSource); + player.setMediaSource(mediaSourceRef.get()); player.prepare(); player.play(); runUntilPlaybackState(player, Player.STATE_ENDED); @@ -205,6 +240,7 @@ public final class ServerSideAdInsertionMediaSourceTest { @Test public void playbackWithNewlyInsertedAds_playsSuccessfulWithoutRendererResets() throws Exception { Context context = ApplicationProvider.getApplicationContext(); + AtomicReference periodUid = new AtomicReference<>(); CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(context); ExoPlayer player = new ExoPlayer.Builder(context, renderersFactory) @@ -213,33 +249,43 @@ public final class ServerSideAdInsertionMediaSourceTest { player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory); - ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), - /* adPlaybackStateUpdater= */ null); - AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); - adPlaybackState = + AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( - adPlaybackState, + new AdPlaybackState(/* adsId= */ new Object()), /* fromPositionUs= */ 900_000, /* toPositionUs= */ 1_000_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); - + AtomicReference mediaSourceRef = new AtomicReference<>(); + mediaSourceRef.set( + new ServerSideAdInsertionMediaSource( + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ contentTimeline -> { + periodUid.set( + checkNotNull( + contentTimeline.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid)); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState)); + return true; + })); AnalyticsListener listener = mock(AnalyticsListener.class); player.addAnalyticsListener(listener); - player.setMediaSource(mediaSource); + player.setMediaSource(mediaSourceRef.get()); player.prepare(); // Add ad at the current playback position during playback. runUntilPlaybackState(player, Player.STATE_READY); - adPlaybackState = + AdPlaybackState secondAdPlaybackState = addAdGroupToAdPlaybackState( - adPlaybackState, + firstAdPlaybackState, /* fromPositionUs= */ 0, /* toPositionUs= */ 500_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); runUntilPendingCommandsAreFullyHandled(player); player.play(); @@ -265,6 +311,7 @@ public final class ServerSideAdInsertionMediaSourceTest { public void playbackWithAdditionalAdsInAdGroup_playsSuccessfulWithoutRendererResets() throws Exception { Context context = ApplicationProvider.getApplicationContext(); + AtomicReference periodUid = new AtomicReference<>(); CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(context); ExoPlayer player = new ExoPlayer.Builder(context, renderersFactory) @@ -273,32 +320,45 @@ public final class ServerSideAdInsertionMediaSourceTest { player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory); - ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), - /* adPlaybackStateUpdater= */ null); - AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); - adPlaybackState = + AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( - adPlaybackState, + new AdPlaybackState(/* adsId= */ new Object()), /* fromPositionUs= */ 0, /* toPositionUs= */ 500_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); + AtomicReference mediaSourceRef = new AtomicReference<>(); + mediaSourceRef.set( + new ServerSideAdInsertionMediaSource( + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ contentTimeline -> { + if (periodUid.get() == null) { + periodUid.set( + checkNotNull( + contentTimeline.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid)); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState)); + } + return true; + })); AnalyticsListener listener = mock(AnalyticsListener.class); player.addAnalyticsListener(listener); - player.setMediaSource(mediaSource); + player.setMediaSource(mediaSourceRef.get()); player.prepare(); // Wait until playback is ready with first ad and then replace by 3 ads. runUntilPlaybackState(player, Player.STATE_READY); - adPlaybackState = - adPlaybackState + AdPlaybackState secondAdPlaybackState = + firstAdPlaybackState .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3) .withAdDurationsUs( /* adGroupIndex= */ 0, /* adDurationsUs...= */ 50_000, 250_000, 200_000); - mediaSource.setAdPlaybackState(adPlaybackState); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); runUntilPendingCommandsAreFullyHandled(player); player.play(); @@ -327,10 +387,6 @@ public final class ServerSideAdInsertionMediaSourceTest { new ExoPlayer.Builder(context).setClock(new FakeClock(/* isAutoAdvancing= */ true)).build(); player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); - ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), - /* adPlaybackStateUpdater= */ null); AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); adPlaybackState = addAdGroupToAdPlaybackState( @@ -344,17 +400,32 @@ public final class ServerSideAdInsertionMediaSourceTest { /* fromPositionUs= */ 600_000, /* toPositionUs= */ 700_000, /* contentResumeOffsetUs= */ 1_000_000); - adPlaybackState = + AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 900_000, /* toPositionUs= */ 1_000_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); + + AtomicReference mediaSourceRef = new AtomicReference<>(); + mediaSourceRef.set( + new ServerSideAdInsertionMediaSource( + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ contentTimeline -> { + Object periodUid = + checkNotNull( + contentTimeline.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState)); + return true; + })); AnalyticsListener listener = mock(AnalyticsListener.class); player.addAnalyticsListener(listener); - player.setMediaSource(mediaSource); + player.setMediaSource(mediaSourceRef.get()); player.prepare(); // Play to the first content part, then seek past the midroll. playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 100);