From f599a9b8f90d2ce14eb4025ad82c04db62a2920b Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 29 Mar 2023 16:33:35 +0000 Subject: [PATCH] Keep content timeline and ad playback states together For multi-period live streams the content timeline for which the global ad playback state has been split needs to be kept together to not run into a race between timeline refreshes and ad events. PiperOrigin-RevId: 520358964 --- RELEASENOTES.md | 5 + .../ads/ServerSideAdInsertionMediaSource.java | 22 +- .../media3/exoplayer/ExoPlayerTest.java | 247 ++++++++++++------ .../exoplayer/MediaPeriodQueueTest.java | 27 +- .../ServerSideAdInsertionMediaSourceTest.java | 38 ++- .../ImaServerSideAdInsertionMediaSource.java | 3 +- 6 files changed, 234 insertions(+), 108 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c1459bc261..3bc30c13c7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,11 @@ * Add parameters to `LoadControl` methods `shouldStartPlayback` and `onTracksSelected` that allow associating these methods with the relevant `MediaPeriod`. + * Change signature of + `ServerSideAdInsertionMediaSource.setAdPlaybackStates(Map)` by adding a timeline parameter that contains the + periods with the UIDs used as keys in the map. This is required to avoid + concurrency issues with multi-period live streams. * Audio: * Fix bug where some playbacks fail when tunneling is enabled and `AudioProcessors` are active, e.g. for gapless trimming 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 296cc3a3a6..a210cc689b 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 @@ -92,8 +92,8 @@ 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 - * #setAdPlaybackStates(ImmutableMap)} ad playback state update}. If false is returned, the - * source refresh is immediately published. + * #setAdPlaybackStates(ImmutableMap, Timeline)} ad playback state update}. If false is + * returned, the source refresh is immediately published. * *

