mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Mark played ads in multi-period VOD streams
#minor-release PiperOrigin-RevId: 425842813
This commit is contained in:
parent
392ec6f394
commit
b911e2ee4e
@ -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(),
|
||||
|
@ -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() {}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user