Clarify/fix position reference points for AdPlaybackState.

The positions were interchangeably used with window and period
positions. This change more clearly ensures that all positions in the
AdPlaybackState are based on periods and that we use the right adjustments
for all usages.

PiperOrigin-RevId: 297811633
This commit is contained in:
tonihei 2020-02-28 11:57:56 +00:00 committed by Andrew Lewis
parent d1b900604a
commit 67d1b728d3
4 changed files with 65 additions and 40 deletions

View File

@ -318,6 +318,7 @@ public final class ImaAdsLoader
@Nullable private final AdEventListener adEventListener; @Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory; private final ImaFactory imaFactory;
private final Timeline.Period period; private final Timeline.Period period;
private final Timeline.Window window;
private final List<VideoAdPlayerCallback> adCallbacks; private final List<VideoAdPlayerCallback> adCallbacks;
private final AdDisplayContainer adDisplayContainer; private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
@ -469,6 +470,7 @@ public final class ImaAdsLoader
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
period = new Timeline.Period(); period = new Timeline.Period();
window = new Timeline.Window();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
@ -757,14 +759,16 @@ public final class ImaAdsLoader
sentPendingContentPositionMs = true; sentPendingContentPositionMs = true;
contentPositionMs = pendingContentPositionMs; contentPositionMs = pendingContentPositionMs;
expectedAdGroupIndex = expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
expectedAdGroupIndex = expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
contentPositionMs = player.getCurrentPosition(); contentPositionMs = getContentPeriodPositionMs();
// Update the expected ad group index for the current content position. The update is delayed // Update the expected ad group index for the current content position. The update is delayed
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
// just after an ad group isn't incorrectly attributed to the next ad group. // just after an ad group isn't incorrectly attributed to the next ad group.
@ -966,7 +970,7 @@ public final class ImaAdsLoader
} }
Assertions.checkArgument(timeline.getPeriodCount() == 1); Assertions.checkArgument(timeline.getPeriodCount() == 1);
this.timeline = timeline; this.timeline = timeline;
long contentDurationUs = timeline.getPeriod(0, period).durationUs; long contentDurationUs = timeline.getPeriod(/* periodIndex= */ 0, period).durationUs;
contentDurationMs = C.usToMs(contentDurationUs); contentDurationMs = C.usToMs(contentDurationUs);
if (contentDurationUs != C.TIME_UNSET) { if (contentDurationUs != C.TIME_UNSET) {
adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
@ -1029,9 +1033,10 @@ public final class ImaAdsLoader
// Skip ads based on the start position as required. // Skip ads based on the start position as required.
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
long contentPositionMs = player.getContentPosition(); long contentPositionMs = getContentPeriodPositionMs();
int adGroupIndexForPosition = int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) { if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
// Skip any ad groups before the one at or immediately before the playback position. // Skip any ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) { for (int i = 0; i < adGroupIndexForPosition; i++) {
@ -1169,7 +1174,7 @@ public final class ImaAdsLoader
} }
updateAdPlaybackState(); updateAdPlaybackState();
} else if (!timeline.isEmpty()) { } else if (!timeline.isEmpty()) {
long positionMs = player.getCurrentPosition(); long positionMs = getContentPeriodPositionMs();
timeline.getPeriod(/* periodIndex= */ 0, period); timeline.getPeriod(/* periodIndex= */ 0, period);
int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs));
if (newAdGroupIndex != C.INDEX_UNSET) { if (newAdGroupIndex != C.INDEX_UNSET) {
@ -1311,8 +1316,9 @@ public final class ImaAdsLoader
} }
private void checkForContentComplete() { private void checkForContentComplete() {
if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET if (contentDurationMs != C.TIME_UNSET
&& player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs && pendingContentPositionMs == C.TIME_UNSET
&& getContentPeriodPositionMs() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
&& !sentContentComplete) { && !sentContentComplete) {
adsLoader.contentComplete(); adsLoader.contentComplete();
if (DEBUG) { if (DEBUG) {
@ -1322,7 +1328,8 @@ public final class ImaAdsLoader
// After sending content complete IMA will not poll the content position, so set the expected // After sending content complete IMA will not poll the content position, so set the expected
// ad group index. // ad group index.
expectedAdGroupIndex = expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentDurationMs)); adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentDurationMs), C.msToUs(contentDurationMs));
} }
} }
@ -1374,6 +1381,12 @@ public final class ImaAdsLoader
} }
} }
private long getContentPeriodPositionMs() {
long contentWindowPositionMs = player.getContentPosition();
return contentWindowPositionMs
- timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs();
}
private static long[] getAdGroupTimesUs(List<Float> cuePoints) { private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
if (cuePoints.isEmpty()) { if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad. // If no cue points are specified, there is a preroll ad.

View File

@ -466,8 +466,8 @@ public abstract class Timeline {
* microseconds. * microseconds.
* *
* @param adGroupIndex The ad group index. * @param adGroupIndex The ad group index.
* @return The time of the ad group at the index, in microseconds, or {@link * @return The time of the ad group at the index relative to the start of the enclosing {@link
* C#TIME_END_OF_SOURCE} for a post-roll ad group. * Period}, in microseconds, or {@link C#TIME_END_OF_SOURCE} for a post-roll ad group.
*/ */
public long getAdGroupTimeUs(int adGroupIndex) { public long getAdGroupTimeUs(int adGroupIndex) {
return adPlaybackState.adGroupTimesUs[adGroupIndex]; return adPlaybackState.adGroupTimesUs[adGroupIndex];
@ -510,22 +510,23 @@ public abstract class Timeline {
} }
/** /**
* Returns the index of the ad group at or before {@code positionUs}, if that ad group is * Returns the index of the ad group at or before {@code positionUs} in the period, if that ad
* unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has * group is unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code
* no ads remaining to be played, or if there is no such ad group. * positionUs} has no ads remaining to be played, or if there is no such ad group.
* *
* @param positionUs The position at or before which to find an ad group, in microseconds. * @param positionUs The period position at or before which to find an ad group, in
* microseconds.
* @return The index of the ad group, or {@link C#INDEX_UNSET}. * @return The index of the ad group, or {@link C#INDEX_UNSET}.
*/ */
public int getAdGroupIndexForPositionUs(long positionUs) { public int getAdGroupIndexForPositionUs(long positionUs) {
return adPlaybackState.getAdGroupIndexForPositionUs(positionUs); return adPlaybackState.getAdGroupIndexForPositionUs(positionUs, durationUs);
} }
/** /**
* Returns the index of the next ad group after {@code positionUs} that has ads remaining to be * Returns the index of the next ad group after {@code positionUs} in the period that has ads
* played. Returns {@link C#INDEX_UNSET} if there is no such ad group. * remaining to be played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
* *
* @param positionUs The position after which to find an ad group, in microseconds. * @param positionUs The period position after which to find an ad group, in microseconds.
* @return The index of the ad group, or {@link C#INDEX_UNSET}. * @return The index of the ad group, or {@link C#INDEX_UNSET}.
*/ */
public int getAdGroupIndexAfterPositionUs(long positionUs) { public int getAdGroupIndexAfterPositionUs(long positionUs) {

View File

@ -189,11 +189,15 @@ public final class PlaybackStatsListener
@Override @Override
public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) { public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) {
Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd()); Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd());
long contentPositionUs = long contentPeriodPositionUs =
eventTime eventTime
.timeline .timeline
.getPeriodByUid(eventTime.mediaPeriodId.periodUid, period) .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period)
.getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex); .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex);
long contentWindowPositionUs =
contentPeriodPositionUs == C.TIME_END_OF_SOURCE
? C.TIME_END_OF_SOURCE
: contentPeriodPositionUs + period.getPositionInWindowUs();
EventTime contentEventTime = EventTime contentEventTime =
new EventTime( new EventTime(
eventTime.realtimeMs, eventTime.realtimeMs,
@ -203,7 +207,7 @@ public final class PlaybackStatsListener
eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.periodUid,
eventTime.mediaPeriodId.windowSequenceNumber, eventTime.mediaPeriodId.windowSequenceNumber,
eventTime.mediaPeriodId.adGroupIndex), eventTime.mediaPeriodId.adGroupIndex),
/* eventPlaybackPositionMs= */ C.usToMs(contentPositionUs), /* eventPlaybackPositionMs= */ C.usToMs(contentWindowPositionUs),
eventTime.currentPlaybackPositionMs, eventTime.currentPlaybackPositionMs,
eventTime.totalBufferedDurationMs); eventTime.totalBufferedDurationMs);
Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) Assertions.checkNotNull(playbackStatsTrackers.get(contentSession))