Called on the playback thread. * @@ -115,7 +115,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource private Handler playbackHandler; @Nullable private SharedMediaPeriod lastUsedMediaPeriod; - @Nullable private Timeline contentTimeline; private ImmutableMap adPlaybackStates; /** @@ -139,8 +138,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource /** * 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}. + * the period UID of a period in the {@code contentTimeline}. * *

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 @@ -151,8 +149,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource *

May be called from any thread. * * @param adPlaybackStates The map of {@link AdPlaybackState} keyed by their period UID. + * @param contentTimeline The content timeline containing the periods with the UIDs used as keys + * in the map of playback states. */ - public void setAdPlaybackStates(ImmutableMap adPlaybackStates) { + public void setAdPlaybackStates( + ImmutableMap adPlaybackStates, Timeline contentTimeline) { checkArgument(!adPlaybackStates.isEmpty()); Object adsId = checkNotNull(adPlaybackStates.values().asList().get(0).adsId); for (Map.Entry entry : adPlaybackStates.entrySet()) { @@ -188,7 +189,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource if (playbackHandler == null) { this.adPlaybackStates = adPlaybackStates; } else { - Timeline finalContentTimeline = contentTimeline; playbackHandler.post( () -> { for (SharedMediaPeriod mediaPeriod : mediaPeriods.values()) { @@ -207,10 +207,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } } this.adPlaybackStates = adPlaybackStates; - if (finalContentTimeline != null) { - refreshSourceInfo( - new ServerSideAdInsertionTimeline(finalContentTimeline, adPlaybackStates)); - } + refreshSourceInfo( + new ServerSideAdInsertionTimeline(contentTimeline, adPlaybackStates)); }); } } @@ -250,7 +248,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { - this.contentTimeline = timeline; if ((adPlaybackStateUpdater == null || !adPlaybackStateUpdater.onAdPlaybackStateUpdateRequested(timeline)) && !adPlaybackStates.isEmpty()) { @@ -261,7 +258,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @Override protected void releaseSourceInternal() { releaseLastUsedMediaPeriod(); - contentTimeline = null; synchronized (this) { playbackHandler = null; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 0403daf231..044a099692 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -171,6 +171,7 @@ import androidx.media3.test.utils.robolectric.TestPlayerRunHelper; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -5050,25 +5051,40 @@ public final class ExoPlayerTest { ArgumentCaptor newPositionArgumentCaptor = ArgumentCaptor.forClass(PositionInfo.class); ArgumentCaptor reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class); - FakeTimeline adTimeline = + // Create a multi-period timeline without ads. + FakeTimeline fakeContentTimeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ 4, + "windowId", + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ DEFAULT_WINDOW_DURATION_US, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + /* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE), + MediaItem.EMPTY)); + // Create the ad playback state matching to the periods in the content timeline. + ImmutableMap adPlaybackStates = FakeTimeline.createMultiPeriodAdTimeline( - "windowId", - /* numberOfPlayedAds= */ 0, - /* isAdPeriodFlags...= */ false, - true, - true, - false); + "windowId", + /* numberOfPlayedAds= */ 0, + /* isAdPeriodFlags...= */ false, + true, + true, + false) + .getAdPlaybackStates(/* windowIndex= */ 0); Listener listener = mock(Listener.class); ExoPlayer player = new TestExoPlayerBuilder(context).build(); player.addListener(listener); AtomicReference sourceReference = new AtomicReference<>(); sourceReference.set( new ServerSideAdInsertionMediaSource( - new FakeMediaSource(adTimeline), + new FakeMediaSource(fakeContentTimeline), contentTimeline -> { - sourceReference - .get() - .setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0)); + sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline); return true; })); player.setMediaSource(sourceReference.get()); @@ -5141,25 +5157,40 @@ public final class ExoPlayerTest { ArgumentCaptor newPositionArgumentCaptor = ArgumentCaptor.forClass(PositionInfo.class); ArgumentCaptor reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class); - FakeTimeline adTimeline = + // Create a multi-period timeline without ads. + FakeTimeline fakeContentTimeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ 4, + "windowId", + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ DEFAULT_WINDOW_DURATION_US, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + /* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE), + MediaItem.EMPTY)); + // Create the ad playback state matching to the periods in the content timeline. + ImmutableMap adPlaybackStates = FakeTimeline.createMultiPeriodAdTimeline( - "windowId", - /* numberOfPlayedAds= */ 0, - /* isAdPeriodFlags...= */ false, - true, - false, - false); + "windowId", + /* numberOfPlayedAds= */ 0, + /* isAdPeriodFlags...= */ false, + true, + false, + false) + .getAdPlaybackStates(/* windowIndex= */ 0); Listener listener = mock(Listener.class); ExoPlayer player = new TestExoPlayerBuilder(context).build(); player.addListener(listener); AtomicReference sourceReference = new AtomicReference<>(); sourceReference.set( new ServerSideAdInsertionMediaSource( - new FakeMediaSource(adTimeline), + new FakeMediaSource(fakeContentTimeline), contentTimeline -> { - sourceReference - .get() - .setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0)); + sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline); return true; })); player.setMediaSource(sourceReference.get()); @@ -5203,25 +5234,40 @@ public final class ExoPlayerTest { ArgumentCaptor newPositionArgumentCaptor = ArgumentCaptor.forClass(PositionInfo.class); ArgumentCaptor reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class); - FakeTimeline adTimeline = + // Create a multi-period timeline without ads. + FakeTimeline fakeContentTimeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ 4, + "windowId", + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ DEFAULT_WINDOW_DURATION_US, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + /* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE), + MediaItem.EMPTY)); + // Create the ad playback state matching to the periods in the content timeline. + ImmutableMap adPlaybackStates = FakeTimeline.createMultiPeriodAdTimeline( - "windowId", - /* numberOfPlayedAds= */ 0, - /* isAdPeriodFlags...= */ false, - true, - true, - false); + "windowId", + /* numberOfPlayedAds= */ 0, + /* isAdPeriodFlags...= */ false, + true, + true, + false) + .getAdPlaybackStates(/* windowIndex= */ 0); Listener listener = mock(Listener.class); ExoPlayer player = new TestExoPlayerBuilder(context).build(); player.addListener(listener); AtomicReference sourceReference = new AtomicReference<>(); sourceReference.set( new ServerSideAdInsertionMediaSource( - new FakeMediaSource(adTimeline), + new FakeMediaSource(fakeContentTimeline), contentTimeline -> { - sourceReference - .get() - .setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0)); + sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline); return true; })); player.setMediaSource(sourceReference.get()); @@ -5270,25 +5316,40 @@ public final class ExoPlayerTest { ArgumentCaptor newPositionArgumentCaptor = ArgumentCaptor.forClass(PositionInfo.class); ArgumentCaptor reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class); - FakeTimeline adTimeline = + // Create a multi-period timeline without ads. + FakeTimeline fakeContentTimeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ 4, + "windowId", + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ DEFAULT_WINDOW_DURATION_US, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + /* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE), + MediaItem.EMPTY)); + // Create the ad playback state matching to the periods in the content timeline. + ImmutableMap adPlaybackStates = FakeTimeline.createMultiPeriodAdTimeline( - "windowId", - /* numberOfPlayedAds= */ 0, - /* isAdPeriodFlags...= */ false, - true, - true, - false); + "windowId", + /* numberOfPlayedAds= */ 0, + /* isAdPeriodFlags...= */ false, + true, + true, + false) + .getAdPlaybackStates(/* windowIndex= */ 0); Listener listener = mock(Listener.class); ExoPlayer player = new TestExoPlayerBuilder(context).build(); player.addListener(listener); AtomicReference sourceReference = new AtomicReference<>(); sourceReference.set( new ServerSideAdInsertionMediaSource( - new FakeMediaSource(adTimeline), + new FakeMediaSource(fakeContentTimeline), contentTimeline -> { - sourceReference - .get() - .setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0)); + sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline); return true; })); player.setMediaSource(sourceReference.get()); @@ -5351,25 +5412,40 @@ public final class ExoPlayerTest { ArgumentCaptor newPositionArgumentCaptor = ArgumentCaptor.forClass(PositionInfo.class); ArgumentCaptor reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class); - FakeTimeline adTimeline = + // Create a multi-period timeline without ads. + FakeTimeline fakeContentTimeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ 4, + "windowId", + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ DEFAULT_WINDOW_DURATION_US, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + /* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE), + MediaItem.EMPTY)); + // Create the ad playback state matching to the periods in the content timeline. + ImmutableMap adPlaybackStates = FakeTimeline.createMultiPeriodAdTimeline( - "windowId", - /* numberOfPlayedAds= */ 2, - /* isAdPeriodFlags...= */ false, - true, - true, - false); + "windowId", + /* numberOfPlayedAds= */ 2, + /* isAdPeriodFlags...= */ false, + true, + true, + false) + .getAdPlaybackStates(/* windowIndex= */ 0); Listener listener = mock(Listener.class); ExoPlayer player = new TestExoPlayerBuilder(context).build(); player.addListener(listener); AtomicReference sourceReference = new AtomicReference<>(); sourceReference.set( new ServerSideAdInsertionMediaSource( - new FakeMediaSource(adTimeline), + new FakeMediaSource(fakeContentTimeline), contentTimeline -> { - sourceReference - .get() - .setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0)); + sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline); return true; })); player.setMediaSource(sourceReference.get()); @@ -5406,29 +5482,44 @@ public final class ExoPlayerTest { ArgumentCaptor newPositionArgumentCaptor = ArgumentCaptor.forClass(PositionInfo.class); ArgumentCaptor reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class); - FakeTimeline adTimeline = + // Create a multi-period timeline without ads. + FakeTimeline fakeContentTimeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ 8, + "windowId", + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ DEFAULT_WINDOW_DURATION_US, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + /* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE), + MediaItem.EMPTY)); + // Create the ad playback state matching to the periods in the content timeline. + ImmutableMap adPlaybackStates = FakeTimeline.createMultiPeriodAdTimeline( - "windowId", - /* numberOfPlayedAds= */ Integer.MAX_VALUE, - /* isAdPeriodFlags...= */ true, - false, - true, - true, - false, - true, - true, - true); + "windowId", + /* numberOfPlayedAds= */ Integer.MAX_VALUE, + /* isAdPeriodFlags...= */ true, + false, + true, + true, + false, + true, + true, + true) + .getAdPlaybackStates(/* windowIndex= */ 0); Listener listener = mock(Listener.class); ExoPlayer player = new TestExoPlayerBuilder(context).build(); player.addListener(listener); AtomicReference sourceReference = new AtomicReference<>(); sourceReference.set( new ServerSideAdInsertionMediaSource( - new FakeMediaSource(adTimeline, ExoPlayerTestRunner.AUDIO_FORMAT), + new FakeMediaSource(fakeContentTimeline, ExoPlayerTestRunner.AUDIO_FORMAT), contentTimeline -> { - sourceReference - .get() - .setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0)); + sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline); return true; })); player.setMediaSource(sourceReference.get()); @@ -5508,9 +5599,10 @@ public final class ExoPlayerTest { adPlaybackState.withPlayedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup+ */ 0); adPlaybackState = adPlaybackState.withPlayedAd(/* adGroupIndex= */ 3, /* adIndexInAdGroup+ */ 0); - FakeTimeline adTimeline = + // Create a multi-period timeline without ads. + FakeTimeline fakeContentTimeline = new FakeTimeline( - new TimelineWindowDefinition( + new FakeTimeline.TimelineWindowDefinition( /* periodCount= */ 1, "windowId", /* isSeekable= */ true, @@ -5520,20 +5612,19 @@ public final class ExoPlayerTest { /* durationUs= */ DEFAULT_WINDOW_DURATION_US, /* defaultPositionUs= */ 0, /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, - /* adPlaybackStates= */ ImmutableList.of(adPlaybackState), + /* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE), MediaItem.EMPTY)); - + ImmutableMap adPlaybackStates = + ImmutableMap.of(/* period.uid */ new Pair<>("windowId", 0), adPlaybackState); Listener listener = mock(Listener.class); ExoPlayer player = new TestExoPlayerBuilder(context).build(); player.addListener(listener); AtomicReference sourceReference = new AtomicReference<>(); sourceReference.set( new ServerSideAdInsertionMediaSource( - new FakeMediaSource(adTimeline, ExoPlayerTestRunner.AUDIO_FORMAT), + new FakeMediaSource(fakeContentTimeline, ExoPlayerTestRunner.AUDIO_FORMAT), contentTimeline -> { - sourceReference - .get() - .setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0)); + sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline); return true; })); player.setMediaSource(sourceReference.get()); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java index d9e9720116..d6c014a3d2 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java @@ -17,6 +17,7 @@ package androidx.media3.exoplayer; import static androidx.media3.test.utils.ExoPlayerTestRunner.AUDIO_FORMAT; import static androidx.media3.test.utils.ExoPlayerTestRunner.VIDEO_FORMAT; +import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US; import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; @@ -57,6 +58,7 @@ import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; @@ -1568,13 +1570,28 @@ public final class MediaPeriodQueueTest { private static Timeline createMultiPeriodServerSideInsertedTimeline( Object windowId, int numberOfPlayedAds, boolean... isAdPeriodFlags) throws InterruptedException { - FakeTimeline timeline = - FakeTimeline.createMultiPeriodAdTimeline(windowId, numberOfPlayedAds, isAdPeriodFlags); + FakeTimeline fakeContentTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + isAdPeriodFlags.length, + windowId, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ DEFAULT_WINDOW_DURATION_US, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + /* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE), + MediaItem.EMPTY)); + ImmutableMap adPlaybackStates = + FakeTimeline.createMultiPeriodAdTimeline(windowId, numberOfPlayedAds, isAdPeriodFlags) + .getAdPlaybackStates(/* windowIndex= */ 0); ServerSideAdInsertionMediaSource serverSideAdInsertionMediaSource = new ServerSideAdInsertionMediaSource( - new FakeMediaSource(timeline, VIDEO_FORMAT, AUDIO_FORMAT), contentTimeline -> false); - serverSideAdInsertionMediaSource.setAdPlaybackStates( - timeline.getAdPlaybackStates(/* windowIndex= */ 0)); + new FakeMediaSource(fakeContentTimeline, VIDEO_FORMAT, AUDIO_FORMAT), + contentTimeline -> false); + serverSideAdInsertionMediaSource.setAdPlaybackStates(adPlaybackStates, fakeContentTimeline); AtomicReference serverSideAdInsertionTimelineRef = new AtomicReference<>(); CountDownLatch countDownLatch = new CountDownLatch(/* count= */ 1); serverSideAdInsertionMediaSource.prepareSource( 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 da101f451c..0c77095421 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 @@ -55,6 +55,7 @@ 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.ArrayList; import java.util.concurrent.atomic.AtomicReference; import org.junit.Assert; import org.junit.Rule; @@ -110,7 +111,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.setAdPlaybackStates( + ImmutableMap.of(new Pair<>(0, 0), adPlaybackState), wrappedTimeline); mediaSource.prepareSource( (source, timeline) -> timelineReference.set(timeline), @@ -191,7 +193,8 @@ public final class ServerSideAdInsertionMediaSourceTest { wrappedTimeline.getPeriod( /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) .uid, - adPlaybackState)); + adPlaybackState), + wrappedTimeline); mediaSource.prepareSource( (source, timeline) -> timelineReference.set(timeline), @@ -231,12 +234,13 @@ public final class ServerSideAdInsertionMediaSourceTest { @Test public void timeline_missingAdPlaybackStateByPeriodUid_isAssertedAndThrows() { + FakeMediaSource contentSource = new FakeMediaSource(); ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource( - new FakeMediaSource(), /* adPlaybackStateUpdater= */ null); + new ServerSideAdInsertionMediaSource(contentSource, /* 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()))); + ImmutableMap.of(new Object(), new AdPlaybackState(/* adsId= */ new Object())), + contentSource.getInitialTimeline()); Assert.assertThrows( IllegalStateException.class, @@ -292,7 +296,8 @@ public final class ServerSideAdInsertionMediaSourceTest { .uid); mediaSourceRef .get() - .setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState)); + .setAdPlaybackStates( + ImmutableMap.of(periodUid, firstAdPlaybackState), contentTimeline); return true; })); @@ -338,6 +343,7 @@ public final class ServerSideAdInsertionMediaSourceTest { /* contentResumeOffsetUs= */ 0, /* adDurationsUs...= */ 100_000); AtomicReference mediaSourceRef = new AtomicReference<>(); + ArrayList contentTimelines = new ArrayList<>(); mediaSourceRef.set( new ServerSideAdInsertionMediaSource( new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), @@ -347,9 +353,11 @@ public final class ServerSideAdInsertionMediaSourceTest { contentTimeline.getPeriod( /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) .uid)); + contentTimelines.add(contentTimeline); mediaSourceRef .get() - .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState)); + .setAdPlaybackStates( + ImmutableMap.of(periodUid.get(), firstAdPlaybackState), contentTimeline); return true; })); AnalyticsListener listener = mock(AnalyticsListener.class); @@ -367,7 +375,8 @@ public final class ServerSideAdInsertionMediaSourceTest { /* adDurationsUs...= */ 500_000); mediaSourceRef .get() - .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); + .setAdPlaybackStates( + ImmutableMap.of(periodUid.get(), secondAdPlaybackState), contentTimelines.get(1)); runUntilPendingCommandsAreFullyHandled(player); player.play(); @@ -376,6 +385,7 @@ public final class ServerSideAdInsertionMediaSourceTest { // Assert all samples have been played. DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP); + assertThat(contentTimelines).hasSize(2); // Assert playback has been reported with ads: [content][ad0][content][ad1][content] // 5*2(audio+video) format changes, 4 discontinuities between parts. verify(listener, times(4)) @@ -409,10 +419,12 @@ public final class ServerSideAdInsertionMediaSourceTest { /* contentResumeOffsetUs= */ 0, /* adDurationsUs...= */ 500_000); AtomicReference mediaSourceRef = new AtomicReference<>(); + ArrayList contentTimelines = new ArrayList<>(); mediaSourceRef.set( new ServerSideAdInsertionMediaSource( new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), /* adPlaybackStateUpdater= */ contentTimeline -> { + contentTimelines.add(contentTimeline); if (periodUid.get() == null) { periodUid.set( checkNotNull( @@ -421,7 +433,8 @@ public final class ServerSideAdInsertionMediaSourceTest { .uid)); mediaSourceRef .get() - .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState)); + .setAdPlaybackStates( + ImmutableMap.of(periodUid.get(), firstAdPlaybackState), contentTimeline); } return true; })); @@ -440,7 +453,8 @@ public final class ServerSideAdInsertionMediaSourceTest { /* adGroupIndex= */ 0, /* adDurationsUs...= */ 50_000, 250_000, 200_000); mediaSourceRef .get() - .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); + .setAdPlaybackStates( + ImmutableMap.of(periodUid.get(), secondAdPlaybackState), contentTimelines.get(1)); runUntilPendingCommandsAreFullyHandled(player); player.play(); @@ -449,6 +463,7 @@ public final class ServerSideAdInsertionMediaSourceTest { // Assert all samples have been played. DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP); + assertThat(contentTimelines).hasSize(2); // Assert playback has been reported with ads: [ad0][ad1][ad2][content] // 4*2(audio+video) format changes, 3 discontinuities between parts. verify(listener, times(3)) @@ -501,7 +516,8 @@ public final class ServerSideAdInsertionMediaSourceTest { .uid); mediaSourceRef .get() - .setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState)); + .setAdPlaybackStates( + ImmutableMap.of(periodUid, firstAdPlaybackState), contentTimeline); return true; })); diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index 1134a97266..870f3e4e08 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -702,7 +702,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou splitAdPlaybackStates = ImmutableMap.of(periodUid, adPlaybackState); } streamPlayer.setAdPlaybackStates(adsId, splitAdPlaybackStates, contentTimeline); - checkNotNull(serverSideAdInsertionMediaSource).setAdPlaybackStates(splitAdPlaybackStates); + checkNotNull(serverSideAdInsertionMediaSource) + .setAdPlaybackStates(splitAdPlaybackStates, contentTimeline); if (!isLiveStream) { adsLoader.setAdPlaybackState(adsId, adPlaybackState); }