diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java index 107f28a625..67cecee673 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.ima; import static com.google.android.exoplayer2.ext.ima.ImaUtil.BITRATE_UNSET; import static com.google.android.exoplayer2.ext.ima.ImaUtil.TIMEOUT_UNSET; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupTimesUsForCuePoints; import static com.google.android.exoplayer2.ext.ima.ImaUtil.getImaLooper; -import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; import static java.lang.Math.max; @@ -280,11 +280,11 @@ import java.util.Map; } } - /** Starts using the ads loader for playback. */ - public void start(Player player, AdViewProvider adViewProvider, EventListener eventListener) { - this.player = player; - player.addListener(this); - boolean playWhenReady = player.getPlayWhenReady(); + /** + * Starts passing events from this instance (including any pending ad playback state) and + * registers obstructions. + */ + public void start(AdViewProvider adViewProvider, EventListener eventListener) { this.eventListener = eventListener; lastVolumePercent = 0; lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; @@ -293,13 +293,9 @@ import java.util.Map; if (!AdPlaybackState.NONE.equals(adPlaybackState)) { // Pass the ad playback state to the player, and resume ads if necessary. eventListener.onAdPlaybackState(adPlaybackState); - if (adsManager != null && imaPausedContent && playWhenReady) { - adsManager.resume(); - } } else if (adsManager != null) { adPlaybackState = - new AdPlaybackState( - adsId, ImaUtil.getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints())); + new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints())); updateAdPlaybackState(); } for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) { @@ -311,14 +307,36 @@ import java.util.Map; } } - /** Stops using the ads loader for playback. */ - public void stop() { - @Nullable Player player = this.player; - if (player == null) { - return; + /** + * Populates the ad playback state with loaded cue points, if available. Any preroll will be + * paused immediately while waiting for this instance to be {@link #activate(Player) activated}. + */ + public void maybePreloadAds(long contentPositionMs, long contentDurationMs) { + maybeInitializeAdsManager(contentPositionMs, contentDurationMs); + } + + /** Activates playback. */ + public void activate(Player player) { + this.player = player; + player.addListener(this); + + boolean playWhenReady = player.getPlayWhenReady(); + onTimelineChanged(player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + if (!AdPlaybackState.NONE.equals(adPlaybackState) + && adsManager != null + && imaPausedContent + && playWhenReady) { + adsManager.resume(); } - if (adsManager != null && imaPausedContent) { - adsManager.pause(); + } + + /** Deactivates playback. */ + public void deactivate() { + Player player = checkNotNull(this.player); + if (!AdPlaybackState.NONE.equals(adPlaybackState) && imaPausedContent) { + if (adsManager != null) { + adsManager.pause(); + } adPlaybackState = adPlaybackState.withAdResumePositionUs( playingAd ? C.msToUs(player.getCurrentPosition()) : 0); @@ -326,10 +344,15 @@ import java.util.Map; lastVolumePercent = getPlayerVolumePercent(); lastAdProgress = getAdVideoProgressUpdate(); lastContentProgress = getContentVideoProgressUpdate(); - adDisplayContainer.unregisterAllFriendlyObstructions(); + player.removeListener(this); this.player = null; + } + + /** Stops passing of events from this instance and unregisters obstructions. */ + public void stop() { eventListener = null; + adDisplayContainer.unregisterAllFriendlyObstructions(); } /** Releases all resources used by the ad tag loader. */ @@ -392,7 +415,6 @@ import java.util.Map; // The player is being reset or contains no media. return; } - checkArgument(timeline.getPeriodCount() == 1); this.timeline = timeline; Player player = checkNotNull(this.player); long contentDurationUs = timeline.getPeriod(player.getCurrentPeriodIndex(), period).durationUs; @@ -592,14 +614,13 @@ import java.util.Map; } private VideoProgressUpdate getContentVideoProgressUpdate() { - if (player == null) { - return lastContentProgress; - } boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; long contentPositionMs; if (pendingContentPositionMs != C.TIME_UNSET) { sentPendingContentPositionMs = true; contentPositionMs = pendingContentPositionMs; + } else if (player == null) { + return lastContentProgress; } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; @@ -923,7 +944,8 @@ import java.util.Map; adCallbacks.get(i).onResume(adMediaInfo); } } - if (!checkNotNull(player).getPlayWhenReady()) { + if (player == null || !player.getPlayWhenReady()) { + // Either this loader hasn't been activated yet, or the player is paused now. checkNotNull(adsManager).pause(); } } @@ -941,7 +963,14 @@ import java.util.Map; // to a different position, so drop the event. See also [Internal: b/159111848]. return; } - checkState(adMediaInfo.equals(imaAdMediaInfo)); + if (configuration.debugModeEnabled && !adMediaInfo.equals(imaAdMediaInfo)) { + Log.w( + TAG, + "Unexpected pauseAd for " + + getAdMediaInfoString(adMediaInfo) + + ", expected " + + getAdMediaInfoString(imaAdMediaInfo)); + } imaAdState = IMA_AD_STATE_PAUSED; for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onPause(adMediaInfo); @@ -1157,9 +1186,13 @@ import java.util.Map; throw new IllegalStateException("Failed to find cue point"); } - private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { + private String getAdMediaInfoString(@Nullable AdMediaInfo adMediaInfo) { @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; + return "AdMediaInfo[" + + (adMediaInfo == null ? "null" : adMediaInfo.getUrl()) + + ", " + + adInfo + + "]"; } private static long getContentPeriodPositionMs( @@ -1226,16 +1259,12 @@ import java.util.Map; if (configuration.applicationAdEventListener != null) { adsManager.addAdEventListener(configuration.applicationAdEventListener); } - if (player != null) { - // If a player is attached already, start playback immediately. - try { - adPlaybackState = - new AdPlaybackState( - adsId, ImaUtil.getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdsManagerLoaded", e); - } + try { + adPlaybackState = + new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints())); + updateAdPlaybackState(); + } catch (RuntimeException e) { + maybeNotifyInternalError("onAdsManagerLoaded", e); } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 8bb6057a56..36f1a2ea5f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -44,6 +44,7 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -57,6 +58,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Set; @@ -371,12 +373,16 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { private final ImaUtil.Configuration configuration; private final Context context; private final ImaUtil.ImaFactory imaFactory; + private final HashMap adTagLoaderByAdsId; + private final HashMap adTagLoaderByAdsMediaSource; + private final Timeline.Period period; + private final Timeline.Window window; private boolean wasSetPlayerCalled; @Nullable private Player nextPlayer; - @Nullable private AdTagLoader adTagLoader; private List supportedMimeTypes; @Nullable private Player player; + @Nullable private AdTagLoader currentAdTagLoader; private ImaAdsLoader( Context context, ImaUtil.Configuration configuration, ImaUtil.ImaFactory imaFactory) { @@ -384,6 +390,10 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { this.configuration = configuration; this.imaFactory = imaFactory; supportedMimeTypes = ImmutableList.of(); + adTagLoaderByAdsId = new HashMap<>(); + adTagLoaderByAdsMediaSource = new HashMap<>(); + period = new Timeline.Period(); + window = new Timeline.Window(); } /** @@ -394,7 +404,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { @SuppressWarnings("nullness:nullness.on.outer") @Nullable public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { - return adTagLoader != null ? adTagLoader.getAdsLoader() : null; + return currentAdTagLoader != null ? currentAdTagLoader.getAdsLoader() : null; } /** @@ -410,7 +420,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { */ @Nullable public AdDisplayContainer getAdDisplayContainer() { - return adTagLoader != null ? adTagLoader.getAdDisplayContainer() : null; + return currentAdTagLoader != null ? currentAdTagLoader.getAdDisplayContainer() : null; } /** @@ -427,8 +437,8 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { * null} if playing audio-only ads. */ public void requestAds(DataSpec adTagDataSpec, Object adsId, @Nullable ViewGroup adViewGroup) { - if (adTagLoader == null) { - adTagLoader = + if (!adTagLoaderByAdsId.containsKey(adsId)) { + AdTagLoader adTagLoader = new AdTagLoader( context, configuration, @@ -437,6 +447,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { adTagDataSpec, adsId, adViewGroup); + adTagLoaderByAdsId.put(adsId, adTagLoader); } } @@ -448,8 +459,8 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { * IMA SDK provides the UI to skip ads in the ad view group passed via {@link AdViewProvider}. */ public void skipAd() { - if (adTagLoader != null) { - adTagLoader.skipAd(); + if (currentAdTagLoader != null) { + currentAdTagLoader.skipAd(); } } @@ -494,37 +505,67 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { EventListener eventListener) { checkState( wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player."); - player = nextPlayer; - @Nullable Player player = this.player; - if (player == null) { - return; + if (adTagLoaderByAdsMediaSource.isEmpty()) { + player = nextPlayer; + @Nullable Player player = this.player; + if (player == null) { + return; + } + player.addListener(this); } + + @Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId); if (adTagLoader == null) { requestAds(adTagDataSpec, adsId, adViewProvider.getAdViewGroup()); + adTagLoader = adTagLoaderByAdsId.get(adsId); } - checkNotNull(adTagLoader).start(player, adViewProvider, eventListener); + adTagLoaderByAdsMediaSource.put(adsMediaSource, checkNotNull(adTagLoader)); + checkNotNull(adTagLoader).start(adViewProvider, eventListener); + maybeUpdateCurrentAdTagLoader(); } @Override public void stop(AdsMediaSource adsMediaSource) { - if (player != null && adTagLoader != null) { - adTagLoader.stop(); + @Nullable AdTagLoader removedAdTagLoader = adTagLoaderByAdsMediaSource.remove(adsMediaSource); + maybeUpdateCurrentAdTagLoader(); + if (removedAdTagLoader != null) { + removedAdTagLoader.stop(); + } + + if (player != null && adTagLoaderByAdsMediaSource.isEmpty()) { + player.removeListener(this); + player = null; } } @Override public void release() { - if (adTagLoader != null) { + if (player != null) { + player.removeListener(this); + player = null; + maybeUpdateCurrentAdTagLoader(); + } + nextPlayer = null; + + for (AdTagLoader adTagLoader : adTagLoaderByAdsMediaSource.values()) { adTagLoader.release(); } + adTagLoaderByAdsMediaSource.clear(); + + for (AdTagLoader adTagLoader : adTagLoaderByAdsId.values()) { + adTagLoader.release(); + } + adTagLoaderByAdsId.clear(); } @Override public void handlePrepareComplete( AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup) { - if (adTagLoader != null) { - adTagLoader.handlePrepareComplete(adGroupIndex, adIndexInAdGroup); + if (player == null) { + return; } + checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource)) + .handlePrepareComplete(adGroupIndex, adIndexInAdGroup); } @Override @@ -533,9 +574,112 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { int adGroupIndex, int adIndexInAdGroup, IOException exception) { - if (adTagLoader != null) { - adTagLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception); + if (player == null) { + return; } + checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource)) + .handlePrepareError(adGroupIndex, adIndexInAdGroup, exception); + } + + // Player.EventListener implementation. + + @Override + public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { + if (timeline.isEmpty()) { + // The player is being reset or contains no media. + return; + } + maybeUpdateCurrentAdTagLoader(); + maybePreloadNextPeriodAds(); + } + + @Override + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + maybeUpdateCurrentAdTagLoader(); + maybePreloadNextPeriodAds(); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + maybePreloadNextPeriodAds(); + } + + @Override + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + maybePreloadNextPeriodAds(); + } + + // Internal methods. + + private void maybeUpdateCurrentAdTagLoader() { + @Nullable AdTagLoader oldAdTagLoader = currentAdTagLoader; + @Nullable AdTagLoader newAdTagLoader = getCurrentAdTagLoader(); + if (!Util.areEqual(oldAdTagLoader, newAdTagLoader)) { + if (oldAdTagLoader != null) { + oldAdTagLoader.deactivate(); + } + currentAdTagLoader = newAdTagLoader; + if (newAdTagLoader != null) { + newAdTagLoader.activate(checkNotNull(player)); + } + } + } + + @Nullable + private AdTagLoader getCurrentAdTagLoader() { + @Nullable Player player = this.player; + if (player == null) { + return null; + } + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return null; + } + int periodIndex = player.getCurrentPeriodIndex(); + @Nullable Object adsId = timeline.getPeriod(periodIndex, period).getAdsId(); + if (adsId == null) { + return null; + } + @Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId); + if (adTagLoader == null || !adTagLoaderByAdsMediaSource.containsValue(adTagLoader)) { + return null; + } + return adTagLoader; + } + + private void maybePreloadNextPeriodAds() { + @Nullable Player player = this.player; + if (player == null) { + return; + } + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int nextPeriodIndex = + timeline.getNextPeriodIndex( + player.getCurrentPeriodIndex(), + period, + window, + player.getRepeatMode(), + player.getShuffleModeEnabled()); + if (nextPeriodIndex == C.INDEX_UNSET) { + return; + } + timeline.getPeriod(nextPeriodIndex, period); + @Nullable Object nextAdsId = period.getAdsId(); + if (nextAdsId == null) { + return; + } + @Nullable AdTagLoader nextAdTagLoader = adTagLoaderByAdsId.get(nextAdsId); + if (nextAdTagLoader == null || nextAdTagLoader == currentAdTagLoader) { + return; + } + long periodPositionUs = + timeline.getPeriodPosition( + window, period, period.windowIndex, /* windowPositionUs= */ C.TIME_UNSET) + .second; + nextAdTagLoader.maybePreloadAds(C.usToMs(periodPositionUs), C.usToMs(period.durationUs)); } /** diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java index 4c98233acb..a5802fad0d 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -21,25 +21,30 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.StubExoPlayer; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import java.util.ArrayList; +import com.google.android.exoplayer2.util.ListenerSet; /** A fake player for testing content/ad playback. */ /* package */ final class FakePlayer extends StubExoPlayer { - private final ArrayList listeners; + private final ListenerSet listeners; private final Timeline.Period period; - private final Timeline timeline; + private Timeline timeline; @Player.State private int state; private boolean playWhenReady; - private long position; - private long contentPosition; + private int periodIndex; + private long positionMs; + private long contentPositionMs; private boolean isPlayingAd; private int adGroupIndex; private int adIndexInAdGroup; public FakePlayer() { - listeners = new ArrayList<>(); + listeners = + new ListenerSet<>( + Looper.getMainLooper(), + Player.Events::new, + (listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags)); period = new Timeline.Period(); state = Player.STATE_IDLE; playWhenReady = true; @@ -48,26 +53,27 @@ import java.util.ArrayList; /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */ public void updateTimeline(Timeline timeline, @TimelineChangeReason int reason) { - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(timeline, reason); - } + this.timeline = timeline; + listeners.sendEvent( + Player.EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged(timeline, reason)); } /** * Sets the state of this player as if it were playing content at the given {@code position}. If * an ad is currently playing, this will trigger a position discontinuity. */ - public void setPlayingContentPosition(long position) { + public void setPlayingContentPosition(int periodIndex, long positionMs) { boolean notify = isPlayingAd; isPlayingAd = false; adGroupIndex = C.INDEX_UNSET; adIndexInAdGroup = C.INDEX_UNSET; - this.position = position; - contentPosition = position; + this.periodIndex = periodIndex; + this.positionMs = positionMs; + contentPositionMs = positionMs; if (notify) { - for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION); - } + listeners.sendEvent( + Player.EVENT_POSITION_DISCONTINUITY, + listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION)); } } @@ -77,17 +83,22 @@ import java.util.ArrayList; * position discontinuity. */ public void setPlayingAdPosition( - int adGroupIndex, int adIndexInAdGroup, long position, long contentPosition) { + int periodIndex, + int adGroupIndex, + int adIndexInAdGroup, + long positionMs, + long contentPositionMs) { boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup; isPlayingAd = true; + this.periodIndex = periodIndex; this.adGroupIndex = adGroupIndex; this.adIndexInAdGroup = adIndexInAdGroup; - this.position = position; - this.contentPosition = contentPosition; + this.positionMs = positionMs; + this.contentPositionMs = contentPositionMs; if (notify) { - for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION); - } + listeners.sendEvent( + EVENT_POSITION_DISCONTINUITY, + listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION)); } } @@ -99,16 +110,18 @@ import java.util.ArrayList; this.state = state; this.playWhenReady = playWhenReady; if (playbackStateChanged || playWhenReadyChanged) { - for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, state); - if (playbackStateChanged) { - listener.onPlaybackStateChanged(state); - } - if (playWhenReadyChanged) { - listener.onPlayWhenReadyChanged( - playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); - } - } + listeners.sendEvent( + Player.EVENT_PLAYBACK_STATE_CHANGED, + listener -> { + listener.onPlayerStateChanged(playWhenReady, state); + if (playbackStateChanged) { + listener.onPlaybackStateChanged(state); + } + if (playWhenReadyChanged) { + listener.onPlayWhenReadyChanged( + playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + } + }); } } @@ -145,6 +158,17 @@ import java.util.ArrayList; return playWhenReady; } + @Override + @RepeatMode + public int getRepeatMode() { + return REPEAT_MODE_OFF; + } + + @Override + public boolean getShuffleModeEnabled() { + return false; + } + @Override public int getRendererCount() { return 0; @@ -162,7 +186,7 @@ import java.util.ArrayList; @Override public int getCurrentPeriodIndex() { - return 0; + return periodIndex; } @Override @@ -186,7 +210,7 @@ import java.util.ArrayList; @Override public long getCurrentPosition() { - return position; + return positionMs; } @Override @@ -206,6 +230,6 @@ import java.util.ArrayList; @Override public long getContentPosition() { - return contentPosition; + return contentPositionMs; } } diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index c8110ea4e4..a96cc955d6 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -53,18 +53,15 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.ext.ima.ImaUtil.ImaFactory; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; -import com.google.android.exoplayer2.source.MaskingMediaSource.PlaceholderTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; -import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; @@ -133,12 +130,13 @@ public final class ImaAdsLoaderTest { private ContentProgressProvider contentProgressProvider; private VideoAdPlayer videoAdPlayer; private TestAdsLoaderListener adsLoaderListener; - private FakePlayer fakeExoPlayer; + private FakePlayer fakePlayer; private ImaAdsLoader imaAdsLoader; @Before public void setUp() { setupMocks(); + fakePlayer = new FakePlayer(); adViewGroup = new FrameLayout(getApplicationContext()); View adOverlayView = new View(getApplicationContext()); adViewProvider = @@ -166,19 +164,32 @@ public final class ImaAdsLoaderTest { return ImmutableList.of(); } }; + imaAdsLoader = + new ImaAdsLoader.Builder(getApplicationContext()) + .setImaFactory(mockImaFactory) + .setImaSdkSettings(mockImaSdkSettings) + .build(); + imaAdsLoader.setPlayer(fakePlayer); + adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + TEST_ADS_ID, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider); + adsLoaderListener = new TestAdsLoaderListener(getInitialTimelineWindowDefinition(TEST_ADS_ID)); + when(mockAdsManager.getAdCuePoints()).thenReturn(PREROLL_CUE_POINTS_SECONDS); } @After public void teardown() { - if (imaAdsLoader != null) { - imaAdsLoader.release(); - } + imaAdsLoader.release(); } @Test - public void builder_overridesPlayerType() { + public void loader_overridesCustomPlayerType() { when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type"); - setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -187,7 +198,6 @@ public final class ImaAdsLoaderTest { @Test public void start_setsAdUiViewGroup() { - setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -198,7 +208,6 @@ public final class ImaAdsLoaderTest { @Test public void startForAudioOnlyAds_createsAudioOnlyAdDisplayContainer() { - setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, audioAdsAdViewProvider, adsLoaderListener); @@ -210,8 +219,11 @@ public final class ImaAdsLoaderTest { @Test public void start_withPlaceholderContent_initializedAdsLoader() { - Timeline placeholderTimeline = new PlaceholderTimeline(MediaItem.fromUri(Uri.EMPTY)); - setupPlayback(placeholderTimeline, PREROLL_CUE_POINTS_SECONDS); + adsLoaderListener = + new TestAdsLoaderListener( + getInitialTimelineWindowDefinition(TEST_ADS_ID, /* isPlaceholder= */ true)); + + when(mockAdsManager.getAdCuePoints()).thenReturn(PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -221,11 +233,10 @@ public final class ImaAdsLoaderTest { @Test public void start_updatesAdPlaybackState() { - setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 0) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); @@ -233,7 +244,6 @@ public final class ImaAdsLoaderTest { @Test public void startAfterRelease() { - setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.release(); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -241,14 +251,13 @@ public final class ImaAdsLoaderTest { @Test public void startAndCallbacksAfterRelease() { - setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); // Request ads in order to get a reference to the ad event listener. imaAdsLoader.requestAds(TEST_DATA_SPEC, TEST_ADS_ID, adViewGroup); imaAdsLoader.release(); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); - fakeExoPlayer.setPlayingContentPosition(/* position= */ 0); - fakeExoPlayer.setState(Player.STATE_READY, true); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, /* positionMs= */ 0); + fakePlayer.setState(Player.STATE_READY, true); // If callbacks are invoked there is no crash. // Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown @@ -271,8 +280,6 @@ public final class ImaAdsLoaderTest { @Test public void playback_withPrerollAd_marksAdAsPlayed() { - setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); - // Load the preroll ad. imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -282,24 +289,25 @@ public final class ImaAdsLoaderTest { // Play the preroll ad. videoAdPlayer.playAd(TEST_AD_MEDIA_INFO); - fakeExoPlayer.setPlayingAdPosition( + fakePlayer.setPlayingAdPosition( + /* periodIndex= */ 0, /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* position= */ 0, /* contentPosition= */ 0); - fakeExoPlayer.setState(Player.STATE_READY, true); + fakePlayer.setState(Player.STATE_READY, true); adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd)); // Play the content. - fakeExoPlayer.setPlayingContentPosition(0); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, /* positionMs= */ 0); videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); // Verify that the preroll ad has been marked as played. - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 0) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -316,14 +324,14 @@ public final class ImaAdsLoaderTest { when(mockMidrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR); when(mockMidrollFetchErrorAdEvent.getAdData()) .thenReturn(ImmutableMap.of("adBreakTime", "20.5")); - setupPlayback(CONTENT_TIMELINE, ImmutableList.of(20.5f)); + when(mockAdsManager.getAdCuePoints()).thenReturn(ImmutableList.of(20.5f)); // Simulate loading an empty midroll ad. imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); adEventListener.onAdEvent(mockMidrollFetchErrorAdEvent); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 20_500_000) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -338,7 +346,7 @@ public final class ImaAdsLoaderTest { when(mockMidrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR); when(mockMidrollFetchErrorAdEvent.getAdData()) .thenReturn(ImmutableMap.of("adBreakTime", "5.5")); - setupPlayback(CONTENT_TIMELINE, ImmutableList.of(5.5f)); + when(mockAdsManager.getAdCuePoints()).thenReturn(ImmutableList.of(5.5f)); // Simulate loading an empty midroll ad and advancing the player position. imaAdsLoader.start( @@ -349,7 +357,7 @@ public final class ImaAdsLoaderTest { playerPositionUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; long periodDurationUs = CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs; - fakeExoPlayer.setPlayingContentPosition(C.usToMs(playerPositionUs)); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, C.usToMs(playerPositionUs)); // Verify the content progress is updated to reflect the new player position. assertThat(contentProgressProvider.getContentProgress()) @@ -364,14 +372,14 @@ public final class ImaAdsLoaderTest { when(mockPostrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR); when(mockPostrollFetchErrorAdEvent.getAdData()) .thenReturn(ImmutableMap.of("adBreakTime", "-1")); - setupPlayback(CONTENT_TIMELINE, ImmutableList.of(-1f)); + when(mockAdsManager.getAdCuePoints()).thenReturn(ImmutableList.of(-1f)); // Simulate loading an empty postroll ad. imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); adEventListener.onAdEvent(mockPostrollFetchErrorAdEvent); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -388,18 +396,18 @@ public final class ImaAdsLoaderTest { adGroupPositionInWindowUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; ImmutableList cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND); - setupPlayback(CONTENT_TIMELINE, cuePoints); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); // Advance playback to just before the midroll and simulate buffering. imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs)); - fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, C.usToMs(adGroupPositionInWindowUs)); + fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); // Advance before the timeout and simulating polling content progress. ShadowSystemClock.advanceBy(Duration.ofSeconds(1)); contentProgressProvider.getContentProgress(); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); @@ -413,18 +421,18 @@ public final class ImaAdsLoaderTest { adGroupPositionInWindowUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; ImmutableList cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND); - setupPlayback(CONTENT_TIMELINE, cuePoints); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); // Advance playback to just before the midroll and simulate buffering. imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs)); - fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, C.usToMs(adGroupPositionInWindowUs)); + fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); // Advance past the timeout and simulate polling content progress. ShadowSystemClock.advanceBy(Duration.ofSeconds(5)); contentProgressProvider.getContentProgress(); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -440,14 +448,15 @@ public final class ImaAdsLoaderTest { midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; ImmutableList cuePoints = ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback(CONTENT_TIMELINE, cuePoints); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000); + fakePlayer.setPlayingContentPosition( + /* periodIndex= */ 0, C.usToMs(midrollWindowTimeUs) - 1_000); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble()); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); @@ -460,9 +469,9 @@ public final class ImaAdsLoaderTest { midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; ImmutableList cuePoints = ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback(CONTENT_TIMELINE, cuePoints); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs)); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, C.usToMs(midrollWindowTimeUs)); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -472,7 +481,7 @@ public final class ImaAdsLoaderTest { assertThat(playAdsAfterTimeCaptor.getValue()) .isWithin(0.1) .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -486,9 +495,10 @@ public final class ImaAdsLoaderTest { midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; ImmutableList cuePoints = ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback(CONTENT_TIMELINE, cuePoints); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000); + fakePlayer.setPlayingContentPosition( + /* periodIndex= */ 0, C.usToMs(midrollWindowTimeUs) + 1_000); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -498,7 +508,7 @@ public final class ImaAdsLoaderTest { assertThat(playAdsAfterTimeCaptor.getValue()) .isWithin(0.1) .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -519,14 +529,15 @@ public final class ImaAdsLoaderTest { ImmutableList.of( (float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND, (float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback(CONTENT_TIMELINE, cuePoints); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000); + fakePlayer.setPlayingContentPosition( + /* periodIndex= */ 0, C.usToMs(secondMidrollWindowTimeUs) - 1_000); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble()); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); @@ -546,9 +557,9 @@ public final class ImaAdsLoaderTest { ImmutableList.of( (float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND, (float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback(CONTENT_TIMELINE, cuePoints); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs)); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, C.usToMs(secondMidrollWindowTimeUs)); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -558,7 +569,7 @@ public final class ImaAdsLoaderTest { assertThat(playAdsAfterTimeCaptor.getValue()) .isWithin(0.1) .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -567,23 +578,30 @@ public final class ImaAdsLoaderTest { @Test public void resumePlaybackBeforeMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll() { + imaAdsLoader = + new ImaAdsLoader.Builder(getApplicationContext()) + .setPlayAdBeforeStartPosition(false) + .setImaFactory(mockImaFactory) + .setImaSdkSettings(mockImaSdkSettings) + .build(); + imaAdsLoader.setPlayer(fakePlayer); + adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + TEST_ADS_ID, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider); long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND; long midrollPeriodTimeUs = midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; ImmutableList cuePoints = ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback( - CONTENT_TIMELINE, - cuePoints, - new ImaAdsLoader.Builder(getApplicationContext()) - .setPlayAdBeforeStartPosition(false) - .setImaFactory(mockImaFactory) - .setImaSdkSettings(mockImaSdkSettings) - .build(), - TEST_DATA_SPEC, - TEST_ADS_ID); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000); + fakePlayer.setPlayingContentPosition( + /* periodIndex= */ 0, C.usToMs(midrollWindowTimeUs) - 1_000); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -593,7 +611,7 @@ public final class ImaAdsLoaderTest { assertThat(playAdsAfterTimeCaptor.getValue()) .isWithin(0.1d) .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withSkippedAdGroup(/* adGroupIndex= */ 0) @@ -602,23 +620,29 @@ public final class ImaAdsLoaderTest { @Test public void resumePlaybackAtMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll() { + imaAdsLoader = + new ImaAdsLoader.Builder(getApplicationContext()) + .setPlayAdBeforeStartPosition(false) + .setImaFactory(mockImaFactory) + .setImaSdkSettings(mockImaSdkSettings) + .build(); + imaAdsLoader.setPlayer(fakePlayer); + adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + TEST_ADS_ID, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider); long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND; long midrollPeriodTimeUs = midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; ImmutableList cuePoints = ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback( - CONTENT_TIMELINE, - cuePoints, - new ImaAdsLoader.Builder(getApplicationContext()) - .setPlayAdBeforeStartPosition(false) - .setImaFactory(mockImaFactory) - .setImaSdkSettings(mockImaSdkSettings) - .build(), - TEST_DATA_SPEC, - TEST_ADS_ID); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs)); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, C.usToMs(midrollWindowTimeUs)); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -628,7 +652,7 @@ public final class ImaAdsLoaderTest { assertThat(playAdsAfterTimeCaptor.getValue()) .isWithin(0.1d) .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -637,28 +661,35 @@ public final class ImaAdsLoaderTest { @Test public void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMidroll() { + imaAdsLoader = + new ImaAdsLoader.Builder(getApplicationContext()) + .setPlayAdBeforeStartPosition(false) + .setImaFactory(mockImaFactory) + .setImaSdkSettings(mockImaSdkSettings) + .build(); + imaAdsLoader.setPlayer(fakePlayer); + adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + TEST_ADS_ID, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider); long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND; long midrollPeriodTimeUs = midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; ImmutableList cuePoints = ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback( - CONTENT_TIMELINE, - cuePoints, - new ImaAdsLoader.Builder(getApplicationContext()) - .setPlayAdBeforeStartPosition(false) - .setImaFactory(mockImaFactory) - .setImaSdkSettings(mockImaSdkSettings) - .build(), - TEST_DATA_SPEC, - TEST_ADS_ID); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000); + fakePlayer.setPlayingContentPosition( + /* periodIndex= */ 0, C.usToMs(midrollWindowTimeUs) + 1_000); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); verify(mockAdsManager).destroy(); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -669,6 +700,21 @@ public final class ImaAdsLoaderTest { @Test public void resumePlaybackBeforeSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll() { + imaAdsLoader = + new ImaAdsLoader.Builder(getApplicationContext()) + .setPlayAdBeforeStartPosition(false) + .setImaFactory(mockImaFactory) + .setImaSdkSettings(mockImaSdkSettings) + .build(); + imaAdsLoader.setPlayer(fakePlayer); + adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + TEST_ADS_ID, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider); long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND; long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs @@ -681,18 +727,10 @@ public final class ImaAdsLoaderTest { ImmutableList.of( (float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND, (float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback( - CONTENT_TIMELINE, - cuePoints, - new ImaAdsLoader.Builder(getApplicationContext()) - .setPlayAdBeforeStartPosition(false) - .setImaFactory(mockImaFactory) - .setImaSdkSettings(mockImaSdkSettings) - .build(), - TEST_DATA_SPEC, - TEST_ADS_ID); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000); + fakePlayer.setPlayingContentPosition( + /* periodIndex= */ 0, C.usToMs(secondMidrollWindowTimeUs) - 1_000); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -702,7 +740,7 @@ public final class ImaAdsLoaderTest { assertThat(playAdsAfterTimeCaptor.getValue()) .isWithin(0.1d) .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withSkippedAdGroup(/* adGroupIndex= */ 0) @@ -711,6 +749,21 @@ public final class ImaAdsLoaderTest { @Test public void resumePlaybackAtSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll() { + imaAdsLoader = + new ImaAdsLoader.Builder(getApplicationContext()) + .setPlayAdBeforeStartPosition(false) + .setImaFactory(mockImaFactory) + .setImaSdkSettings(mockImaSdkSettings) + .build(); + imaAdsLoader.setPlayer(fakePlayer); + adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + TEST_ADS_ID, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider); long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND; long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs @@ -723,18 +776,9 @@ public final class ImaAdsLoaderTest { ImmutableList.of( (float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND, (float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND); - setupPlayback( - CONTENT_TIMELINE, - cuePoints, - new ImaAdsLoader.Builder(getApplicationContext()) - .setPlayAdBeforeStartPosition(false) - .setImaFactory(mockImaFactory) - .setImaSdkSettings(mockImaSdkSettings) - .build(), - TEST_DATA_SPEC, - TEST_ADS_ID); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs)); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, C.usToMs(secondMidrollWindowTimeUs)); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -744,7 +788,7 @@ public final class ImaAdsLoaderTest { assertThat(playAdsAfterTimeCaptor.getValue()) .isWithin(0.1d) .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -762,16 +806,6 @@ public final class ImaAdsLoaderTest { + " \n" + ""; DataSpec adDataSpec = new DataSpec(Util.getDataUriForString("text/xml", adsResponse)); - - setupPlayback( - CONTENT_TIMELINE, - ImmutableList.of(0f), - new ImaAdsLoader.Builder(getApplicationContext()) - .setImaFactory(mockImaFactory) - .setImaSdkSettings(mockImaSdkSettings) - .build(), - adDataSpec, - TEST_ADS_ID); imaAdsLoader.start(adsMediaSource, adDataSpec, TEST_ADS_ID, adViewProvider, adsLoaderListener); verify(mockAdsRequest).setAdsResponse(adsResponse); @@ -779,15 +813,6 @@ public final class ImaAdsLoaderTest { @Test public void requestAdTagWithUri_requestsWithAdTagUrl() throws Exception { - setupPlayback( - CONTENT_TIMELINE, - ImmutableList.of(0f), - new ImaAdsLoader.Builder(getApplicationContext()) - .setImaFactory(mockImaFactory) - .setImaSdkSettings(mockImaSdkSettings) - .build(), - TEST_DATA_SPEC, - TEST_ADS_ID); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -796,7 +821,6 @@ public final class ImaAdsLoaderTest { @Test public void setsDefaultMimeTypes() throws Exception { - setupPlayback(CONTENT_TIMELINE, ImmutableList.of(0f)); imaAdsLoader.setSupportedContentTypes(C.TYPE_DASH, C.TYPE_OTHER); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -814,16 +838,23 @@ public final class ImaAdsLoaderTest { @Test public void buildWithAdMediaMimeTypes_setsMimeTypes() throws Exception { - setupPlayback( - CONTENT_TIMELINE, - ImmutableList.of(0f), + imaAdsLoader = new ImaAdsLoader.Builder(getApplicationContext()) + .setAdMediaMimeTypes(ImmutableList.of(MimeTypes.AUDIO_MPEG)) .setImaFactory(mockImaFactory) .setImaSdkSettings(mockImaSdkSettings) - .setAdMediaMimeTypes(ImmutableList.of(MimeTypes.AUDIO_MPEG)) - .build(), - TEST_DATA_SPEC, - TEST_ADS_ID); + .build(); + imaAdsLoader.setPlayer(fakePlayer); + adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + TEST_ADS_ID, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider); + when(mockAdsManager.getAdCuePoints()).thenReturn(PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.setSupportedContentTypes(C.TYPE_OTHER); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); @@ -833,7 +864,6 @@ public final class ImaAdsLoaderTest { @Test public void stop_unregistersAllVideoControlOverlays() { - setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); imaAdsLoader.requestAds(TEST_DATA_SPEC, TEST_ADS_ID, adViewGroup); @@ -848,8 +878,9 @@ public final class ImaAdsLoaderTest { public void loadAd_withLargeAdCuePoint_updatesAdPlaybackStateWithLoadedAd() { // Use a large enough value to test correct truncating of large cue points. float midrollTimeSecs = Float.MAX_VALUE; - ImmutableList cuePoints = ImmutableList.of(midrollTimeSecs); - setupPlayback(CONTENT_TIMELINE, cuePoints); + List cuePoints = ImmutableList.of(midrollTimeSecs); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); + imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); videoAdPlayer.loadAd( @@ -886,7 +917,7 @@ public final class ImaAdsLoaderTest { } }); - assertThat(adsLoaderListener.adPlaybackState) + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) .isEqualTo( new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) @@ -895,37 +926,126 @@ public final class ImaAdsLoaderTest { .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})); } - private void setupPlayback(Timeline contentTimeline, List cuePoints) { - setupPlayback( - contentTimeline, - cuePoints, - new ImaAdsLoader.Builder(getApplicationContext()) - .setImaFactory(mockImaFactory) - .setImaSdkSettings(mockImaSdkSettings) - .build(), - TEST_DATA_SPEC, - TEST_ADS_ID); - } - - private void setupPlayback( - Timeline contentTimeline, - List cuePoints, - ImaAdsLoader imaAdsLoader, - DataSpec adTagDataSpec, - Object adsId) { - fakeExoPlayer = new FakePlayer(); - adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline); - adsMediaSource = + @Test + public void playbackWithTwoAdsMediaSources_preloadsSecondAdTag() { + Object secondAdsId = new Object(); + AdsMediaSource secondAdsMediaSource = new AdsMediaSource( - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), - adTagDataSpec, - adsId, + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + secondAdsId, new DefaultMediaSourceFactory((Context) getApplicationContext()), imaAdsLoader, adViewProvider); - when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); - this.imaAdsLoader = imaAdsLoader; - imaAdsLoader.setPlayer(fakeExoPlayer); + adsLoaderListener = + new TestAdsLoaderListener( + getInitialTimelineWindowDefinition(TEST_ADS_ID), + getInitialTimelineWindowDefinition(secondAdsId)); + + // Load and play the preroll ad then content. + imaAdsLoader.start( + adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); + adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); + videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); + adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); + videoAdPlayer.playAd(TEST_AD_MEDIA_INFO); + fakePlayer.setPlayingAdPosition( + /* periodIndex= */ 0, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* position= */ 0, + /* contentPosition= */ 0); + fakePlayer.setState(Player.STATE_READY, true); + adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); + adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd)); + adEventListener.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd)); + adEventListener.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd)); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, /* positionMs= */ 0); + videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO); + adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); + + // Simulate starting to buffer the second ads media source. + imaAdsLoader.start( + secondAdsMediaSource, TEST_DATA_SPEC, secondAdsId, adViewProvider, adsLoaderListener); + + // Verify that the preroll ad has been marked as played. + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) + .isEqualTo( + new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 0) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI) + .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withAdResumePositionUs(/* adResumePositionUs= */ 0)); + // Verify that the second source's ad cue points have preloaded. + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 1)) + .isEqualTo(new AdPlaybackState(secondAdsId, /* adGroupTimesUs...= */ 0)); + } + + @Test + public void playbackWithTwoAdsMediaSources_preloadsSecondAdTagWithBackgroundResume() { + Object secondAdsId = new Object(); + AdsMediaSource secondAdsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + secondAdsId, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider); + adsLoaderListener = + new TestAdsLoaderListener( + getInitialTimelineWindowDefinition(TEST_ADS_ID), + getInitialTimelineWindowDefinition(secondAdsId)); + + // Load and play the preroll ad then content. + imaAdsLoader.start( + adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); + adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); + videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); + adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); + videoAdPlayer.playAd(TEST_AD_MEDIA_INFO); + fakePlayer.setPlayingAdPosition( + /* periodIndex= */ 0, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* position= */ 0, + /* contentPosition= */ 0); + fakePlayer.setState(Player.STATE_READY, true); + adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); + adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd)); + adEventListener.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd)); + adEventListener.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd)); + fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, /* positionMs= */ 0); + videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO); + adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); + + // Simulate starting to buffer the second ads media source. + imaAdsLoader.start( + secondAdsMediaSource, TEST_DATA_SPEC, secondAdsId, adViewProvider, adsLoaderListener); + + // Simulate backgrounding/resuming. + imaAdsLoader.stop(adsMediaSource); + imaAdsLoader.stop(secondAdsMediaSource); + imaAdsLoader.start( + adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); + imaAdsLoader.start( + secondAdsMediaSource, TEST_DATA_SPEC, secondAdsId, adViewProvider, adsLoaderListener); + + // Verify that the preroll ad has been marked as played. + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 0)) + .isEqualTo( + new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 0) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI) + .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withAdResumePositionUs(/* adResumePositionUs= */ 0)); + // Verify that the second source's ad cue points have preloaded. + assertThat(adsLoaderListener.getAdPlaybackState(/* periodIndex= */ 1)) + .isEqualTo(new AdPlaybackState(secondAdsId, /* adGroupTimesUs...= */ 0)); } private void setupMocks() { @@ -1023,16 +1143,16 @@ public final class ImaAdsLoaderTest { } /** Ad loader event listener that forwards ad playback state to a fake player. */ - private static final class TestAdsLoaderListener implements AdsLoader.EventListener { + private final class TestAdsLoaderListener implements AdsLoader.EventListener { - private final FakePlayer fakeExoPlayer; - private final Timeline contentTimeline; + private final TimelineWindowDefinition[] timelineWindowDefinitions; - public AdPlaybackState adPlaybackState; + public TestAdsLoaderListener(TimelineWindowDefinition... timelineWindowDefinitions) { + this.timelineWindowDefinitions = timelineWindowDefinitions; + } - public TestAdsLoaderListener(FakePlayer fakeExoPlayer, Timeline contentTimeline) { - this.fakeExoPlayer = fakeExoPlayer; - this.contentTimeline = contentTimeline; + public AdPlaybackState getAdPlaybackState(int periodIndex) { + return timelineWindowDefinitions[periodIndex].adPlaybackState; } @Override @@ -1043,10 +1163,28 @@ public final class ImaAdsLoaderTest { Arrays.fill(adDurationsUs[adGroupIndex], TEST_AD_DURATION_US); } adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); - this.adPlaybackState = adPlaybackState; - fakeExoPlayer.updateTimeline( - new SinglePeriodAdTimeline(contentTimeline, adPlaybackState), - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + + // Update the timeline window definition(s) to reflect the new ad playback state. + for (int i = 0; i < timelineWindowDefinitions.length; i++) { + TimelineWindowDefinition timelineWindowDefinition = timelineWindowDefinitions[i]; + if (!Util.areEqual(timelineWindowDefinition.adPlaybackState.adsId, adPlaybackState.adsId)) { + continue; + } + timelineWindowDefinitions[i] = + new TimelineWindowDefinition( + timelineWindowDefinition.periodCount, + timelineWindowDefinition.id, + timelineWindowDefinition.isSeekable, + timelineWindowDefinition.isDynamic, + timelineWindowDefinition.isLive, + timelineWindowDefinition.isPlaceholder, + timelineWindowDefinition.durationUs, + timelineWindowDefinition.defaultPositionUs, + timelineWindowDefinition.windowOffsetInFirstPeriodUs, + adPlaybackState); + } + fakePlayer.updateTimeline( + new FakeTimeline(timelineWindowDefinitions), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Override @@ -1064,4 +1202,24 @@ public final class ImaAdsLoaderTest { // Do nothing. } } + + private static TimelineWindowDefinition getInitialTimelineWindowDefinition(Object adsId) { + return getInitialTimelineWindowDefinition(adsId, /* isPlaceholder= */ false); + } + + private static TimelineWindowDefinition getInitialTimelineWindowDefinition( + Object adsId, boolean isPlaceholder) { + return new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ new Object(), + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ isPlaceholder, + /* durationUs= */ CONTENT_DURATION_US, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ TimelineWindowDefinition + .DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + new AdPlaybackState(adsId)); + } }