View File

@ -29,8 +29,7 @@ import java.util.Arrays;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
* Represents ad group times relative to the start of the media and information on the state and * Represents ad group times and information on the state and URIs of ads within each ad group.
* URIs of ads within each ad group.
* *
* <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the * <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the
* required changes. * required changes.
@ -272,8 +271,9 @@ public final class AdPlaybackState {
/** The number of ad groups. */ /** The number of ad groups. */
public final int adGroupCount; public final int adGroupCount;
/** /**
* The times of ad groups, in microseconds. A final element with the value {@link * The times of ad groups, in microseconds, relative to the start of the {@link
* C#TIME_END_OF_SOURCE} indicates a postroll ad. * com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with the value
* {@link C#TIME_END_OF_SOURCE} indicates a postroll ad.
*/ */
public final long[] adGroupTimesUs; public final long[] adGroupTimesUs;
/** The ad groups. */ /** The ad groups. */
@ -286,8 +286,9 @@ public final class AdPlaybackState {
/** /**
* Creates a new ad playback state with the specified ad group times. * Creates a new ad playback state with the specified ad group times.
* *
* @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value * @param adGroupTimesUs The times of ad groups in microseconds, relative to the start of the
* {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. * {@link com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with
* the value {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad.
*/ */
public AdPlaybackState(long... adGroupTimesUs) { public AdPlaybackState(long... adGroupTimesUs) {
int count = adGroupTimesUs.length; int count = adGroupTimesUs.length;
@ -315,16 +316,18 @@ public final class AdPlaybackState {
* unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no
* ads remaining to be played, or if there is no such ad group. * ads remaining to be played, or if there is no such ad group.
* *
* @param positionUs The position at or before which to find an ad group, in microseconds, or * @param positionUs The period position at or before which to find an ad group, in microseconds,
* {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any * or {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any
* unplayed postroll ad group will be returned). * unplayed postroll ad group will be returned).
* @param periodDurationUs The duration of the containing timeline period, in microseconds, or
* {@link C#TIME_UNSET} if not known.
* @return The index of the ad group, or {@link C#INDEX_UNSET}. * @return The index of the ad group, or {@link C#INDEX_UNSET}.
*/ */
public int getAdGroupIndexForPositionUs(long positionUs) { public int getAdGroupIndexForPositionUs(long positionUs, long periodDurationUs) {
// Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
// In practice we expect there to be few ad groups so the search shouldn't be expensive. // In practice we expect there to be few ad groups so the search shouldn't be expensive.
int index = adGroupTimesUs.length - 1; int index = adGroupTimesUs.length - 1;
while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) { while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) {
index--; index--;
} }
return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET; return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET;
@ -334,11 +337,11 @@ public final class AdPlaybackState {
* Returns the index of the next ad group after {@code positionUs} that has ads remaining to be * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be
* played. Returns {@link C#INDEX_UNSET} if there is no such ad group. * played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
* *
* @param positionUs The position after which to find an ad group, in microseconds, or {@link * @param positionUs The period position after which to find an ad group, in microseconds, or
* C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad group * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad
* after the position). * group after the position).
* @param periodDurationUs The duration of the containing period in microseconds, or {@link * @param periodDurationUs The duration of the containing timeline period, in microseconds, or
* C#TIME_UNSET} if not known. * {@link C#TIME_UNSET} if not known.
* @return The index of the ad group, or {@link C#INDEX_UNSET}. * @return The index of the ad group, or {@link C#INDEX_UNSET}.
*/ */
public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) { public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) {
@ -425,7 +428,10 @@ public final class AdPlaybackState {
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
} }
/** Returns an instance with the specified ad resume position, in microseconds. */ /**
* Returns an instance with the specified ad resume position, in microseconds, relative to the
* start of the current ad.
*/
@CheckResult @CheckResult
public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) { public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) {
if (this.adResumePositionUs == adResumePositionUs) { if (this.adResumePositionUs == adResumePositionUs) {
@ -471,14 +477,15 @@ public final class AdPlaybackState {
return result; return result;
} }
private boolean isPositionBeforeAdGroup(long positionUs, int adGroupIndex) { private boolean isPositionBeforeAdGroup(
long positionUs, long periodDurationUs, int adGroupIndex) {
if (positionUs == C.TIME_END_OF_SOURCE) { if (positionUs == C.TIME_END_OF_SOURCE) {
// The end of the content is at (but not before) any postroll ad, and after any other ads. // The end of the content is at (but not before) any postroll ad, and after any other ads.
return false; return false;
} }
long adGroupPositionUs = adGroupTimesUs[adGroupIndex]; long adGroupPositionUs = adGroupTimesUs[adGroupIndex];
if (adGroupPositionUs == C.TIME_END_OF_SOURCE) { if (adGroupPositionUs == C.TIME_END_OF_SOURCE) {
return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs; return periodDurationUs == C.TIME_UNSET || positionUs < periodDurationUs;
} else { } else {
return positionUs < adGroupPositionUs; return positionUs < adGroupPositionUs;
} }