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
This commit is contained in:
parent
a49a7d72e1
commit
afc3a79e10
@ -15,13 +15,17 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.ima;
|
package androidx.media3.exoplayer.ima;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
|
import static androidx.media3.common.util.Util.sum;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import androidx.annotation.CheckResult;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.AdOverlayInfo;
|
import androidx.media3.common.AdOverlayInfo;
|
||||||
import androidx.media3.common.AdPlaybackState;
|
import androidx.media3.common.AdPlaybackState;
|
||||||
@ -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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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
|
* 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
|
* 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
|
// 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.
|
// 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;
|
long elapsedAdGroupAdDurationUs = 0;
|
||||||
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
|
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
|
||||||
contentTimeline.getPeriod(j, period, /* setIds= */ true);
|
contentTimeline.getPeriod(j, period, /* setIds= */ true);
|
||||||
@ -375,13 +465,5 @@ import java.util.Set;
|
|||||||
return adPlaybackState;
|
return adPlaybackState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long getTotalDurationUs(long[] durationsUs) {
|
|
||||||
long totalDurationUs = 0;
|
|
||||||
for (long adDurationUs : durationsUs) {
|
|
||||||
totalDurationUs += adDurationUs;
|
|
||||||
}
|
|
||||||
return totalDurationUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImaUtil() {}
|
private ImaUtil() {}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import android.util.Pair;
|
|||||||
import androidx.media3.common.AdPlaybackState;
|
import androidx.media3.common.AdPlaybackState;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Timeline;
|
import androidx.media3.common.Timeline;
|
||||||
|
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil;
|
||||||
import androidx.media3.test.utils.FakeTimeline;
|
import androidx.media3.test.utils.FakeTimeline;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
@ -503,4 +504,224 @@ public class ImaUtilTest {
|
|||||||
assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user