From bdd64ce6a16f5df615e7b7332ef91a740cbf3041 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 31 Jan 2022 10:32:10 +0000 Subject: [PATCH] Setup VOD cue points to fill them when loaded This makes sure the number of ads in an ad group matches to the number of periods representing an ad group in a multi-period timeline. This makes it easier to accurately mark ads as played in multi-period windows which is needed to correctly prevent seeking over unplayed ads. PiperOrigin-RevId: 425317085 --- .../android/exoplayer2/ext/ima/ImaUtil.java | 100 +++++++- .../exoplayer2/ext/ima/ImaUtilTest.java | 221 ++++++++++++++++++ 2 files changed, 312 insertions(+), 9 deletions(-) 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); + } }