Mark played ads in multi-period VOD streams

#minor-release

PiperOrigin-RevId: 425842813
This commit is contained in:
bachinger 2022-02-02 11:41:55 +00:00 committed by Ian Baker
parent 392ec6f394
commit b911e2ee4e
3 changed files with 161 additions and 26 deletions

View File

@ -16,12 +16,14 @@
package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdPlaybackStateForPeriods;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationAndPropagate;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationInAdGroup;
import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
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.msToUs;
import static com.google.android.exoplayer2.util.Util.secToUs;
import static com.google.android.exoplayer2.util.Util.sum;
import static com.google.android.exoplayer2.util.Util.usToMs;
@ -30,6 +32,7 @@ import static java.lang.Math.min;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.util.Pair;
import android.view.ViewGroup;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
@ -618,18 +621,26 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
return;
}
if (oldPosition.adGroupIndex != C.INDEX_UNSET && newPosition.adGroupIndex == C.INDEX_UNSET) {
AdPlaybackState newAdPlaybackState = adPlaybackState;
for (int i = 0; i <= oldPosition.adIndexInAdGroup; i++) {
int state = newAdPlaybackState.getAdGroup(oldPosition.adGroupIndex).states[i];
if (state != AdPlaybackState.AD_STATE_SKIPPED
&& state != AdPlaybackState.AD_STATE_ERROR) {
newAdPlaybackState =
newAdPlaybackState.withPlayedAd(
oldPosition.adGroupIndex, /* adIndexInAdGroup= */ i);
}
if (oldPosition.adGroupIndex != C.INDEX_UNSET) {
int adGroupIndex = oldPosition.adGroupIndex;
int adIndexInAdGroup = oldPosition.adIndexInAdGroup;
Timeline timeline = player.getCurrentTimeline();
Timeline.Window window =
timeline.getWindow(oldPosition.mediaItemIndex, new Timeline.Window());
if (window.lastPeriodIndex > window.firstPeriodIndex) {
// Map adGroupIndex and adIndexInAdGroup to multi-period window.
Pair<Integer, Integer> adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(
oldPosition.periodIndex, adPlaybackState, timeline);
adGroupIndex = adGroupIndexAndAdIndexInAdGroup.first;
adIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup.second;
}
int adState = adPlaybackState.getAdGroup(adGroupIndex).states[adIndexInAdGroup];
if (adState == AdPlaybackState.AD_STATE_AVAILABLE
|| adState == AdPlaybackState.AD_STATE_UNAVAILABLE) {
setAdPlaybackState(
adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ adIndexInAdGroup));
}
setAdPlaybackState(newAdPlaybackState);
}
}
@ -698,8 +709,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
long positionInWindowUs =
timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.positionInWindowUs;
long currentPeriodPosition =
Util.msToUs(player.getCurrentPosition()) - positionInWindowUs;
long currentPeriodPosition = msToUs(player.getCurrentPosition()) - positionInWindowUs;
newAdPlaybackState =
addLiveAdBreak(
event.getAd(),

View File

@ -23,6 +23,7 @@ import static java.lang.Math.max;
import android.content.Context;
import android.os.Looper;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.CheckResult;
@ -355,14 +356,13 @@ 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.
* content timeline.
*
* <p>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.
* <p>If a period is enclosed by an ad group, the period is considered an ad period. Splitting
* results in a separate {@link AdPlaybackState ad playback state} for each period that has either
* no ads or a single ad. In the latter case, the duration of the single ad is set to the duration
* of the period consuming the entire duration of the period. Accordingly an ad period does not
* contribute to the duration of the containing window.
*
* @param adPlaybackState The ad playback state to be split.
* @param contentTimeline The content timeline for each period of which to create an {@link
@ -398,15 +398,19 @@ import java.util.Set;
long elapsedAdGroupAdDurationUs = 0;
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
contentTimeline.getPeriod(j, period, /* setIds= */ true);
if (totalElapsedContentDurationUs < adGroup.timeUs) {
// TODO(b/192231683) Remove subtracted US from ad group time when we can upgrade the SDK.
// Subtract one microsecond to work around rounding errors with adGroup.timeUs.
if (totalElapsedContentDurationUs < adGroup.timeUs - 1) {
// 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).
// TODO(b/192231683) Remove additional US when we can upgrade the SDK.
// Add one microsecond to work around rounding errors with adGroup.timeUs.
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs + 1) {
// The period ends before the end of the ad group, so it is an ad period (Note: A VOD ad
// reported by the IMA SDK spans multiple periods before the LOADED event arrives).
adPlaybackStates.put(
checkNotNull(period.uid),
splitAdGroupForPeriod(adsId, adGroup, periodStartUs, period.durationUs));
@ -430,7 +434,6 @@ import java.util.Set;
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)
@ -465,5 +468,54 @@ import java.util.Set;
return adPlaybackState;
}
/**
* Returns the {@code adGroupIndex} and the {@code adIndexInAdGroup} for the given period index of
* an ad period.
*
* @param adPeriodIndex The period index of the ad period.
* @param adPlaybackState The ad playback state that holds the ad group and ad information.
* @param timeline The timeline that contains the ad period.
* @return A pair with the ad group index (first) and the ad index in that ad group (second).
*/
public static Pair<Integer, Integer> getAdGroupAndIndexInMultiPeriodWindow(
int adPeriodIndex, AdPlaybackState adPlaybackState, Timeline timeline) {
Timeline.Period period = new Timeline.Period();
int periodIndex = 0;
long totalElapsedContentDurationUs = 0;
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
int adIndexInAdGroup = 0;
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
long adGroupDurationUs = sum(adGroup.durationsUs);
long elapsedAdGroupAdDurationUs = 0;
for (int j = periodIndex; j < timeline.getPeriodCount(); j++) {
timeline.getPeriod(j, period, /* setIds= */ true);
// TODO(b/192231683) Remove subtracted US from ad group time when we can upgrade the SDK.
// Subtract one microsecond to work around rounding errors with adGroup.timeUs.
if (totalElapsedContentDurationUs < adGroup.timeUs - 1) {
// Period starts before the ad group, so it is a content period.
totalElapsedContentDurationUs += period.durationUs;
} else {
long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs;
// TODO(b/192231683) Remove additional US when we can upgrade the SDK.
// Add one microsecond to work around rounding errors with adGroup.timeUs.
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs + 1) {
// The period ends before the end of the ad group, so it is an ad period.
if (j == adPeriodIndex) {
return new Pair<>(/* adGroupIndex= */ i, adIndexInAdGroup);
}
elapsedAdGroupAdDurationUs += period.durationUs;
adIndexInAdGroup++;
} else {
// Period is after the current ad group. Continue with next ad group.
break;
}
}
// Increment the period index to the next unclassified period.
periodIndex++;
}
}
throw new IllegalStateException();
}
private ImaUtil() {}
}

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static com.google.common.truth.Truth.assertThat;
@ -27,6 +28,7 @@ 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.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -463,7 +465,9 @@ public class ImaUtilTest {
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "adsId",
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs + 1)
// TODO(b/192231683) Reduce additional period duration to 1 when rounding work
// around removed.
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs + 2)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
@ -724,4 +728,73 @@ public class ImaUtilTest {
assertThat(adGroup.durationsUs[1]).isEqualTo(5_000_000);
assertThat(adGroup.durationsUs[2]).isEqualTo(20_000_000);
}
@Test
public void getAdGroupAndIndexInMultiPeriodWindow_correctAdGroupIndexAndAdIndexInAdGroup() {
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 9, new Object()));
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / 9;
// [ad, ad, content, ad, ad, ad, content, ad, ad]
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId", 0, periodDurationUs, 2 * periodDurationUs)
.withAdCount(/* adGroupIndex= */ 0, 2)
.withAdCount(/* adGroupIndex= */ 1, 3)
.withAdCount(/* adGroupIndex= */ 2, 2)
.withAdDurationsUs(
/* adGroupIndex= */ 0,
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs,
periodDurationUs)
.withAdDurationsUs(
/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs, periodDurationUs)
.withAdDurationsUs(/* adGroupIndex= */ 2, periodDurationUs, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
Pair<Integer, Integer> adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 0, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0);
adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 1, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
Assert.assertThrows(
IllegalStateException.class,
() ->
getAdGroupAndIndexInMultiPeriodWindow(
/* adPeriodIndex= */ 2, adPlaybackState, timeline));
adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 3, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0);
adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 4, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 5, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(2);
Assert.assertThrows(
IllegalStateException.class,
() ->
getAdGroupAndIndexInMultiPeriodWindow(
/* adPeriodIndex= */ 6, adPlaybackState, timeline));
adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 7, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0);
adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 8, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
}
}