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 6703b02709..0a81774365 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 @@ -15,14 +15,19 @@ */ package androidx.media3.exoplayer.ima; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; + import android.content.Context; import android.os.Looper; import android.view.View; import android.view.ViewGroup; import androidx.annotation.Nullable; import androidx.media3.common.AdOverlayInfo; +import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AdViewProvider; import androidx.media3.common.C; +import androidx.media3.common.Timeline; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSchemeDataSource; import androidx.media3.datasource.DataSourceUtil; @@ -43,10 +48,13 @@ import com.google.ads.interactivemedia.v3.api.UiElement; import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; /** Utilities for working with IMA SDK and IMA extension data types. */ @@ -255,5 +263,125 @@ import java.util.Set; } } + /** + * Splits an {@link AdPlaybackState} into a separate {@link AdPlaybackState} for each period of a + * content timeline. Ad group times are expected to not take previous ad duration into account and + * needs to be translated to the actual position in the {@code contentTimeline} by adding prior ad + * durations. + * + *

If a period is enclosed by an ad group, the period is considered an ad period and gets an ad + * playback state assigned with a single ad in a single ad group. The duration of the ad is set to + * the duration of the period. All other periods are considered content periods with an empty ad + * playback state without any ads. + * + * @param adPlaybackState The ad playback state to be split. + * @param contentTimeline The content timeline for each period of which to create an {@link + * AdPlaybackState}. + * @return A map of ad playback states for each period UID in the content timeline. + */ + public static ImmutableMap splitAdPlaybackStateForPeriods( + AdPlaybackState adPlaybackState, Timeline contentTimeline) { + 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); + } + + int periodIndex = 0; + long totalElapsedContentDurationUs = 0; + Object adsId = checkNotNull(adPlaybackState.adsId); + AdPlaybackState contentOnlyAdPlaybackState = new AdPlaybackState(adsId); + Map adPlaybackStates = new HashMap<>(); + for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i); + if (adGroup.timeUs == C.TIME_END_OF_SOURCE) { + checkState(i == adPlaybackState.adGroupCount - 1); + // The last ad group is a placeholder for a potential post roll. We can just stop here. + break; + } + // The ad group start timeUs is in content position. We need to add the ad + // duration before the ad group to translate the start time to the position in the period. + long adGroupDurationUs = getTotalDurationUs(adGroup.durationsUs); + long elapsedAdGroupAdDurationUs = 0; + for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) { + contentTimeline.getPeriod(j, period, /* setIds= */ true); + if (totalElapsedContentDurationUs < adGroup.timeUs) { + // Period starts before the ad group, so it is a content period. + adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState); + totalElapsedContentDurationUs += period.durationUs; + } 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: An ad + // reported by the IMA SDK may span multiple periods). + adPlaybackStates.put( + checkNotNull(period.uid), + splitAdGroupForPeriod(adsId, adGroup, periodStartUs, period.durationUs)); + elapsedAdGroupAdDurationUs += period.durationUs; + } 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. + for (int i = periodIndex; i < contentTimeline.getPeriodCount(); i++) { + contentTimeline.getPeriod(i, period, /* setIds= */ true); + adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState); + } + return ImmutableMap.copyOf(adPlaybackStates); + } + + private static AdPlaybackState splitAdGroupForPeriod( + Object adsId, AdPlaybackState.AdGroup adGroup, long periodStartUs, long periodDurationUs) { + checkState(adGroup.timeUs <= periodStartUs); + AdPlaybackState adPlaybackState = + new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + long periodEndUs = periodStartUs + periodDurationUs; + long adDurationsUs = 0; + for (int i = 0; i < adGroup.count; i++) { + adDurationsUs += adGroup.durationsUs[i]; + if (periodEndUs == adGroup.timeUs + adDurationsUs) { + // Map the state of the global ad state to the period specific ad state. + switch (adGroup.states[i]) { + case AdPlaybackState.AD_STATE_PLAYED: + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + break; + case AdPlaybackState.AD_STATE_SKIPPED: + adPlaybackState = + adPlaybackState.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + break; + case AdPlaybackState.AD_STATE_ERROR: + adPlaybackState = + adPlaybackState.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + break; + default: + // Do nothing. + break; + } + break; + } + } + return adPlaybackState; + } + + private static long getTotalDurationUs(long[] durationsUs) { + long totalDurationUs = 0; + for (long adDurationUs : durationsUs) { + totalDurationUs += adDurationUs; + } + return totalDurationUs; + } + private ImaUtil() {} } 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 new file mode 100644 index 0000000000..5e723a914a --- /dev/null +++ b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaUtilTest.java @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.ima; + +import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US; +import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; +import static com.google.common.truth.Truth.assertThat; + +import android.util.Pair; +import androidx.media3.common.AdPlaybackState; +import androidx.media3.common.C; +import androidx.media3.common.Timeline; +import androidx.media3.test.utils.FakeTimeline; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableMap; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link ImaUtil}. */ +@RunWith(AndroidJUnit4.class) +public class ImaUtilTest { + + @Test + public void splitAdPlaybackStateForPeriods_emptyTimeline_emptyMapOfAdPlaybackStates() { + 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); + } + + @Test + public void splitAdPlaybackStateForPeriods_livePlaceholder_isIgnored() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "", C.TIME_END_OF_SOURCE) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline singlePeriodTimeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, singlePeriodTimeline); + + assertThat(adPlaybackStates).hasSize(3); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_noAds_splitToEmptyAdPlaybackStates() { + AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ "adsId"); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 11, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(11); + for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) { + assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId"); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_twoPrerollAds_splitToFirstTwoPeriods() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs( + /* adGroupIndex= */ 0, + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs, + periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + for (int i = 0; i < 2; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId"); + assertThat(periodAdPlaybackState.getAdGroup(0).timeUs).isEqualTo(0); + assertThat(periodAdPlaybackState.getAdGroup(0).isServerSideInserted).isTrue(); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + int adDurationUs = i == 0 ? 125_500_000 : 2_500_000; + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(adDurationUs); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_onePrerollAdGroup_splitToFirstThreePeriods() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs( + /* adGroupIndex= */ 0, + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 3 * periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + for (int i = 0; i < 3; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + int adDurationUs = i == 0 ? 125_500_000 : 2_500_000; + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(adDurationUs); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_twoMidrollAds_splitToMiddleTwoPeriods() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + for (int i = 1; i < 3; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).timeUs).isEqualTo(0); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_500_000); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_oneMidrollAdGroupOneAd_adSpansTwoPeriods() { + int periodCount = 5; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 2 * periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + for (int i = 1; i < 3; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_000_000); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_oneMidrollAdGroupTwoAds_eachAdSplitsToOnePeriod() { + int periodCount = 5; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + for (int i = 1; i < 3; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_000_000); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_twoPostrollAds_splitToLastTwoPeriods() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 2 * periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0); + for (int i = 2; i < periodCount; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_500_000); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_onePostrollAdGroup_splitToLastThreePeriods() { + int periodCount = 7; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 4 * periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 3 * periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + for (int i = 4; i < adPlaybackStates.size(); i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(periodDurationUs); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_preMidAndPostrollAdGroup_splitCorrectly() { + int periodCount = 11; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", 0, (2 * periodDurationUs), (5 * periodDurationUs)) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs( + /* adGroupIndex= */ 0, + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs, + periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true) + .withAdCount(/* adGroupIndex= */ 1, 2) + .withAdDurationsUs(/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 1, true) + .withAdCount(/* adGroupIndex= */ 2, 2) + .withAdDurationsUs(/* adGroupIndex= */ 2, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 2, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 8)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 9)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 10)).adGroupCount).isEqualTo(1); + } + + @Test + public void splitAdPlaybackStateForPeriods_midAndPostrollAdGroup_splitCorrectly() { + int periodCount = 9; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + (2 * periodDurationUs), + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + (5 * periodDurationUs)) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true) + .withAdCount(/* adGroupIndex= */ 1, 2) + .withAdDurationsUs(/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 1, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 8)).adGroupCount).isEqualTo(1); + } + + @Test + public void splitAdPlaybackStateForPeriods_correctAdsIdInSplitPlaybackStates() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 2 * periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 2 * periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + for (int i = 0; i < adPlaybackStates.size(); i++) { + assertThat(adPlaybackStates.get(new Pair<>(0L, i)).adsId).isEqualTo("adsId"); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_correctAdPlaybackStates() { + int periodCount = 7; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", 0) + .withAdCount(/* adGroupIndex= */ 0, periodCount) + .withAdDurationsUs( + /* adGroupIndex= */ 0, /* adDurationsUs...= */ + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs, + periodDurationUs, + periodDurationUs, + periodDurationUs, + periodDurationUs, + periodDurationUs, + periodDurationUs) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1) + .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).getAdGroup(/* adGroupIndex= */ 0).states[0]) + .isEqualTo(AdPlaybackState.AD_STATE_PLAYED); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).getAdGroup(/* adGroupIndex= */ 0).states[0]) + .isEqualTo(AdPlaybackState.AD_STATE_SKIPPED); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).getAdGroup(/* adGroupIndex= */ 0).states[0]) + .isEqualTo(AdPlaybackState.AD_STATE_ERROR); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).getAdGroup(/* adGroupIndex= */ 0).states[0]) + .isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE); + } + + @Test + public void splitAdPlaybackStateForPeriods_lateMidrollAdGroupStartTimeUs_adGroupIgnored() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs + 1) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) { + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_earlyMidrollAdGroupStartTimeUs_adGroupIgnored() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", periodDurationUs - 1) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) { + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0); + assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId"); + } + } +}