diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java index b069c5ef34d..6eb7be8e4cb 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java @@ -15,9 +15,15 @@ */ package com.google.android.exoplayer2.ext.ima; +import static com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType.LOADED; import static com.google.android.exoplayer2.ext.ima.ImaUtil.addLiveAdBreak; import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInLiveMultiPeriodTimeline; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.getWindowStartTimeUs; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.handleAdPeriodRemovedFromTimeline; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.maybeCorrectPreviouslyUnknownAdDuration; import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToMsRounded; import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToUsRounded; import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdGroup; @@ -545,7 +551,9 @@ private ImaServerSideAdInsertionMediaSource( componentListener = new ComponentListener( isLiveStream - ? (isDashStream ? new NoopAdEventListener() : new SinglePeriodLiveAdEventListener()) + ? (isDashStream + ? new MultiPeriodLiveAdEventListener() + : new SinglePeriodLiveAdEventListener()) : new VodAdEventListener()); adPlaybackState = adsLoader.getAdPlaybackState(adsId); } @@ -677,6 +685,12 @@ private void setContentTimeline(Timeline contentTimeline) { if (contentTimeline.equals(this.contentTimeline)) { return; } + if (isLiveStream && Objects.equals(streamRequest.getFormat(), StreamFormat.DASH)) { + // If the ad started playing while the corresponding period in the timeline had an unknown + // duration, the ad duration is estimated and needs to be corrected when the actual duration + // is reported. + adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + } this.contentTimeline = contentTimeline; invalidateServerSideAdInsertionAdPlaybackState(); } @@ -685,7 +699,7 @@ private void setContentTimeline(Timeline contentTimeline) { private void invalidateServerSideAdInsertionAdPlaybackState() { if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) { ImmutableMap splitAdPlaybackStates; - if (streamRequest.getFormat() == StreamFormat.DASH) { + if (Objects.equals(streamRequest.getFormat(), StreamFormat.DASH)) { // DASH ad groups are always split by period. splitAdPlaybackStates = splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline); } else { @@ -815,8 +829,9 @@ public void onPositionDiscontinuity( Player.PositionInfo oldPosition, Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) { - if (reason != Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { - // Only auto transitions within the same or to the next media item are of interest. + if (!(reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION + || (isLiveStream && reason == Player.DISCONTINUITY_REASON_REMOVE))) { + // Only auto transitions and removals of an ad period in live streams need to be handled. return; } @@ -843,12 +858,24 @@ public void onPositionDiscontinuity( Timeline.Window window = timeline.getWindow(oldPosition.mediaItemIndex, new Timeline.Window()); if (window.lastPeriodIndex > window.firstPeriodIndex) { + if (reason == Player.DISCONTINUITY_REASON_REMOVE) { + setAdPlaybackState( + handleAdPeriodRemovedFromTimeline( + player.getCurrentPeriodIndex(), timeline, adPlaybackState)); + return; + } // Map adGroupIndex and adIndexInAdGroup to multi-period window. Pair adGroupIndexAndAdIndexInAdGroup = - getAdGroupAndIndexInMultiPeriodWindow( - oldPosition.periodIndex - window.firstPeriodIndex, - adPlaybackState, - checkNotNull(contentTimeline)); + window.isLive() + ? getAdGroupAndIndexInLiveMultiPeriodTimeline( + oldPosition.mediaItemIndex, + oldPosition.periodIndex - window.firstPeriodIndex, + timeline, + adPlaybackState) + : getAdGroupAndIndexInVodMultiPeriodTimeline( + oldPosition.periodIndex - window.firstPeriodIndex, + adPlaybackState, + checkNotNull(contentTimeline)); adGroupIndex = adGroupIndexAndAdIndexInAdGroup.first; adIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup.second; } @@ -926,9 +953,9 @@ public void onAdEvent(AdEvent event) { @Override public boolean onAdPlaybackStateUpdateRequested(Timeline contentTimeline) { mainHandler.post(() -> setContentTimeline(contentTimeline)); - // Defer source refresh to ad playback state update for VOD. Refresh immediately when live - // with single period. - return !isLiveStream || contentTimeline.getPeriodCount() > 1; + // Defer source refresh to ad playback state update for VOD (wait for potential ad cue points) + // or DASH (split manifest). + return !isLiveStream || Objects.equals(streamRequest.getFormat(), StreamFormat.DASH); } } @@ -1368,7 +1395,7 @@ public void onAdEvent(AdEvent event) { private class SinglePeriodLiveAdEventListener implements AdEventListener { @Override public void onAdEvent(AdEvent event) { - if (event.getType() != AdEvent.AdEventType.LOADED) { + if (!Objects.equals(event.getType(), LOADED)) { return; } AdPlaybackState newAdPlaybackState = adPlaybackState; @@ -1396,14 +1423,40 @@ public void onAdEvent(AdEvent event) { } } - private static class NoopAdEventListener implements AdEventListener { + private class MultiPeriodLiveAdEventListener implements AdEventListener { @Override public void onAdEvent(AdEvent event) { - Log.w( - "ImaSSAIMediaSource", - String.format( - "Ignoring IMA ad event %s because the current stream type is not supported.", - event.getType().name())); + if (!Objects.equals(event.getType(), LOADED)) { + return; + } + AdPodInfo adPodInfo = event.getAd().getAdPodInfo(); + Timeline timeline = player.getCurrentTimeline(); + Timeline.Window window = new Timeline.Window(); + Timeline.Period adPeriod = new Timeline.Period(); + // In case all periods are in the live window, we need the correct ad group duration when + // inserting the first ad. Try calculate ad group duration from media structure. + long totalAdDurationUs = + getAdGroupDurationUsForLiveAdPeriodIndex( + timeline, + adPodInfo, + /* adPeriodIndex= */ player.getCurrentPeriodIndex(), + window, + adPeriod); + long adPeriodStartTimeUs = + getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs) + + adPeriod.positionInWindowUs; + long adDurationUs = + adPeriod.durationUs != C.TIME_UNSET + ? adPeriod.durationUs + : secToUsRounded(event.getAd().getDuration()); + setAdPlaybackState( + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ adPeriodStartTimeUs, + adDurationUs, + adPodInfo.getAdPosition(), + totalAdDurationUs, + adPodInfo.getTotalAds(), + adPlaybackState)); } } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java index d94b93561ec..3954825049f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.ima; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE; +import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_PLAYED; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE; import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.getMediaPeriodPositionUsForContent; @@ -38,6 +39,7 @@ import com.google.ads.interactivemedia.v3.api.AdError; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdEvent; +import com.google.ads.interactivemedia.v3.api.AdPodInfo; import com.google.ads.interactivemedia.v3.api.AdsLoader; import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; @@ -380,7 +382,7 @@ private static long[] updateAdDurationAndPropagate( * @param positionInFirstPeriodUs The position of the window in the first period. * @return The window start time, in microseconds. */ - private static long getWindowStartTimeUs(long windowStartTimeMs, long positionInFirstPeriodUs) { + public static long getWindowStartTimeUs(long windowStartTimeMs, long positionInFirstPeriodUs) { // Revert us/ms truncation introduced in `DashMediaSource.DashTimeline`. return msToUs(windowStartTimeMs) + (positionInFirstPeriodUs % 1000); } @@ -508,6 +510,10 @@ private static AdPlaybackState splitAdGroupForPeriod( /* adGroupIndex= */ 0, isLiveStream ? sanitizedDurationUs : 0); // Map the state of the global ad state to the period specific ad state. switch (adGroup.states[i]) { + case AD_STATE_AVAILABLE: + adPlaybackState = + adPlaybackState.withAvailableAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + break; case AdPlaybackState.AD_STATE_PLAYED: adPlaybackState = adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); @@ -530,32 +536,299 @@ private static AdPlaybackState splitAdGroupForPeriod( return adPlaybackState; } + /** + * Updates a previously estimated ad duration with the period duration from the timeline. + * + *

This method must only be called for multi period live streams and is useful in the case that + * an ad started playing while its period duration was still unknown. In this case the estimated + * ad duration was used which can be corrected as soon as the {@code contentTimeline} was + * refreshed with the actual period duration. + * + *

The method queries the {@linkplain AdPlaybackState ad playback state} for an ad that starts + * at the period start time of the last period that has a known duration. If found, the ad + * duration is set to the period duration and the new ad playback state is returned. If not found + * or the duration is already correct the ad playback state remains unchanged. + * + * @param contentTimeline The live content timeline. + * @param adPlaybackState The ad playback state. + * @return The (potentially) updated ad playback state. + */ + public static AdPlaybackState maybeCorrectPreviouslyUnknownAdDuration( + Timeline contentTimeline, AdPlaybackState adPlaybackState) { + Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); + if (window.firstPeriodIndex == window.lastPeriodIndex || adPlaybackState.adGroupCount < 2) { + // Single period with unknown duration, or only a postroll placeholder. + return adPlaybackState; + } + Timeline.Period period = new Timeline.Period(); + // Get the first period from the end with a known duration. + int periodIndex = window.lastPeriodIndex; + while (periodIndex >= window.firstPeriodIndex + && contentTimeline.getPeriod(periodIndex, period).durationUs == C.TIME_UNSET) { + periodIndex--; + } + // Search for an ad group at or before the period start. + long windowStartTimeUs = + getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs); + long periodStartTimeUs = windowStartTimeUs + period.positionInWindowUs; + int adGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs( + periodStartTimeUs, /* periodDurationUs= */ C.TIME_UNSET); + if (adGroupIndex == C.INDEX_UNSET) { + // No ad group at or before the period start. + return adPlaybackState; + } + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); + if (adGroup.timeUs + adGroup.contentResumeOffsetUs < periodStartTimeUs) { + // Ad group ends before the period starts. + return adPlaybackState; + } + // Period is inside the ad group. Get ad start that matches the period start. + long adGroupDurationUs = 0; + for (int adIndex = 0; adIndex < adGroup.durationsUs.length; adIndex++) { + long adDurationUs = adGroup.durationsUs[adIndex]; + if (adGroup.timeUs + adGroupDurationUs < periodStartTimeUs) { + adGroupDurationUs += adDurationUs; + continue; + } + if (period.durationUs == adDurationUs) { + // No update required. + return adPlaybackState; + } + // Set the ad duration to the period duration. + adPlaybackState = + updateAdDurationInAdGroup( + adGroupIndex, /* adIndexInAdGroup= */ adIndex, period.durationUs, adPlaybackState); + // Get the ad group again and set the new content resume offset after update. + adGroupDurationUs = sum(adPlaybackState.getAdGroup(adGroupIndex).durationsUs); + return adPlaybackState.withContentResumeOffsetUs(adGroupIndex, adGroupDurationUs); + } + // Return unchanged. + return adPlaybackState; + } + + /** + * Returns the sum of the durations of all the ads in an ad pod, in microseconds. + * + *

If all periods are contained in the current window and have a known duration, the sum of the + * period durations is returned. If this is not possible, {@link AdPodInfo#getMaxDuration() the + * SDK provided group duration} is used instead. + * + *

Do not use this method with public timelines of VOD multi-period streams that have the ad + * duration subtracted from period durations. + * + * @param timeline The timeline. + * @param adPodInfo The {@link AdPodInfo} from the SDK event. + * @param adPeriodIndex The period index of the period corresponding to the {@linkplain + * AdPodInfo#getAdPosition() ad position in the ad pod info}. + * @param window The window to be populated as the window containing the ad period. + * @param period The period to be populated as the ad period. + * @return The duration of the ad group, in microseconds. + */ + public static long getAdGroupDurationUsForLiveAdPeriodIndex( + Timeline timeline, + AdPodInfo adPodInfo, + int adPeriodIndex, + Timeline.Window window, + Timeline.Period period) { + timeline.getPeriod(adPeriodIndex, period); + timeline.getWindow(period.windowIndex, window); + checkArgument(window.isLive()); + int adIndexInAdGroup = adPodInfo.getAdPosition() - 1; + int firstAdPeriodIndex = adPeriodIndex - adIndexInAdGroup; + int lastAdPeriodIndex = adPeriodIndex + (adPodInfo.getTotalAds() - adIndexInAdGroup - 1); + long adGroupDurationUs = C.TIME_UNSET; + if (window.firstPeriodIndex <= firstAdPeriodIndex + && lastAdPeriodIndex < window.lastPeriodIndex) { + adGroupDurationUs = 0L; + Timeline.Period currentPeriod = new Timeline.Period(); + for (int periodIndex = firstAdPeriodIndex; periodIndex <= lastAdPeriodIndex; periodIndex++) { + long durationUs = timeline.getPeriod(periodIndex, currentPeriod).durationUs; + if (durationUs == C.TIME_UNSET) { + adGroupDurationUs = C.TIME_UNSET; + break; + } + adGroupDurationUs += durationUs; + } + } + return adGroupDurationUs != C.TIME_UNSET + ? adGroupDurationUs + : secToUsRounded(adPodInfo.getMaxDuration()); + } + + /** + * Handles the case when an ad period has been removed from the timeline while being played. + * + *

This makes sure no unplayed ads are left in the ad playback state after buffering or after + * the player has been paused in a live stream. In such a case periods may be removed from the + * timeline before playback transitions to the next period automatically. + * + * @param currentPeriodIndex The index of the {@linkplain Player#getCurrentPeriodIndex() current + * period}. + * @param timeline The timeline containing the current period. + * @param adPlaybackState The ad playback state to query. + * @return The updated ad playback state. + */ + @CheckResult + public static AdPlaybackState handleAdPeriodRemovedFromTimeline( + int currentPeriodIndex, Timeline timeline, AdPlaybackState adPlaybackState) { + Timeline.Period currentPeriod = timeline.getPeriod(currentPeriodIndex, new Timeline.Period()); + Timeline.Window window = timeline.getWindow(currentPeriod.windowIndex, new Timeline.Window()); + long currentPeriodStartTimeUs = + getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs) + + currentPeriod.positionInWindowUs; + int adGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs(currentPeriodStartTimeUs, C.TIME_UNSET); + if (adGroupIndex != C.INDEX_UNSET) { + AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); + if (adGroup.timeUs + adGroup.contentResumeOffsetUs <= currentPeriodStartTimeUs) { + // current period starts after ad group. + return markAdGroupAsPlayed(adGroupIndex, /* preserveDurations= */ true, adPlaybackState); + } + int lastAvailableAdIndex = C.INDEX_UNSET; + long adGroupDurationUs = 0; + for (int i = 0; i < adGroup.states.length; i++) { + int state = adGroup.states[i]; + if (state == AD_STATE_AVAILABLE) { + lastAvailableAdIndex = i; + } + if (currentPeriodStartTimeUs <= adGroup.timeUs + adGroupDurationUs) { + if (currentPeriodStartTimeUs == adGroup.timeUs + adGroupDurationUs) { + // Found the ad matching the current period. + if (state == AD_STATE_AVAILABLE || state == AD_STATE_PLAYED) { + return adPlaybackState; + } + if (state == AD_STATE_UNAVAILABLE && lastAvailableAdIndex == i - 1) { + // First unavailable ad in the group. + if (currentPeriod.durationUs == C.TIME_UNSET) { + // All ad data available except the last period. Will be corrected as normal. + return adPlaybackState; + } + // Else we can set the duration in the ad group as if we received the ad event. + adPlaybackState = + updateAdDurationInAdGroup( + adGroupIndex, + /* adIndexInAdGroup= */ i, + /* adDurationUs= */ currentPeriod.durationUs, + adPlaybackState); + adGroup = adPlaybackState.getAdGroup(adGroupIndex); + return adPlaybackState.withContentResumeOffsetUs( + adGroupIndex, sum(adGroup.durationsUs)); + } + } + // We can't calculate the index position of the current period within the ad group after + // an ad period has been removed from the timeline. Give up. + adPlaybackState = + markAdGroupAsPlayed(adGroupIndex, /* preserveDurations= */ false, adPlaybackState); + // Future SDK events belonging to the skipped ad group are handled in #addLiveAdBreak() + // like when joining the live stream while an ad is playing and will insert a new separate + // partial ad group. + if (currentPeriod.durationUs != C.TIME_UNSET) { + // Insert the current ad as a single ad group instead of the skipped ad group. + return addLiveAdBreak( + currentPeriodStartTimeUs, + /* adDurationUs= */ currentPeriod.durationUs, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ currentPeriod.durationUs, + /* totalAdsInAdPod= */ 1, + adPlaybackState); + } + return adPlaybackState; + } + if (state == AD_STATE_AVAILABLE || state == AD_STATE_UNAVAILABLE) { + adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ i); + } + adGroupDurationUs += adGroup.durationsUs[i]; + } + } + return adPlaybackState; + } + + private static AdPlaybackState markAdGroupAsPlayed( + int adGroupIndex, boolean preserveDurations, AdPlaybackState adPlaybackState) { + AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); + long[] newDurationsUs = new long[adGroup.durationsUs.length]; + for (int adIndex = 0; adIndex < newDurationsUs.length; adIndex++) { + newDurationsUs[adIndex] = preserveDurations ? adGroup.durationsUs[adIndex] : 0; + adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ adIndex); + } + return adPlaybackState + .withAdDurationsUs(adGroupIndex, newDurationsUs) + .withContentResumeOffsetUs(adGroupIndex, sum(newDurationsUs)); + } + + /** + * Returns the {@code adGroupIndex} in the ad playback state corresponding to the given period + * index in the timeline and the {@code adIndexInAdGroup} of the first unplayed ad in that ad + * group. + * + * @param currentMediaItemIndex The {@linkplain Player#getCurrentMediaItemIndex() current media + * item index}. + * @param adPeriodIndex The index of the ad period in the timeline. + * @param timeline The timeline that contains the period for the ad period index. + * @param adPlaybackState The ad playback state that holds the ad group and ad information. + * @return A pair with the ad group index (first) and the ad index in that ad group (second). + * @exception IllegalStateException If no unplayed ad is found before or at the start time of the + * ad period. + */ + public static Pair getAdGroupAndIndexInLiveMultiPeriodTimeline( + int currentMediaItemIndex, + int adPeriodIndex, + Timeline timeline, + AdPlaybackState adPlaybackState) { + Timeline.Window window = + timeline.getWindow(/* windowIndex= */ currentMediaItemIndex, new Timeline.Window()); + checkArgument(window.isLive()); + Timeline.Period period = new Timeline.Period(); + timeline.getPeriod(adPeriodIndex, period, /* setIds= */ true); + long adPeriodStartTimeUs = + getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs) + + period.positionInWindowUs; + int adGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs(adPeriodStartTimeUs, C.TIME_UNSET); + if (adGroupIndex != C.INDEX_UNSET) { + AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); + for (int k = 0; k < adGroup.states.length; k++) { + if (adGroup.states[k] == AD_STATE_AVAILABLE || adGroup.states[k] == AD_STATE_UNAVAILABLE) { + return new Pair<>(/* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ k); + } + } + } + throw new IllegalStateException( + String.format( + "No unplayed ad group found before or at the start time us %d of the period with index" + + " %d", + adPeriodStartTimeUs, adPeriodIndex)); + } + /** * Returns the {@code adGroupIndex} and the {@code adIndexInAdGroup} for the given period index of - * an ad period. + * an ad period in a VOD multi-period timeline. * * @param adPeriodIndex The period index of the ad period. * @param adPlaybackState The ad playback state that holds the ad group and ad information. * @param contentTimeline The timeline that contains the ad period. * @return A pair with the ad group index (first) and the ad index in that ad group (second). */ - public static Pair getAdGroupAndIndexInMultiPeriodWindow( + public static Pair getAdGroupAndIndexInVodMultiPeriodTimeline( int adPeriodIndex, AdPlaybackState adPlaybackState, Timeline contentTimeline) { - Timeline.Period period = new Timeline.Period(); + Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); + checkArgument(contentTimeline.getWindowCount() == 1); int periodIndex = 0; long totalElapsedContentDurationUs = 0; - Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); if (window.isLive()) { long windowStartTimeUs = getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs); totalElapsedContentDurationUs = windowStartTimeUs - window.positionInFirstPeriodUs; } + Timeline.Period period = new Timeline.Period(); for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { int adIndexInAdGroup = 0; AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i); long adGroupDurationUs = sum(adGroup.durationsUs); long elapsedAdGroupAdDurationUs = 0; - for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) { + for (int j = periodIndex; j < min(contentTimeline.getPeriodCount(), adPeriodIndex + 1); j++) { contentTimeline.getPeriod(j, period, /* setIds= */ true); if (totalElapsedContentDurationUs < adGroup.timeUs) { // Period starts before the ad group, so it is a content period. diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java index 33e440d3b54..ea8416ff7da 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java @@ -16,26 +16,33 @@ package com.google.android.exoplayer2.ext.ima; import static com.google.android.exoplayer2.ext.ima.ImaUtil.addLiveAdBreak; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInLiveMultiPeriodTimeline; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.handleAdPeriodRemovedFromTimeline; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.maybeCorrectPreviouslyUnknownAdDuration; +import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToUsRounded; import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdGroup; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_ERROR; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_PLAYED; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_SKIPPED; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE; +import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; import static com.google.android.exoplayer2.testutil.FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_US; import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US; import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.ads.interactivemedia.v3.api.AdPodInfo; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil; import com.google.android.exoplayer2.testutil.FakeMultiPeriodLiveTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.common.collect.ImmutableMap; @@ -657,14 +664,14 @@ public void splitAdPlaybackStateForPeriods_earlyMidrollAdGroupStartTimeUs_adGrou // Mark previous ad group as played. Pair adGroupAndAdIndex = - ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( + ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline( /* adPeriodIndex= */ 3, adPlaybackState, liveTimeline); adPlaybackState = adPlaybackState.withPlayedAd( /* adGroupIndex= */ adGroupAndAdIndex.first, /* adIndexInAdGroup= */ adGroupAndAdIndex.second); adGroupAndAdIndex = - ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( + ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline( /* adPeriodIndex= */ 4, adPlaybackState, liveTimeline); adPlaybackState = adPlaybackState.withPlayedAd( @@ -782,14 +789,14 @@ public void splitAdPlaybackStateForPeriods_earlyMidrollAdGroupStartTimeUs_adGrou // Mark previous ad group as played. adGroupAndAdIndex = - ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( + ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline( /* adPeriodIndex= */ 2, adPlaybackState, liveTimeline); adPlaybackState = adPlaybackState.withPlayedAd( /* adGroupIndex= */ adGroupAndAdIndex.first, /* adIndexInAdGroup= */ adGroupAndAdIndex.second); adGroupAndAdIndex = - ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( + ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline( /* adPeriodIndex= */ 3, adPlaybackState, liveTimeline); adPlaybackState = adPlaybackState.withPlayedAd( @@ -1002,7 +1009,7 @@ public void splitAdPlaybackStateForPeriods_earlyMidrollAdGroupStartTimeUs_adGrou @Test public void expandAdGroupPlaceHolder_expandWithFirstAdInGroup_correctExpansion() { AdPlaybackState adPlaybackState = - ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + addAdGroupToAdPlaybackState( AdPlaybackState.NONE, /* fromPositionUs= */ 0, /* contentResumeOffsetUs= */ 0, @@ -1024,10 +1031,313 @@ public void expandAdGroupPlaceHolder_expandWithFirstAdInGroup_correctExpansion() assertThat(adGroup.durationsUs[2]).isEqualTo(0); } + @Test + public void maybeCorrectPreviouslyUnknownAdDuration_singleAdInAdGroup_adDurationCorrected() { + long liveWindowDurationUs = 60_000_000L; + long nowUs = 110_234_567L; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + addAdGroupToAdPlaybackState( + adPlaybackState, + /* fromPositionUs= */ 80_000_000L, + /* contentResumeOffsetUs= */ 123, + /* adDurationsUs...= */ 123); + // Second ad group was inserted when the period duration was unknown and can be corrected with + // the new content timeline. + adPlaybackState = + addAdGroupToAdPlaybackState( + adPlaybackState, + /* fromPositionUs= */ 90_000_000L, + /* contentResumeOffsetUs= */ 123, + /* adDurationsUs...= */ 123); + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + liveWindowDurationUs, + nowUs, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + + adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs).isEqualTo(80_000_000L); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(123L); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 1).timeUs).isEqualTo(90_000_000L); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 1).durationsUs) + .asList() + .containsExactly(AD_PERIOD_DURATION_US); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 1).contentResumeOffsetUs) + .isEqualTo(AD_PERIOD_DURATION_US); + } + + @Test + public void maybeCorrectPreviouslyUnknownAdDuration_multipleAdsInAdGroup_adDurationCorrected() { + long liveWindowDurationUs = 60_000_000L; + long nowUs = 110_234_567L; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + addAdGroupToAdPlaybackState( + adPlaybackState, + /* fromPositionUs= */ 80_000_000L, + /* contentResumeOffsetUs= */ 10_000_123L, + /* adDurationsUs...= */ AD_PERIOD_DURATION_US, + 123L); + // Content timeline: [content, ad, ad, content] + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + liveWindowDurationUs, + nowUs, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + + adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs).isEqualTo(80_000_000L); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(AD_PERIOD_DURATION_US, AD_PERIOD_DURATION_US); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).contentResumeOffsetUs) + .isEqualTo(2 * AD_PERIOD_DURATION_US); + } + + @Test + public void + maybeCorrectPreviouslyUnknownAdDuration_windowPastAdGroups_adPlaybackStateNotChanged() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + addAdGroupToAdPlaybackState( + adPlaybackState, + /* fromPositionUs= */ 80_000_000L, + /* contentResumeOffsetUs= */ 123, + /* adDurationsUs...= */ 123); + adPlaybackState = + addAdGroupToAdPlaybackState( + adPlaybackState, + /* fromPositionUs= */ 90_000_000L, + /* contentResumeOffsetUs= */ 123, + /* adDurationsUs...= */ 123); + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 60_000_000L, + /* nowUs= */ 160_000_000L, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + + AdPlaybackState correctedAdPlaybackState = + maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + + assertThat(contentTimeline.getWindow(/* windowIndex= */ 0, new Window()).windowStartTimeMs) + .isEqualTo(100_000L); + assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState); + } + + @Test + public void + maybeCorrectPreviouslyUnknownAdDuration_withInsertionRemainder_preserveRemainderDuration() { + // Content and ad period in window at the beginning: [c, a, a] + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 50_000_000L, + /* nowUs= */ 50_000_000L, + /* adSequencePattern= */ new boolean[] {false, true, true, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + // Insert first ad resulting in group [10_000, 20_000, 0] + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 30_000_000, + /* adDurationUs= */ 10_000_000, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 40_000_123, + /* totalAdsInAdPod= */ 4, + adPlaybackState); + + AdPlaybackState correctedAdPlaybackState = + maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + + // Assert starting point: no change because the second ad period is still last of window. + assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState); + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 30_000_123L, 0L, 0L) + .inOrder(); + + // Get third ad period into timeline so the second ad period gets a duration: [c, a, a, a] + contentTimeline.advanceNowUs(1L); + correctedAdPlaybackState = + maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState); + + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 20_000_123L, 0L) + .inOrder(); + + // Get next ad period into timeline so the third ad period gets a duration: [c, a, a, a, a] + contentTimeline.advanceNowUs(10_000_000L); + correctedAdPlaybackState = + maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState); + + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_123L) + .inOrder(); + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly(1, 0, 0, 0) + .inOrder(); + + // It doesn't matter whether the live break event or the correction propagates the remainder + // forward. Updating the ad by ad event later only marks the ad as available. + correctedAdPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 40_000_000, + /* adDurationUs= */ 10_000_000, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 40_000_000, + /* totalAdsInAdPod= */ 4, + correctedAdPlaybackState); + + // No change in durations. + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_123L) + .inOrder(); + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly(1, 1, 0, 0); + + correctedAdPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 40_000_000, + /* adDurationUs= */ 10_000_000, + /* adPositionInAdPod= */ 3, + /* totalAdDurationUs= */ 40_000_000, + /* totalAdsInAdPod= */ 4, + correctedAdPlaybackState); + + // No change in durations. + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_123L) + .inOrder(); + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly(1, 1, 1, 0); + + correctedAdPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 40_000_000, + /* adDurationUs= */ 9_999_999L, + /* adPositionInAdPod= */ 3, + /* totalAdDurationUs= */ 40_000_000, + /* totalAdsInAdPod= */ 4, + correctedAdPlaybackState); + + // Last duration updated with ad pod duration. + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 9_999_999L) + .inOrder(); + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly(1, 1, 1, 1); + + // Get next period into timeline so the 4th ad period gets a duration: [..., a, a, c] + contentTimeline.advanceNowUs(10_000_000L); + correctedAdPlaybackState = + maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState); + + // Last duration corrected when period arrives. + assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_000L); + } + + @Test + public void + maybeCorrectPreviouslyUnknownAdDuration_singleContentPeriodTimeline_adPlaybackStateNotChanged() { + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 30_000_000L, + /* nowUs= */ 80_000_000L, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + addAdGroupToAdPlaybackState( + adPlaybackState, + /* fromPositionUs= */ 30_000_000L, + /* contentResumeOffsetUs= */ 123, + /* adDurationsUs...= */ 123); + adPlaybackState = + addAdGroupToAdPlaybackState( + adPlaybackState, + /* fromPositionUs= */ 40_000_000L, + /* contentResumeOffsetUs= */ 123, + /* adDurationsUs...= */ 123); + + AdPlaybackState correctedAdPlaybackState = + maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + + assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState); + } + + @Test + public void + maybeCorrectPreviouslyUnknownAdDuration_singleAdPeriodTimeline_doesNotOverrideWithTimeUnset() { + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 10_000_000L, + /* nowUs= */ 90_000_000L, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + addAdGroupToAdPlaybackState( + adPlaybackState, + /* fromPositionUs= */ 80_000_000L, + /* contentResumeOffsetUs= */ 123L, + /* adDurationsUs...= */ 123L); + + adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs).isEqualTo(80_000_000L); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(123L); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).contentResumeOffsetUs) + .isEqualTo(123L); + } + @Test public void expandAdGroupPlaceHolder_expandWithMiddleAdInGroup_correctExpansion() { AdPlaybackState adPlaybackState = - ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + addAdGroupToAdPlaybackState( AdPlaybackState.NONE, /* fromPositionUs= */ 0, /* contentResumeOffsetUs= */ 0, @@ -1052,7 +1362,7 @@ public void expandAdGroupPlaceHolder_expandWithMiddleAdInGroup_correctExpansion( @Test public void expandAdGroupPlaceHolder_expandWithLastAdInGroup_correctDurationWrappedAround() { AdPlaybackState adPlaybackState = - ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + addAdGroupToAdPlaybackState( AdPlaybackState.NONE, /* fromPositionUs= */ 0, /* contentResumeOffsetUs= */ 0, @@ -1077,7 +1387,7 @@ public void expandAdGroupPlaceHolder_expandWithLastAdInGroup_correctDurationWrap @Test public void expandAdGroupPlaceHolder_expandSingleAdInAdGroup_noExpansionCorrectDuration() { AdPlaybackState adPlaybackState = - ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + addAdGroupToAdPlaybackState( AdPlaybackState.NONE, /* fromPositionUs= */ 0, /* contentResumeOffsetUs= */ 0, @@ -1100,7 +1410,7 @@ public void expandAdGroupPlaceHolder_expandSingleAdInAdGroup_noExpansionCorrectD @Test public void expandAdGroupPlaceHolder_singleAdInAdGroupOverLength_correctsAdDuration() { AdPlaybackState adPlaybackState = - ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + addAdGroupToAdPlaybackState( AdPlaybackState.NONE, /* fromPositionUs= */ 0, /* contentResumeOffsetUs= */ 0, @@ -1123,7 +1433,7 @@ public void expandAdGroupPlaceHolder_singleAdInAdGroupOverLength_correctsAdDurat @Test public void expandAdGroupPlaceHolder_initialDurationTooLarge_overriddenWhenExpanded() { AdPlaybackState adPlaybackState = - ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + addAdGroupToAdPlaybackState( AdPlaybackState.NONE, /* fromPositionUs= */ 0, /* contentResumeOffsetUs= */ 0, @@ -1147,7 +1457,7 @@ public void expandAdGroupPlaceHolder_initialDurationTooLarge_overriddenWhenExpan @Test public void insertAdDurationInAdGroup_correctDurationAndPropagation() { AdPlaybackState adPlaybackState = - ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + addAdGroupToAdPlaybackState( AdPlaybackState.NONE, /* fromPositionUs= */ 0, /* contentResumeOffsetUs= */ 0, @@ -1172,7 +1482,7 @@ public void insertAdDurationInAdGroup_correctDurationAndPropagation() { @Test public void insertAdDurationInAdGroup_insertLast_correctDurationAndPropagation() { AdPlaybackState adPlaybackState = - ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + addAdGroupToAdPlaybackState( AdPlaybackState.NONE, /* fromPositionUs= */ 0, /* contentResumeOffsetUs= */ 0, @@ -1197,7 +1507,7 @@ public void insertAdDurationInAdGroup_insertLast_correctDurationAndPropagation() @Test public void insertAdDurationInAdGroup_allDurationsSetAlready_setDurationNoPropagation() { AdPlaybackState adPlaybackState = - ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + addAdGroupToAdPlaybackState( AdPlaybackState.NONE, /* fromPositionUs= */ 0, /* contentResumeOffsetUs= */ 0, @@ -1220,7 +1530,7 @@ public void insertAdDurationInAdGroup_allDurationsSetAlready_setDurationNoPropag } @Test - public void getAdGroupAndIndexInMultiPeriodWindow_correctAdGroupIndexAndAdIndexInAdGroup() { + public void getAdGroupAndIndexInVodMultiPeriodTimeline_correctAdGroupIndexAndAdIndexInAdGroup() { FakeTimeline timeline = new FakeTimeline( new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 9, new Object())); @@ -1241,62 +1551,70 @@ public void getAdGroupAndIndexInMultiPeriodWindow_correctAdGroupIndexAndAdIndexI .withIsServerSideInserted(/* adGroupIndex= */ 0, true); Pair adGroupIndexAndAdIndexInAdGroup = - getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 0, adPlaybackState, timeline); + getAdGroupAndIndexInVodMultiPeriodTimeline( + /* adPeriodIndex= */ 0, adPlaybackState, timeline); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0); adGroupIndexAndAdIndexInAdGroup = - getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 1, adPlaybackState, timeline); + getAdGroupAndIndexInVodMultiPeriodTimeline( + /* adPeriodIndex= */ 1, adPlaybackState, timeline); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1); Assert.assertThrows( IllegalStateException.class, () -> - getAdGroupAndIndexInMultiPeriodWindow( + getAdGroupAndIndexInVodMultiPeriodTimeline( /* adPeriodIndex= */ 2, adPlaybackState, timeline)); adGroupIndexAndAdIndexInAdGroup = - getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 3, adPlaybackState, timeline); + getAdGroupAndIndexInVodMultiPeriodTimeline( + /* adPeriodIndex= */ 3, adPlaybackState, timeline); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0); adGroupIndexAndAdIndexInAdGroup = - getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 4, adPlaybackState, timeline); + getAdGroupAndIndexInVodMultiPeriodTimeline( + /* adPeriodIndex= */ 4, adPlaybackState, timeline); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1); adGroupIndexAndAdIndexInAdGroup = - getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 5, adPlaybackState, timeline); + getAdGroupAndIndexInVodMultiPeriodTimeline( + /* adPeriodIndex= */ 5, adPlaybackState, timeline); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(2); Assert.assertThrows( IllegalStateException.class, () -> - getAdGroupAndIndexInMultiPeriodWindow( + getAdGroupAndIndexInVodMultiPeriodTimeline( /* adPeriodIndex= */ 6, adPlaybackState, timeline)); adGroupIndexAndAdIndexInAdGroup = - getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 7, adPlaybackState, timeline); + getAdGroupAndIndexInVodMultiPeriodTimeline( + /* adPeriodIndex= */ 7, adPlaybackState, timeline); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0); adGroupIndexAndAdIndexInAdGroup = - getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 8, adPlaybackState, timeline); + getAdGroupAndIndexInVodMultiPeriodTimeline( + /* adPeriodIndex= */ 8, adPlaybackState, timeline); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1); } @Test public void - getAdGroupAndIndexInMultiPeriodWindow_liveWindow_correctAdGroupIndexAndAdIndexInAdGroup() { + getAdGroupAndIndexInLiveMultiPeriodTimeline_calledForPeriodsAfterUnplayedAdGroup_correctAdGroupIndexAndAdIndexInAdGroup() { + // Content live window with content and ad periods: c, [a, c, a, a, a, c, a], a, a FakeMultiPeriodLiveTimeline contentTimeline = new FakeMultiPeriodLiveTimeline( /* availabilityStartTimeUs= */ 0, /* liveWindowDurationUs= */ 100_000_000, - /* nowUs= */ 150_000_000, - /* adSequencePattern= */ new boolean[] {false, true, true}, + /* nowUs= */ 159_000_123, + /* adSequencePattern= */ new boolean[] {false, true, true, true}, /* isContentTimeline= */ true, /* populateAds= */ false, /* playedAds= */ false); @@ -1304,12 +1622,21 @@ public void getAdGroupAndIndexInMultiPeriodWindow_correctAdGroupIndexAndAdIndexI new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); adPlaybackState = addLiveAdBreak( - /* currentContentPeriodPositionUs= */ 80_000_000, + /* currentContentPeriodPositionUs= */ 50_000_000, /* adDurationUs= */ AD_PERIOD_DURATION_US, /* adPositionInAdPod= */ 1, /* totalAdDurationUs= */ AD_PERIOD_DURATION_US, /* totalAdsInAdPod= */ 1, adPlaybackState); + + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 0, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(0, 0)); + adPlaybackState = adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); adPlaybackState = @@ -1317,50 +1644,531 @@ public void getAdGroupAndIndexInMultiPeriodWindow_correctAdGroupIndexAndAdIndexI /* currentContentPeriodPositionUs= */ 90_000_000, /* adDurationUs= */ AD_PERIOD_DURATION_US, /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 100_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 110_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 3, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + AdPlaybackState finalAdPlaybackState = adPlaybackState; + + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 2, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(1, 0)); + + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0); + + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 3, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(1, 1)); + + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1); + + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 4, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(1, 2)); + + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 150_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 6, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(2, 0)); + } + + @Test + public void + getAdGroupAndIndexInLiveMultiPeriodTimeline_calledForPeriodsBeforeUnplayedAdGroup_throwsWhenCalledForNonAdPeriods() { + // Content live window with content and ad periods: c, [a, c, a, a, a, c, a], a, a + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 100_000_000, + /* nowUs= */ 159_000_123, + /* adSequencePattern= */ new boolean[] {false, true, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + AdPlaybackState adPlaybackState = + new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 50_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 1, /* totalAdDurationUs= */ AD_PERIOD_DURATION_US, /* totalAdsInAdPod= */ 1, adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 90_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 100_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 110_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 3, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + AdPlaybackState finalAdPlaybackState = adPlaybackState; + + Assert.assertThrows( + IllegalStateException.class, + () -> + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 1, + contentTimeline, + finalAdPlaybackState)); + adPlaybackState = adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2); adPlaybackState = addLiveAdBreak( - /* currentContentPeriodPositionUs= */ 130_000_000, + /* currentContentPeriodPositionUs= */ 150_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + AdPlaybackState anotherFinalAdPlaybackState = adPlaybackState; + + Assert.assertThrows( + IllegalStateException.class, + () -> + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 5, + contentTimeline, + anotherFinalAdPlaybackState)); + } + + @Test + public void + getAdGroupAndIndexInLiveMultiPeriodTimeline_partialAdGroupAtTimelineStart_correctAdGroupIndexAndAdIndexInAdGroup() { + // Content timeline with content and ad periods: c, a, a, [a, c, a, a, a, c] + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 100_000_000, + /* nowUs= */ 151_000_123, + /* adSequencePattern= */ new boolean[] {false, true, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + AdPlaybackState adPlaybackState = + new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 30_000_000, /* adDurationUs= */ AD_PERIOD_DURATION_US, /* adPositionInAdPod= */ 1, - /* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US, - /* totalAdsInAdPod= */ 2, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 40_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 50_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 3, + /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 3, + adPlaybackState); + adPlaybackState = + adPlaybackState + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1); + + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 0, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(0, 2)); + } + + @Test + public void + getAdGroupAndIndexInLiveMultiPeriodTimeline_onlyPartialAdGroupInWindow_correctAdGroupIndexAndAdIndexInAdGroup() { + // Content timeline with content and ad periods: c, a, [a, a, a, a], a, c + // First three ad periods of the ad group already outside of the live window. + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 30_000_000, + /* nowUs= */ 71_000_123, + /* adSequencePattern= */ new boolean[] {false, true, true, true, true, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + AdPlaybackState adPlaybackState = + new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + // Ad events of the first two ads of the group have arrived (the first of the window). + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 30_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 6, + adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 40_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 6, + adPlaybackState); + + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 0, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(0, 1)); + + // Ad event for second ad in window arrives. + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 50_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 3, + /* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 6, + adPlaybackState); + + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 1, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(0, 2)); + + // Move one ad period forward: c, a, a, [a, a, a, a], c + contentTimeline.advanceNowUs(10_000_000L); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2); + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 1, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(0, 3)); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 3); + assertThat( + getAdGroupAndIndexInLiveMultiPeriodTimeline( + /* currentMediaItemIndex= */ 0, + /* adPeriodIndex= */ 2, + contentTimeline, + adPlaybackState)) + .isEqualTo(new Pair<>(0, 4)); + } + + @Test + public void handleAdPeriodRemovedFromTimeline_removalCorrectlyHandled() { + // Content timeline with content and ad periods: a,[c, a, a, a, a, a, a, c], a + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 70_000_123, + /* nowUs= */ 189_453_123, + /* adSequencePattern= */ new boolean[] {false, true, true, true, true, true, true}, + /* isContentTimeline= */ false, + /* populateAds= */ true, + /* playedAds= */ false); + AdPlaybackState adPlaybackState = + new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + // Ad events of the first two ads of the group have arrived (the first of the window). + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 120_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 6, adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); adPlaybackState = addLiveAdBreak( /* currentContentPeriodPositionUs= */ 130_000_000, /* adDurationUs= */ AD_PERIOD_DURATION_US, /* adPositionInAdPod= */ 2, - /* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US, - /* totalAdsInAdPod= */ 2, + /* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 6, adPlaybackState); - AdPlaybackState finalAdPlaybackState = adPlaybackState; + + // Current period is content period before ad group. + AdPlaybackState adPlaybackState1 = + handleAdPeriodRemovedFromTimeline( + /* currentPeriodIndex= */ 0, contentTimeline, adPlaybackState); + assertThat(adPlaybackState1.adGroupCount).isEqualTo(2); + assertThat(adPlaybackState1.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly( + AD_STATE_PLAYED, + AD_STATE_AVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE) + .inOrder(); + assertThat(adPlaybackState1.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 40_000_000L, 0L, 0L, 0L) + .inOrder(); + // Current period is played ad. + AdPlaybackState adPlaybackState2 = + handleAdPeriodRemovedFromTimeline( + /* currentPeriodIndex= */ 1, contentTimeline, adPlaybackState); + assertThat(adPlaybackState2.adGroupCount).isEqualTo(2); + assertThat(adPlaybackState2.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly( + AD_STATE_PLAYED, + AD_STATE_AVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE) + .inOrder(); + assertThat(adPlaybackState2.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 40_000_000L, 0L, 0L, 0L) + .inOrder(); + // Current period is available ad. + AdPlaybackState adPlaybackState3 = + handleAdPeriodRemovedFromTimeline( + /* currentPeriodIndex= */ 2, contentTimeline, adPlaybackState); + assertThat(adPlaybackState3.adGroupCount).isEqualTo(2); + assertThat(adPlaybackState3.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly( + AD_STATE_PLAYED, + AD_STATE_AVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE) + .inOrder(); + assertThat(adPlaybackState3.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 40_000_000L, 0L, 0L, 0L) + .inOrder(); + // Current period is first unavailable ad. + AdPlaybackState adPlaybackState4 = + handleAdPeriodRemovedFromTimeline( + /* currentPeriodIndex= */ 3, contentTimeline, adPlaybackState); + assertThat(adPlaybackState4.adGroupCount).isEqualTo(2); + assertThat(adPlaybackState4.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly( + AD_STATE_PLAYED, + AD_STATE_PLAYED, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE, + AD_STATE_UNAVAILABLE) + .inOrder(); + assertThat(adPlaybackState4.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 30_000_000L, 0L, 0L) + .inOrder(); + // Current period is unavailable ad. Give up case. + AdPlaybackState adPlaybackState5 = + handleAdPeriodRemovedFromTimeline( + /* currentPeriodIndex= */ 4, contentTimeline, adPlaybackState); + assertThat(adPlaybackState5.adGroupCount).isEqualTo(3); + assertThat(adPlaybackState5.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly( + AD_STATE_PLAYED, + AD_STATE_PLAYED, + AD_STATE_PLAYED, + AD_STATE_PLAYED, + AD_STATE_PLAYED, + AD_STATE_PLAYED) + .inOrder(); + assertThat(adPlaybackState5.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(0L, 0L, 0L, 0L, 0L, 0L) + .inOrder(); + assertThat(adPlaybackState5.getAdGroup(/* adGroupIndex= */ 1).states) + .asList() + .containsExactly(AD_STATE_AVAILABLE); + assertThat(adPlaybackState5.getAdGroup(/* adGroupIndex= */ 1).durationsUs) + .asList() + .containsExactly(10_000_000L) + .inOrder(); + // Current period is after ad group. + AdPlaybackState adPlaybackState6 = + handleAdPeriodRemovedFromTimeline( + /* currentPeriodIndex= */ 7, contentTimeline, adPlaybackState); + assertThat(adPlaybackState6.adGroupCount).isEqualTo(2); + assertThat(adPlaybackState6.getAdGroup(/* adGroupIndex= */ 0).states) + .asList() + .containsExactly( + AD_STATE_PLAYED, + AD_STATE_PLAYED, + AD_STATE_PLAYED, + AD_STATE_PLAYED, + AD_STATE_PLAYED, + AD_STATE_PLAYED) + .inOrder(); + assertThat(adPlaybackState6.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 40_000_000L, 0L, 0L, 0L) + .inOrder(); + } + + @Test + public void getAdGroupDurationUsForLiveAdPeriodIndex_allAdsInTimeline_correctAdGroupDuration() { + int adPodTotalAdCount = 2; + // Content and ad periods in timeline: [c, a, a, c, a, a]. + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 75_007_123, + /* nowUs= */ 99_321_457, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ false, + /* populateAds= */ true, + /* playedAds= */ false); + AdPodInfo firstAdPodInfo = mock(AdPodInfo.class); + when(firstAdPodInfo.getAdPosition()).thenReturn(1); + when(firstAdPodInfo.getTotalAds()).thenReturn(adPodTotalAdCount); + when(firstAdPodInfo.getMaxDuration()).thenReturn(0.3D); + AdPodInfo secondAdPodInfo = mock(AdPodInfo.class); + when(secondAdPodInfo.getAdPosition()).thenReturn(2); + when(secondAdPodInfo.getTotalAds()).thenReturn(adPodTotalAdCount); + when(secondAdPodInfo.getMaxDuration()).thenReturn(0.3D); assertThat( - getAdGroupAndIndexInMultiPeriodWindow( - /* adPeriodIndex= */ 1, adPlaybackState, contentTimeline)) - .isEqualTo(new Pair<>(0, 0)); + ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex( + timeline, firstAdPodInfo, /* adPeriodIndex= */ 1, new Window(), new Period())) + .isEqualTo(2 * AD_PERIOD_DURATION_US); assertThat( - getAdGroupAndIndexInMultiPeriodWindow( - /* adPeriodIndex= */ 2, adPlaybackState, contentTimeline)) - .isEqualTo(new Pair<>(1, 0)); + ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex( + timeline, secondAdPodInfo, /* adPeriodIndex= */ 2, new Window(), new Period())) + .isEqualTo(2 * AD_PERIOD_DURATION_US); + // The second ad group has the last ad with an unknown duration. assertThat( - getAdGroupAndIndexInMultiPeriodWindow( - /* adPeriodIndex= */ 4, adPlaybackState, contentTimeline)) - .isEqualTo(new Pair<>(2, 0)); + ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex( + timeline, firstAdPodInfo, /* adPeriodIndex= */ 4, new Window(), new Period())) + .isEqualTo(secToUsRounded(0.3D)); assertThat( - getAdGroupAndIndexInMultiPeriodWindow( - /* adPeriodIndex= */ 5, adPlaybackState, contentTimeline)) - .isEqualTo(new Pair<>(2, 1)); - Assert.assertThrows( - IllegalStateException.class, - () -> - getAdGroupAndIndexInMultiPeriodWindow( - /* adPeriodIndex= */ 0, finalAdPlaybackState, contentTimeline)); + ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex( + timeline, secondAdPodInfo, /* adPeriodIndex= */ 5, new Window(), new Period())) + .isEqualTo(secToUsRounded(0.3D)); + } + + @Test + public void + getAdGroupDurationUsForLiveAdPeriodIndex_missingAdPeriodInTimeline_fallbackAdGroupDuration() { + int adPodTotalAdCount = 3; + // Content and ad periods in timeline: [a, a, c]. First two ads not in window. + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 49_321_753, + /* nowUs= */ 85_007_123, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ false, + /* populateAds= */ true, + /* playedAds= */ false); + AdPodInfo firstAdPodInfo = mock(AdPodInfo.class); + when(firstAdPodInfo.getAdPosition()).thenReturn(3); + when(firstAdPodInfo.getTotalAds()).thenReturn(adPodTotalAdCount); + when(firstAdPodInfo.getMaxDuration()).thenReturn(0.3D); + AdPodInfo secondAdPodInfo = mock(AdPodInfo.class); + when(secondAdPodInfo.getAdPosition()).thenReturn(4); + when(secondAdPodInfo.getTotalAds()).thenReturn(adPodTotalAdCount); + when(secondAdPodInfo.getMaxDuration()).thenReturn(0.3D); + + assertThat( + ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex( + timeline, firstAdPodInfo, /* adPeriodIndex= */ 0, new Window(), new Period())) + .isEqualTo(secToUsRounded(0.3D)); + assertThat( + ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex( + timeline, secondAdPodInfo, /* adPeriodIndex= */ 1, new Window(), new Period())) + .isEqualTo(secToUsRounded(0.3D)); } @Test diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimeline.java index 15ebc500ef2..7f85013faaf 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimeline.java @@ -70,7 +70,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { * availabilityStartTimeUs}. True is an ad period, and false a content period. * @param isContentTimeline Whether the timeline is a content timeline without {@link * AdPlaybackState}s. - * @param populateAds Whether to populate ads like after the ad event has been received. This + * @param populateAds Whether to populate ads in the same way if an ad event has been received. * @param playedAds Whether ads should be marked as played if populated. */ public FakeMultiPeriodLiveTimeline( @@ -131,6 +131,7 @@ public int getWindowCount() { @Override public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + checkArgument(windowIndex == 0); MediaItem.LiveConfiguration liveConfiguration = new MediaItem.LiveConfiguration.Builder().build(); window.set(