diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java index e6cbac8716..b35b1c8b4c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.ext.ima; +import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Util.sum; +import static java.lang.Math.max; import android.content.Context; import android.os.Looper; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdError; @@ -263,6 +267,92 @@ import java.util.Set; } } + /** + * Expands a placeholder ad group with a single ad to the requested number of ads and sets the + * duration of the inserted ad. + * + *

The remaining ad group duration is propagated to the ad following the inserted ad. If the + * inserted ad is the last ad, the remaining ad group duration is wrapped around to the first ad + * in the group. + * + * @param adGroupIndex The ad group index of the ad group to expand. + * @param adIndexInAdGroup The ad index to set the duration. + * @param adDurationUs The duration of the ad. + * @param adGroupDurationUs The duration of the whole ad group. + * @param adsInAdGroupCount The number of ads of the ad group. + * @param adPlaybackState The ad playback state to modify. + * @return The updated ad playback state. + */ + @CheckResult + public static AdPlaybackState expandAdGroupPlaceholder( + int adGroupIndex, + long adGroupDurationUs, + int adIndexInAdGroup, + long adDurationUs, + int adsInAdGroupCount, + AdPlaybackState adPlaybackState) { + checkArgument(adIndexInAdGroup < adsInAdGroupCount); + long[] adDurationsUs = + updateAdDurationAndPropagate( + new long[adsInAdGroupCount], adIndexInAdGroup, adDurationUs, adGroupDurationUs); + return adPlaybackState + .withAdCount(adGroupIndex, adDurationsUs.length) + .withAdDurationsUs(adGroupIndex, adDurationsUs); + } + + /** + * Updates the duration of an ad in and ad group. + * + *

The difference of the previous duration and the updated duration is propagated to the ad + * following the updated ad. If the updated ad is the last ad, the remaining duration is wrapped + * around to the first ad in the group. + * + *

The remaining ad duration is only propagated if the destination ad has a duration of 0. + * + * @param adGroupIndex The ad group index of the ad group to expand. + * @param adIndexInAdGroup The ad index to set the duration. + * @param adDurationUs The duration of the ad. + * @param adPlaybackState The ad playback state to modify. + * @return The updated ad playback state. + */ + @CheckResult + public static AdPlaybackState updateAdDurationInAdGroup( + int adGroupIndex, int adIndexInAdGroup, long adDurationUs, AdPlaybackState adPlaybackState) { + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); + checkArgument(adIndexInAdGroup < adGroup.durationsUs.length); + long[] adDurationsUs = + updateAdDurationAndPropagate( + Arrays.copyOf(adGroup.durationsUs, adGroup.durationsUs.length), + adIndexInAdGroup, + adDurationUs, + adGroup.durationsUs[adIndexInAdGroup]); + return adPlaybackState.withAdDurationsUs(adGroupIndex, adDurationsUs); + } + + /** + * Updates the duration of the given ad in the array and propagates the difference to the total + * duration to the next ad. If the updated ad is the last ad, the remaining duration is wrapped + * around to the first ad in the group. + * + *

The remaining ad duration is only propagated if the destination ad has a duration of 0. + * + * @param adDurationsUs The array to edit. + * @param adIndex The index of the ad in the durations array. + * @param adDurationUs The new ad duration. + * @param totalDurationUs The total duration the difference of which to propagate to the next ad. + * @return The updated input array, for convenience. + */ + /* package */ static long[] updateAdDurationAndPropagate( + long[] adDurationsUs, int adIndex, long adDurationUs, long totalDurationUs) { + adDurationsUs[adIndex] = adDurationUs; + int nextAdIndex = (adIndex + 1) % adDurationsUs.length; + if (adDurationsUs[nextAdIndex] == 0) { + // Propagate the remaining duration to the next ad. + adDurationsUs[nextAdIndex] = max(0, totalDurationUs - adDurationUs); + } + return adDurationsUs; + } + /** * 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 @@ -304,7 +394,7 @@ import java.util.Set; } // 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 adGroupDurationUs = sum(adGroup.durationsUs); long elapsedAdGroupAdDurationUs = 0; for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) { contentTimeline.getPeriod(j, period, /* setIds= */ true); @@ -375,13 +465,5 @@ import java.util.Set; return adPlaybackState; } - private static long getTotalDurationUs(long[] durationsUs) { - long totalDurationUs = 0; - for (long adDurationUs : durationsUs) { - totalDurationUs += adDurationUs; - } - return totalDurationUs; - } - private ImaUtil() {} } diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java index a8a6091327..aaf962a195 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java @@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.common.collect.ImmutableMap; import org.junit.Test; @@ -503,4 +504,224 @@ public class ImaUtilTest { assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId"); } } + + @Test + public void expandAdGroupPlaceHolder_expandWithFirstAdInGroup_correctExpansion() { + AdPlaybackState adPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + AdPlaybackState.NONE, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 30_000_000); + + adPlaybackState = + ImaUtil.expandAdGroupPlaceholder( + /* adGroupIndex= */ 0, + /* adGroupDurationUs= */ 30_000_000, + /* adIndexInAdGroup= */ 0, + /* adDurationUs= */ 10_000_000, + /* adsInAdGroupCount= */ 3, + adPlaybackState); + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ 0); + assertThat(adGroup.count).isEqualTo(3); + assertThat(adGroup.durationsUs[0]).isEqualTo(10_000_000); + assertThat(adGroup.durationsUs[1]).isEqualTo(20_000_000); + assertThat(adGroup.durationsUs[2]).isEqualTo(0); + } + + @Test + public void expandAdGroupPlaceHolder_expandWithMiddleAdInGroup_correctExpansion() { + AdPlaybackState adPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + AdPlaybackState.NONE, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 30_000_000); + + adPlaybackState = + ImaUtil.expandAdGroupPlaceholder( + /* adGroupIndex= */ 0, + /* adGroupDurationUs= */ 30_000_000, + /* adIndexInAdGroup= */ 1, + /* adDurationUs= */ 10_000_000, + /* adsInAdGroupCount= */ 3, + adPlaybackState); + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ 0); + assertThat(adGroup.count).isEqualTo(3); + assertThat(adGroup.durationsUs[0]).isEqualTo(0); + assertThat(adGroup.durationsUs[1]).isEqualTo(10_000_000); + assertThat(adGroup.durationsUs[2]).isEqualTo(20_000_000); + } + + @Test + public void expandAdGroupPlaceHolder_expandWithLastAdInGroup_correctDurationWrappedAround() { + AdPlaybackState adPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + AdPlaybackState.NONE, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 30_000_000); + + adPlaybackState = + ImaUtil.expandAdGroupPlaceholder( + /* adGroupIndex= */ 0, + /* adGroupDurationUs= */ 30_000_000, + /* adIndexInAdGroup= */ 2, + /* adDurationUs= */ 10_000_000, + /* adsInAdGroupCount= */ 3, + adPlaybackState); + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ 0); + assertThat(adGroup.count).isEqualTo(3); + assertThat(adGroup.durationsUs[0]).isEqualTo(20_000_000); + assertThat(adGroup.durationsUs[1]).isEqualTo(0); + assertThat(adGroup.durationsUs[2]).isEqualTo(10_000_000); + } + + @Test + public void expandAdGroupPlaceHolder_expandSingleAdInAdGroup_noExpansionCorrectDuration() { + AdPlaybackState adPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + AdPlaybackState.NONE, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 30_000_000); + + adPlaybackState = + ImaUtil.expandAdGroupPlaceholder( + /* adGroupIndex= */ 0, + /* adGroupDurationUs= */ 30_000_000, + /* adIndexInAdGroup= */ 0, + /* adDurationUs= */ 10_000_000, + /* adsInAdGroupCount= */ 1, + adPlaybackState); + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ 0); + assertThat(adGroup.count).isEqualTo(1); + assertThat(adGroup.durationsUs[0]).isEqualTo(10_000_000); + } + + @Test + public void expandAdGroupPlaceHolder_singleAdInAdGroupOverLength_correctsAdDuration() { + AdPlaybackState adPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + AdPlaybackState.NONE, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 10_000_001); + + adPlaybackState = + ImaUtil.expandAdGroupPlaceholder( + /* adGroupIndex= */ 0, + /* adGroupDurationUs= */ 10_000_000, + /* adIndexInAdGroup= */ 0, + /* adDurationUs= */ 10_000_000, + /* adsInAdGroupCount= */ 1, + adPlaybackState); + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ 0); + assertThat(adGroup.count).isEqualTo(1); + assertThat(adGroup.durationsUs[0]).isEqualTo(10_000_000); + } + + @Test + public void expandAdGroupPlaceHolder_initialDurationTooLarge_overriddenWhenExpanded() { + AdPlaybackState adPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + AdPlaybackState.NONE, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 30_000_000); + + adPlaybackState = + ImaUtil.expandAdGroupPlaceholder( + /* adGroupIndex= */ 0, + /* adGroupDurationUs= */ 20_000_000, + /* adIndexInAdGroup= */ 1, + /* adDurationUs= */ 10_000_000, + /* adsInAdGroupCount= */ 2, + adPlaybackState); + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ 0); + assertThat(adGroup.count).isEqualTo(2); + assertThat(adGroup.durationsUs[0]).isEqualTo(10_000_000); + assertThat(adGroup.durationsUs[1]).isEqualTo(10_000_000); + } + + @Test + public void insertAdDurationInAdGroup_correctDurationAndPropagation() { + AdPlaybackState adPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + AdPlaybackState.NONE, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 10_000_000, + 20_000_000, + 0); + + adPlaybackState = + ImaUtil.updateAdDurationInAdGroup( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 1, + /* adDurationUs= */ 15_000_000, + adPlaybackState); + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(0); + assertThat(adGroup.count).isEqualTo(3); + assertThat(adGroup.durationsUs[0]).isEqualTo(10_000_000); + assertThat(adGroup.durationsUs[1]).isEqualTo(15_000_000); + assertThat(adGroup.durationsUs[2]).isEqualTo(5_000_000); + } + + @Test + public void insertAdDurationInAdGroup_insertLast_correctDurationAndPropagation() { + AdPlaybackState adPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + AdPlaybackState.NONE, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 0, + 10_000_000, + 20_000_000); + + adPlaybackState = + ImaUtil.updateAdDurationInAdGroup( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 2, + /* adDurationUs= */ 15_000_000, + adPlaybackState); + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(0); + assertThat(adGroup.count).isEqualTo(3); + assertThat(adGroup.durationsUs[0]).isEqualTo(5_000_000); + assertThat(adGroup.durationsUs[1]).isEqualTo(10_000_000); + assertThat(adGroup.durationsUs[2]).isEqualTo(15_000_000); + } + + @Test + public void insertAdDurationInAdGroup_allDurationsSetAlready_setDurationNoPropagation() { + AdPlaybackState adPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + AdPlaybackState.NONE, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 5_000_000, + 10_000_000, + 20_000_000); + + adPlaybackState = + ImaUtil.updateAdDurationInAdGroup( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 1, + /* adDurationUs= */ 5_000_000, + adPlaybackState); + + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(0); + assertThat(adGroup.count).isEqualTo(3); + assertThat(adGroup.durationsUs[0]).isEqualTo(5_000_000); + assertThat(adGroup.durationsUs[1]).isEqualTo(5_000_000); + assertThat(adGroup.durationsUs[2]).isEqualTo(20_000_000); + } }