diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index d418768885..c1e8711b03 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -678,12 +678,24 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou @MainThread private void invalidateServerSideAdInsertionAdPlaybackState() { if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) { - ImmutableMap splitAdPlaybackStates = - splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline); + ImmutableMap splitAdPlaybackStates; + if (streamRequest.getFormat() == StreamRequest.StreamFormat.DASH) { + // DASH ad groups are always split by period. + splitAdPlaybackStates = splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline); + } else { + // The HLS single period timeline for VOD and live must not be split. + int firstPeriodIndex = + contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).firstPeriodIndex; + Object periodUid = + checkNotNull( + contentTimeline.getPeriod( + firstPeriodIndex, new Timeline.Period(), /* setIds= */ true) + .uid); + splitAdPlaybackStates = ImmutableMap.of(periodUid, adPlaybackState); + } streamPlayer.setAdPlaybackStates(adsId, splitAdPlaybackStates, contentTimeline); checkNotNull(serverSideAdInsertionMediaSource).setAdPlaybackStates(splitAdPlaybackStates); - if (!ImaServerSideAdInsertionUriBuilder.isLiveStream( - checkNotNull(mediaItem.localConfiguration).uri)) { + if (!isLiveStream) { adsLoader.setAdPlaybackState(adsId, adPlaybackState); } } diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java index bd19af60f2..b5e842be4e 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java @@ -20,10 +20,12 @@ import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.common.util.Util.sum; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getMediaPeriodPositionUsForContent; import static java.lang.Math.max; +import static java.lang.Math.min; import android.content.Context; import android.os.Looper; @@ -370,6 +372,19 @@ import java.util.Set; return adDurationsUs; } + /** + * Gets the window start in microseconds since the Unix epoch for a window of a {@linkplain + * Timeline timeline} of the {@code DashMediaSource}. + * + * @param windowStartTimeMs The window start time, in milliseconds. + * @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) { + // Revert us/ms truncation introduced in `DashMediaSource.DashTimeline`. + return msToUs(windowStartTimeMs) + (positionInFirstPeriodUs % 1000); + } + /** * Splits an {@link AdPlaybackState} into a separate {@link AdPlaybackState} for each period of a * content timeline. @@ -387,19 +402,19 @@ import java.util.Set; */ public static ImmutableMap splitAdPlaybackStateForPeriods( AdPlaybackState adPlaybackState, Timeline contentTimeline) { + checkArgument(!contentTimeline.isEmpty()); Timeline.Period period = new Timeline.Period(); - if (contentTimeline.getPeriodCount() == 1) { - // A single period gets the entire ad playback state that may contain multiple ad groups. - return ImmutableMap.of( - checkNotNull( - contentTimeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid), - adPlaybackState); - } - + Timeline.Window window = contentTimeline.getWindow(0, new Timeline.Window()); int periodIndex = 0; long totalElapsedContentDurationUs = 0; Object adsId = checkNotNull(adPlaybackState.adsId); AdPlaybackState contentOnlyAdPlaybackState = new AdPlaybackState(adsId); + if (window.isLive()) { + long windowStartTimeUs = + getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs); + totalElapsedContentDurationUs = windowStartTimeUs - window.positionInFirstPeriodUs; + contentOnlyAdPlaybackState = contentOnlyAdPlaybackState.withLivePostrollPlaceholderAppended(); + } Map adPlaybackStates = new HashMap<>(); for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i); @@ -418,22 +433,42 @@ import java.util.Set; // Period starts before the ad group, so it is a content period. adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState); totalElapsedContentDurationUs += period.durationUs; + // Current period added as a content period. Advance and look at the next period. + periodIndex++; } else { long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs; - if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) { - // The period ends before the end of the ad group, so it is an ad period (Note: A VOD ad - // reported by the IMA SDK spans multiple periods before the LOADED event arrives). + long periodDurationUs = period.durationUs; + if ((periodDurationUs != C.TIME_UNSET + && periodStartUs + periodDurationUs <= adGroup.timeUs + adGroupDurationUs) + || (periodDurationUs == C.TIME_UNSET + && elapsedAdGroupAdDurationUs < adGroupDurationUs + && periodStartUs < adGroup.timeUs + adGroupDurationUs)) { + // Ad period found. The period either ends before the end of the ad group, or it is the + // last period of a live stream and it starts in the ad group. adPlaybackStates.put( checkNotNull(period.uid), - splitAdGroupForPeriod(adsId, adGroup, periodStartUs, period.durationUs)); - elapsedAdGroupAdDurationUs += period.durationUs; + splitAdGroupForPeriod( + adsId, adGroup, periodStartUs, periodDurationUs, window.isLive())); + // Current period added as an ad period. Advance and look at the next period. + periodIndex++; + elapsedAdGroupAdDurationUs += periodDurationUs; + if (periodStartUs + periodDurationUs == adGroup.timeUs + adGroupDurationUs) { + // Periods have consumed the ad group. We're at the end of the ad group. + if (window.isLive()) { + // Add elapsed ad duration to elapsed content duration for live streams to account + // for the content resume offset (relevant because we above compare against + // `adGroup.timeUs`). Instead of `adGroup.contentResumeOffsetUs` we use + // `elapsedAdGroupAdDurationUs` that is the sum of the actual period durations. + totalElapsedContentDurationUs += elapsedAdGroupAdDurationUs; + } + // Continue with next ad group. + break; + } } else { // Period is after the current ad group. Continue with next ad group. break; } } - // Increment the period index to the next unclassified period. - periodIndex++; } } // The remaining periods end after the last ad group, so these are content periods. @@ -445,18 +480,32 @@ import java.util.Set; } private static AdPlaybackState splitAdGroupForPeriod( - Object adsId, AdGroup adGroup, long periodStartUs, long periodDurationUs) { + Object adsId, + AdGroup adGroup, + long periodStartUs, + long periodDurationUs, + boolean isLiveStream) { AdPlaybackState adPlaybackState = new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0) - .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) - .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs) .withIsServerSideInserted(/* adGroupIndex= */ 0, true) - .withContentResumeOffsetUs(/* adGroupIndex= */ 0, adGroup.contentResumeOffsetUs); - long periodEndUs = periodStartUs + periodDurationUs; - long adDurationsUs = 0; + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1); + if (isLiveStream) { + adPlaybackState = adPlaybackState.withLivePostrollPlaceholderAppended(); + } + long adGroupDurationUs = 0; for (int i = 0; i < adGroup.count; i++) { - adDurationsUs += adGroup.durationsUs[i]; - if (periodEndUs <= adGroup.timeUs + adDurationsUs + 10_000) { + long sanitizedDurationUs = + periodDurationUs != C.TIME_UNSET ? periodDurationUs : adGroup.durationsUs[i]; + long periodEndUs = periodStartUs + sanitizedDurationUs; + adGroupDurationUs += adGroup.durationsUs[i]; + // TODO(bachinger): Remove margin constant by making sure the VOD ad group times are adjusted + // to the actual DASH timeline periods. + if (periodEndUs <= adGroup.timeUs + adGroupDurationUs + 10_000) { + adPlaybackState = + adPlaybackState + .withAdDurationsUs(/* adGroupIndex= */ 0, sanitizedDurationUs) + .withContentResumeOffsetUs( + /* adGroupIndex= */ 0, isLiveStream ? sanitizedDurationUs : 0); // Map the state of the global ad state to the period specific ad state. switch (adGroup.states[i]) { case AdPlaybackState.AD_STATE_PLAYED: @@ -495,6 +544,12 @@ import java.util.Set; Timeline.Period period = new Timeline.Period(); 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; + } for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { int adIndexInAdGroup = 0; AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i); @@ -516,6 +571,8 @@ import java.util.Set; adIndexInAdGroup++; } else { // Period is after the current ad group. Continue with next ad group. + totalElapsedContentDurationUs += + min(elapsedAdGroupAdDurationUs, adGroup.contentResumeOffsetUs); break; } } diff --git a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaUtilTest.java b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaUtilTest.java index 8dfeb785ba..9e726f4146 100644 --- a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaUtilTest.java +++ b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaUtilTest.java @@ -23,6 +23,7 @@ import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE; import static androidx.media3.exoplayer.ima.ImaUtil.addLiveAdBreak; import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow; import static androidx.media3.exoplayer.ima.ImaUtil.splitAdGroup; +import static androidx.media3.test.utils.FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_US; import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US; import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; import static com.google.common.truth.Truth.assertThat; @@ -31,7 +32,10 @@ import android.util.Pair; import androidx.media3.common.AdPlaybackState; import androidx.media3.common.C; import androidx.media3.common.Timeline; +import androidx.media3.common.Timeline.Period; +import androidx.media3.common.Timeline.Window; import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil; +import androidx.media3.test.utils.FakeMultiPeriodLiveTimeline; import androidx.media3.test.utils.FakeTimeline; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableMap; @@ -44,29 +48,13 @@ import org.junit.runner.RunWith; public class ImaUtilTest { @Test - public void splitAdPlaybackStateForPeriods_emptyTimeline_emptyMapOfAdPlaybackStates() { + public void splitAdPlaybackStateForPeriods_emptyTimeline_throwsIllegalArgumentException() { AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE); - ImmutableMap adPlaybackStates = - ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, Timeline.EMPTY); - - assertThat(adPlaybackStates).isEmpty(); - } - - @Test - public void splitAdPlaybackStateForPeriods_singlePeriod_doesNotSplit() { - AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE); - FakeTimeline singlePeriodTimeline = - new FakeTimeline( - new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0L)); - - ImmutableMap adPlaybackStates = - ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, singlePeriodTimeline); - - assertThat(adPlaybackStates).hasSize(1); - assertThat(adPlaybackStates).containsEntry(new Pair<>(0L, 0), adPlaybackState); + Assert.assertThrows( + IllegalArgumentException.class, + () -> ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, Timeline.EMPTY)); } @Test @@ -562,6 +550,453 @@ public class ImaUtilTest { } } + @Test + public void + splitAdPlaybackStateForPeriods_liveAdGroupStartedAndMovedOutOfWindow_splitCorrectly() { + long adPeriodDurationUs = AD_PERIOD_DURATION_US; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", C.TIME_END_OF_SOURCE) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + // First window start time (UNIX epoch): 50_000_000 + // Period durations: content=30_000_000, ad=10_000_000 + FakeMultiPeriodLiveTimeline liveTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 100_000_000, + /* nowUs= */ 150_000_000, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false); + // Ad event received from SDK around 130s. + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 130_000_000, + adPeriodDurationUs, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 2 * adPeriodDurationUs, + /* totalAdsInAdPod= */ 2, + adPlaybackState); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(6); + assertThat(adPlaybackStates).hasSize(6); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact. + assertThat(adPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + + // Move 1us forward to include the first us of the next period. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(7); + assertThat(adPlaybackStates).hasSize(7); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L); + assertThat(adPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + + // Move 29_999_999us forward to the last us of the first content period. + liveTimeline.advanceNowUs(/* durationUs= */ 29_999_998L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(7); + assertThat(adPlaybackStates).hasSize(7); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(79_999L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-29_999_999L); + assertThat(adPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + + // Move 1us forward to the drop the first content period at the beginning of the window. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(6); + assertThat(adPlaybackStates).hasSize(6); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(80_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact. + assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + + // Move 1us forward to add the next ad period at the end of the window. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(7); + assertThat(adPlaybackStates).hasSize(7); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(80_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L); + assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(1); + + // Mark previous ad group as played. + Pair adGroupAndAdIndex = + ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( + /* adPeriodIndex= */ 3, adPlaybackState, liveTimeline); + adPlaybackState = + adPlaybackState.withPlayedAd( + /* adGroupIndex= */ adGroupAndAdIndex.first, + /* adIndexInAdGroup= */ adGroupAndAdIndex.second); + adGroupAndAdIndex = + ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( + /* adPeriodIndex= */ 4, adPlaybackState, liveTimeline); + adPlaybackState = + adPlaybackState.withPlayedAd( + /* adGroupIndex= */ adGroupAndAdIndex.first, + /* adIndexInAdGroup= */ adGroupAndAdIndex.second); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + AdPlaybackState.AdGroup adGroup = + adPlaybackStates.get("uid-7[a]").getAdGroup(/* adGroupIndex= */ 0); + assertThat(adGroup.getFirstAdIndexToPlay()).isEqualTo(0); + assertThat(adGroup.states[0]).isEqualTo(AD_STATE_PLAYED); + adGroup = adPlaybackStates.get("uid-8[a]").getAdGroup(/* adGroupIndex= */ 0); + assertThat(adGroup.getFirstAdIndexToPlay()).isEqualTo(0); + assertThat(adGroup.states[0]).isEqualTo(AD_STATE_PLAYED); + + // Move 9_999_998us forward to the last us of the first ad period. Same periods, shifted. + liveTimeline.advanceNowUs(/* durationUs= */ 9_999_998L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(7); + assertThat(adPlaybackStates).hasSize(7); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(89_999L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-9_999_999L); + assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(1); + + // Ad event received from SDK around 180s. + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 180_000_000, + adPeriodDurationUs, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 2 * adPeriodDurationUs, + /* totalAdsInAdPod= */ 2, + adPlaybackState); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(7); + assertThat(adPlaybackStates).hasSize(7); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(89_999L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-9_999_999L); + assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2); + + // Move 1us forward to drop the first ad from the beginning of the window. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(6); + assertThat(adPlaybackStates).hasSize(6); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(90_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact. + assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2); + + // Move 1us forward to add the next ad period at the end of the window. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(7); + assertThat(adPlaybackStates).hasSize(7); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(90_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L); + assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2); + + // Move 39_999_999us to drop an ad and a content period at the beginning of the window. + liveTimeline.advanceNowUs(/* durationUs= */ 39_999_999L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(6); + assertThat(adPlaybackStates).hasSize(6); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(130_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact. + assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-12[c]").adGroupCount).isEqualTo(1); + + // Move 10_000_000us to drop an ad (incomplete ad group at the beginning of the window). + liveTimeline.advanceNowUs(/* durationUs= */ 10_000_000L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(6); + assertThat(adPlaybackStates).hasSize(6); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(140_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact. + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-12[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-13[a]").adGroupCount).isEqualTo(1); + + // Mark previous ad group as played. + adGroupAndAdIndex = + ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( + /* adPeriodIndex= */ 2, adPlaybackState, liveTimeline); + adPlaybackState = + adPlaybackState.withPlayedAd( + /* adGroupIndex= */ adGroupAndAdIndex.first, + /* adIndexInAdGroup= */ adGroupAndAdIndex.second); + adGroupAndAdIndex = + ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( + /* adPeriodIndex= */ 3, adPlaybackState, liveTimeline); + adPlaybackState = + adPlaybackState.withPlayedAd( + /* adGroupIndex= */ adGroupAndAdIndex.first, + /* adIndexInAdGroup= */ adGroupAndAdIndex.second); + // Ad event received from SDK around 230s for ad period with unknown duration. + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 230_000_000, + adPeriodDurationUs - 1000L, // SDK fallback duration. + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 2 * adPeriodDurationUs - 1000, + /* totalAdsInAdPod= */ 2, + adPlaybackState); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(6); + assertThat(adPlaybackStates).hasSize(6); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(140_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact. + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-12[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-13[a]").adGroupCount).isEqualTo(2); + AdPlaybackState.AdGroup actualAdGroup = + adPlaybackStates.get("uid-13[a]").getAdGroup(/* adGroupIndex= */ 0); + assertThat(actualAdGroup.count).isEqualTo(1); + assertThat(actualAdGroup.durationsUs[0]).isEqualTo(adPeriodDurationUs - 1000L); + + // Move 1us forward to add the next ad period at the end of the window. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(7); + assertThat(adPlaybackStates).hasSize(7); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(140_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L); + assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2); + assertThat(adPlaybackStates.get("uid-12[c]").adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get("uid-13[a]").adGroupCount).isEqualTo(2); + actualAdGroup = adPlaybackStates.get("uid-13[a]").getAdGroup(/* adGroupIndex= */ 0); + assertThat(actualAdGroup.count).isEqualTo(1); + assertThat(actualAdGroup.durationsUs[0]).isEqualTo(adPeriodDurationUs); + assertThat(adPlaybackStates.get("uid-14[a]").adGroupCount).isEqualTo(2); + } + + @Test + public void + splitAdPlaybackStateForPeriods_fullAdGroupAtBeginOfWindow_adPeriodsCorrectlyDetected() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", C.TIME_END_OF_SOURCE) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + // Window start time (UNIX epoch): 29_999_999 + // Period durations: content=30_000_000, ad=10_000_000 + FakeMultiPeriodLiveTimeline liveTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 30_000_000, + /* nowUs= */ 59_999_999, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false); + // Ad event received from SDK around 30s. + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 30_000_000, + AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 2, + adPlaybackState); + + ImmutableMap splitAdPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(4); + assertThat(splitAdPlaybackStates).hasSize(4); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(29_999L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-29_999_999L); + assertThat(splitAdPlaybackStates.get("uid-0[c]").adGroupCount).isEqualTo(1); + assertThat(splitAdPlaybackStates.get("uid-1[a]").adGroupCount).isEqualTo(2); + assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2); + assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + + // Move window start to the first microsecond of the first ad period. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(3); + assertThat(splitAdPlaybackStates).hasSize(3); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(30_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); + assertThat(splitAdPlaybackStates.get("uid-1[a]").adGroupCount).isEqualTo(2); + assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2); + assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + + // Move window start to the last microsecond of the first ad period. + liveTimeline.advanceNowUs(/* durationUs= */ 9_999_999L); + splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(3); + assertThat(splitAdPlaybackStates).hasSize(3); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(39_999L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-9_999_999L); + assertThat(splitAdPlaybackStates.get("uid-1[a]").adGroupCount).isEqualTo(2); + assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2); + assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + + // Mark previous ad group as played. + adPlaybackState = + adPlaybackState + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1); + + // Move first ad period out of live window. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(2); + assertThat(splitAdPlaybackStates).hasSize(2); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(40_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); + assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2); + assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + + // Move window start to the last microsecond of the second ad period. Same periods, shifted. + liveTimeline.advanceNowUs(/* durationUs= */ 9_999_999L); + splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(2); + assertThat(splitAdPlaybackStates).hasSize(2); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L - 1L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-9_999_999L); + assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2); + assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + + // Move second ad period out of live window. Only a single content period in the window. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(1); + assertThat(splitAdPlaybackStates).hasSize(1); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0); + assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + + // Move window start 1 microsecond to require 1us microsecond of the next period. + // Note: The ad period is now the last in the window with a duration of TIME_UNSET. Also, the ad + // playback state doesn't know yet that the period is an ad. + liveTimeline.advanceNowUs(/* durationUs= */ 1L); + splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(2); + assertThat(splitAdPlaybackStates).hasSize(2); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L); + // TODO(bachinger): Rounding inaccuracies of 1us because windowStartTimeMs is in milliseconds. + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L); + assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + assertThat(splitAdPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1); + + // The ad break arrives that tells the ad playback state about the ad in the timeline. We assert + // that the same timeline now gets the period marked as an ad expected. + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 80_000_000, + AD_PERIOD_DURATION_US - 1000L, // SDK fallback duration. + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US - 1001L, + /* totalAdsInAdPod= */ 2, + adPlaybackState); + splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(2); + assertThat(splitAdPlaybackStates).hasSize(2); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L); + assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + assertThat(splitAdPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(2); + AdPlaybackState.AdGroup actualAdGroup = + splitAdPlaybackStates.get("uid-4[a]").getAdGroup(/* adGroupIndex= */ 0); + assertThat(actualAdGroup.count).isEqualTo(1); + assertThat(actualAdGroup.durationsUs[0]).isEqualTo(AD_PERIOD_DURATION_US - 1000L); + + // Advance to make the window overlap 1 microsecond into the second ad period. Assert whether + // both ad periods, including the last with unknown duration, are correctly marked as ad. + liveTimeline.advanceNowUs(10_000_000L); + splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline); + + assertThat(liveTimeline.getPeriodCount()).isEqualTo(3); + assertThat(splitAdPlaybackStates).hasSize(3); + assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(60_000L); + assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-10_000_001L); + assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1); + assertThat(splitAdPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(2); + actualAdGroup = splitAdPlaybackStates.get("uid-4[a]").getAdGroup(/* adGroupIndex= */ 0); + assertThat(actualAdGroup.count).isEqualTo(1); + assertThat(actualAdGroup.durationsUs[0]).isEqualTo(AD_PERIOD_DURATION_US); + assertThat(splitAdPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(2); + actualAdGroup = splitAdPlaybackStates.get("uid-5[a]").getAdGroup(/* adGroupIndex= */ 0); + assertThat(actualAdGroup.count).isEqualTo(1); + assertThat(actualAdGroup.durationsUs[0]).isEqualTo(AD_PERIOD_DURATION_US - 1L); // SDK fallback. + } + @Test public void expandAdGroupPlaceHolder_expandWithFirstAdInGroup_correctExpansion() { AdPlaybackState adPlaybackState = @@ -851,6 +1286,80 @@ public class ImaUtilTest { assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1); } + @Test + public void + getAdGroupAndIndexInMultiPeriodWindow_liveWindow_correctAdGroupIndexAndAdIndexInAdGroup() { + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 100_000_000, + /* nowUs= */ 150_000_000, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false); + AdPlaybackState adPlaybackState = + new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 80_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= */ AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 1, + adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 130_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 2, + adPlaybackState); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 130_000_000, + /* adDurationUs= */ AD_PERIOD_DURATION_US, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US, + /* totalAdsInAdPod= */ 2, + adPlaybackState); + AdPlaybackState finalAdPlaybackState = adPlaybackState; + + assertThat( + getAdGroupAndIndexInMultiPeriodWindow( + /* adPeriodIndex= */ 1, adPlaybackState, contentTimeline)) + .isEqualTo(new Pair<>(0, 0)); + assertThat( + getAdGroupAndIndexInMultiPeriodWindow( + /* adPeriodIndex= */ 2, adPlaybackState, contentTimeline)) + .isEqualTo(new Pair<>(1, 0)); + assertThat( + getAdGroupAndIndexInMultiPeriodWindow( + /* adPeriodIndex= */ 4, adPlaybackState, contentTimeline)) + .isEqualTo(new Pair<>(2, 0)); + assertThat( + getAdGroupAndIndexInMultiPeriodWindow( + /* adPeriodIndex= */ 5, adPlaybackState, contentTimeline)) + .isEqualTo(new Pair<>(2, 1)); + Assert.assertThrows( + IllegalStateException.class, + () -> + getAdGroupAndIndexInMultiPeriodWindow( + /* adPeriodIndex= */ 0, finalAdPlaybackState, contentTimeline)); + } + @Test public void addLiveAdBreak_threeAdsHappyPath_createsNewAdGroupAndPropagates() { AdPlaybackState adPlaybackState =