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");
+ }
+ }
+}