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;
private final ImaFactory imaFactory;
private final Timeline.Period period;
private final Timeline.Window window;
private final List<VideoAdPlayerCallback> adCallbacks;
private final AdDisplayContainer adDisplayContainer;
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.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
period = new Timeline.Period();
window = new Timeline.Window();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
@ -757,14 +759,16 @@ public final class ImaAdsLoader
sentPendingContentPositionMs = true;
contentPositionMs = pendingContentPositionMs;
expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
} 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
// 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.
@ -966,7 +970,7 @@ public final class ImaAdsLoader
}
Assertions.checkArgument(timeline.getPeriodCount() == 1);
this.timeline = timeline;
long contentDurationUs = timeline.getPeriod(0, period).durationUs;
long contentDurationUs = timeline.getPeriod(/* periodIndex= */ 0, period).durationUs;
contentDurationMs = C.usToMs(contentDurationUs);
if (contentDurationUs != C.TIME_UNSET) {
adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
@ -1029,9 +1033,10 @@ public final class ImaAdsLoader
// Skip ads based on the start position as required.
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
long contentPositionMs = player.getContentPosition();
long contentPositionMs = getContentPeriodPositionMs();
int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
// Skip any ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) {
@ -1169,7 +1174,7 @@ public final class ImaAdsLoader
}
updateAdPlaybackState();
} else if (!timeline.isEmpty()) {
long positionMs = player.getCurrentPosition();
long positionMs = getContentPeriodPositionMs();
timeline.getPeriod(/* periodIndex= */ 0, period);
int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs));
if (newAdGroupIndex != C.INDEX_UNSET) {
@ -1311,8 +1316,9 @@ public final class ImaAdsLoader
}
private void checkForContentComplete() {
if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET
&& player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
if (contentDurationMs != C.TIME_UNSET
&& pendingContentPositionMs == C.TIME_UNSET
&& getContentPeriodPositionMs() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
&& !sentContentComplete) {
adsLoader.contentComplete();
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
// ad group index.
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) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.

View File

@ -466,8 +466,8 @@ public abstract class Timeline {
* microseconds.
*
* @param adGroupIndex The ad group index.
* @return The time of the ad group at the index, in microseconds, or {@link
* C#TIME_END_OF_SOURCE} for a post-roll ad group.
* @return The time of the ad group at the index relative to the start of the enclosing {@link
* Period}, in microseconds, or {@link C#TIME_END_OF_SOURCE} for a post-roll ad group.
*/
public long getAdGroupTimeUs(int 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
* 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.
* Returns the index of the ad group at or before {@code positionUs} in the period, if that ad
* group is 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.
*
* @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}.
*/
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
* played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
* Returns the index of the next ad group after {@code positionUs} in the period that has ads
* 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}.
*/
public int getAdGroupIndexAfterPositionUs(long positionUs) {

View File

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

View File

@ -29,8 +29,7 @@ import java.util.Arrays;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* Represents ad group times relative to the start of the media and information on the state and
* URIs of ads within each ad group.
* Represents ad group times and information on the state and URIs of ads within each ad group.
*
* <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the
* required changes.
@ -272,8 +271,9 @@ public final class AdPlaybackState {
/** The number of ad groups. */
public final int adGroupCount;
/**
* The times of ad groups, in microseconds. A final element with the value {@link
* C#TIME_END_OF_SOURCE} indicates a postroll ad.
* The times of ad groups, in microseconds, relative to the start of the {@link
* 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;
/** The ad groups. */
@ -286,8 +286,9 @@ public final class AdPlaybackState {
/**
* 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
* {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad.
* @param adGroupTimesUs The times of ad groups in microseconds, relative to the start of the
* {@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) {
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
* 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
* {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any
* @param positionUs The period position at or before which to find an ad group, in microseconds,
* 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).
* @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}.
*/
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.
// In practice we expect there to be few ad groups so the search shouldn't be expensive.
int index = adGroupTimesUs.length - 1;
while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) {
while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) {
index--;
}
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
* 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
* C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad group
* after the position).
* @param periodDurationUs The duration of the containing period in microseconds, or {@link
* C#TIME_UNSET} if not known.
* @param positionUs The period position after which to find an ad group, in microseconds, or
* {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad
* group after the position).
* @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}.
*/
public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) {
@ -425,7 +428,10 @@ public final class AdPlaybackState {
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
public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) {
if (this.adResumePositionUs == adResumePositionUs) {
@ -471,14 +477,15 @@ public final class AdPlaybackState {
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) {
// The end of the content is at (but not before) any postroll ad, and after any other ads.
return false;
}
long adGroupPositionUs = adGroupTimesUs[adGroupIndex];
if (adGroupPositionUs == C.TIME_END_OF_SOURCE) {
return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs;
return periodDurationUs == C.TIME_UNSET || positionUs < periodDurationUs;
} else {
return positionUs < adGroupPositionUs;
}