Make ad state immutable and store state of each ad

Before this change, the ad playback state stored the number of played ads in
each ad group. There was no way to represent that an ad had failed to load (and
it wouldn't be possible just to increment the played ad count to signal a load
error because there might be an unplayed ad before the ad that failed to load).

Represent the state of each ad (unavailable, available, skipped, played, error)
in each ad group. In a later change the player will use this information to
update its loaded MediaPeriods in response to future ads failing to load.

Also make the AdPlaybackState immutable and remove copying/duplication of its
fields in the ad timeline and period.

Issue: #3584

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=183655308
This commit is contained in:
andrewlewis 2018-01-29 06:06:29 -08:00 committed by Oliver Woodman
parent 0c67a5783f
commit b82178ecb4
8 changed files with 499 additions and 197 deletions

View File

@ -48,6 +48,7 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
@ -393,7 +394,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
maybeNotifyAdError(); maybeNotifyAdError();
if (adPlaybackState != null) { if (adPlaybackState != null) {
// Pass the ad playback state to the player, and resume ads if necessary. // Pass the ad playback state to the player, and resume ads if necessary.
eventListener.onAdPlaybackState(adPlaybackState.copy()); eventListener.onAdPlaybackState(adPlaybackState);
if (imaPausedContent && player.getPlayWhenReady()) { if (imaPausedContent && player.getPlayWhenReady()) {
adsManager.resume(); adsManager.resume();
} }
@ -409,7 +410,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override @Override
public void detachPlayer() { public void detachPlayer() {
if (adsManager != null && imaPausedContent) { if (adsManager != null && imaPausedContent) {
adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0); adPlaybackState =
adPlaybackState.withAdResumePositionUs(
playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
adsManager.pause(); adsManager.pause();
} }
lastAdProgress = getAdProgress(); lastAdProgress = getAdProgress();
@ -474,7 +477,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
} }
adPlaybackState.setAdCount(adGroupIndex, adCount); adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount);
updateAdPlaybackState(); updateAdPlaybackState();
if (adGroupIndex != expectedAdGroupIndex) { if (adGroupIndex != expectedAdGroupIndex) {
Log.w( Log.w(
@ -589,8 +592,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex); Log.d(TAG, "loadAd in ad group " + adGroupIndex);
} }
adPlaybackState.addAdUri(adGroupIndex, Uri.parse(adUriString)); int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adPlaybackState.adsLoadedCounts[adGroupIndex] == adPlaybackState.adCounts[adGroupIndex]) { adPlaybackState =
adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString));
if (getAdIndexInAdGroupToLoad(adGroupIndex) == C.INDEX_UNSET) {
// Keep track of the expected ad group index to use as a fallback if the LOADED event is // Keep track of the expected ad group index to use as a fallback if the LOADED event is
// unexpectedly not triggered. // unexpectedly not triggered.
expectedAdGroupIndex++; expectedAdGroupIndex++;
@ -693,7 +698,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
long contentDurationUs = timeline.getPeriod(0, period).durationUs; long contentDurationUs = timeline.getPeriod(0, period).durationUs;
contentDurationMs = C.usToMs(contentDurationUs); contentDurationMs = C.usToMs(contentDurationUs);
if (contentDurationUs != C.TIME_UNSET) { if (contentDurationUs != C.TIME_UNSET) {
adPlaybackState.contentDurationUs = contentDurationUs; adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
} }
updateImaStateForPlayerState(); updateImaStateForPlayerState();
} }
@ -748,7 +753,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (sentContentComplete) { if (sentContentComplete) {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) { for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) {
adPlaybackState.playedAdGroup(i); adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
} }
} }
updateAdPlaybackState(); updateAdPlaybackState();
@ -790,7 +795,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} else /* adGroupIndexForPosition > 0 */ { } else /* adGroupIndexForPosition > 0 */ {
// Skip ad groups before the one at or immediately before the playback position. // Skip 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++) {
adPlaybackState.playedAdGroup(i); adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
} }
// Play ads after the midpoint between the ad to play and the one before it, to avoid issues // Play ads after the midpoint between the ad to play and the one before it, to avoid issues
// with rounding one of the two ad times. // with rounding one of the two ad times.
@ -859,7 +864,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} }
} }
if (playingAd && adGroupIndex != C.INDEX_UNSET) { if (playingAd && adGroupIndex != C.INDEX_UNSET) {
adPlaybackState.playedAdGroup(adGroupIndex); adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex);
adGroupIndex = C.INDEX_UNSET; adGroupIndex = C.INDEX_UNSET;
updateAdPlaybackState(); updateAdPlaybackState();
} }
@ -879,7 +884,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private void stopAdInternal() { private void stopAdInternal() {
Assertions.checkState(imaAdState != IMA_AD_STATE_NONE); Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
imaAdState = IMA_AD_STATE_NONE; imaAdState = IMA_AD_STATE_NONE;
adPlaybackState.playedAd(adGroupIndex); int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].nextAdIndexToPlay;
// TODO: Handle the skipped event so the ad can be marked as skipped rather than played.
adPlaybackState =
adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0);
updateAdPlaybackState(); updateAdPlaybackState();
if (!playingAd) { if (!playingAd) {
adGroupIndex = C.INDEX_UNSET; adGroupIndex = C.INDEX_UNSET;
@ -901,7 +909,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private void updateAdPlaybackState() { private void updateAdPlaybackState() {
// Ignore updates while detached. When a player is attached it will receive the latest state. // Ignore updates while detached. When a player is attached it will receive the latest state.
if (eventListener != null) { if (eventListener != null) {
eventListener.onAdPlaybackState(adPlaybackState.copy()); eventListener.onAdPlaybackState(adPlaybackState);
} }
} }
@ -946,4 +954,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} }
return adGroupTimesUs; return adGroupTimesUs;
} }
/**
* Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all
* ads in the ad group have loaded.
*/
private int getAdIndexInAdGroupToLoad(int adGroupIndex) {
@AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states;
int adIndexInAdGroup = 0;
// IMA loads ads in order.
while (adIndexInAdGroup < states.length
&& states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) {
adIndexInAdGroup++;
}
return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup;
}
} }

View File

@ -326,7 +326,7 @@ import com.google.android.exoplayer2.util.Assertions;
if (adGroupIndex == C.INDEX_UNSET) { if (adGroupIndex == C.INDEX_UNSET) {
return new MediaPeriodId(periodIndex); return new MediaPeriodId(periodIndex);
} else { } else {
int adIndexInAdGroup = period.getPlayedAdCount(adGroupIndex); int adIndexInAdGroup = period.getNextAdIndexToPlay(adGroupIndex);
return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup);
} }
} }
@ -502,7 +502,7 @@ import com.google.android.exoplayer2.util.Assertions;
.getPeriod(id.periodIndex, period) .getPeriod(id.periodIndex, period)
.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup);
long startPositionUs = long startPositionUs =
adIndexInAdGroup == period.getPlayedAdCount(adGroupIndex) adIndexInAdGroup == period.getNextAdIndexToPlay(adGroupIndex)
? period.getAdResumePositionUs() ? period.getAdResumePositionUs()
: 0; : 0;
return new MediaPeriodInfo( return new MediaPeriodInfo(
@ -547,7 +547,7 @@ import com.google.android.exoplayer2.util.Assertions;
boolean isLastAd = boolean isLastAd =
isAd && id.adGroupIndex == lastAdGroupIndex && id.adIndexInAdGroup == postrollAdCount - 1; isAd && id.adGroupIndex == lastAdGroupIndex && id.adIndexInAdGroup == postrollAdCount - 1;
return isLastAd || (!isAd && period.getPlayedAdCount(lastAdGroupIndex) == postrollAdCount); return isLastAd || (!isAd && period.getNextAdIndexToPlay(lastAdGroupIndex) == postrollAdCount);
} }
private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
/** /**
@ -278,12 +279,7 @@ public abstract class Timeline {
public long durationUs; public long durationUs;
private long positionInWindowUs; private long positionInWindowUs;
private long[] adGroupTimesUs; private AdPlaybackState adPlaybackState;
private int[] adCounts;
private int[] adsLoadedCounts;
private int[] adsPlayedCounts;
private long[][] adDurationsUs;
private long adResumePositionUs;
/** /**
* Sets the data held by this period. * Sets the data held by this period.
@ -300,8 +296,7 @@ public abstract class Timeline {
*/ */
public Period set(Object id, Object uid, int windowIndex, long durationUs, public Period set(Object id, Object uid, int windowIndex, long durationUs,
long positionInWindowUs) { long positionInWindowUs) {
return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null, return set(id, uid, windowIndex, durationUs, positionInWindowUs, AdPlaybackState.NONE);
null, C.TIME_UNSET);
} }
/** /**
@ -315,33 +310,23 @@ public abstract class Timeline {
* @param positionInWindowUs The position of the start of this period relative to the start of * @param positionInWindowUs The position of the start of this period relative to the start of
* the window to which it belongs, in milliseconds. May be negative if the start of the * the window to which it belongs, in milliseconds. May be negative if the start of the
* period is not within the window. * period is not within the window.
* @param adGroupTimesUs The times of ad groups relative to the start of the period, in * @param adPlaybackState The state of the period's ads, or {@link AdPlaybackState#NONE} if
* microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that * there are no ads.
* the period has a postroll ad.
* @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET}
* if the number of ads is not yet known.
* @param adsLoadedCounts The number of ads loaded so far in each ad group.
* @param adsPlayedCounts The number of ads played so far in each ad group.
* @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element
* may be {@link C#TIME_UNSET} if the duration is not yet known.
* @param adResumePositionUs The position offset in the first unplayed ad at which to begin
* playback, in microseconds.
* @return This period, for convenience. * @return This period, for convenience.
*/ */
public Period set(Object id, Object uid, int windowIndex, long durationUs, public Period set(
long positionInWindowUs, long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts, Object id,
int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs) { Object uid,
int windowIndex,
long durationUs,
long positionInWindowUs,
AdPlaybackState adPlaybackState) {
this.id = id; this.id = id;
this.uid = uid; this.uid = uid;
this.windowIndex = windowIndex; this.windowIndex = windowIndex;
this.durationUs = durationUs; this.durationUs = durationUs;
this.positionInWindowUs = positionInWindowUs; this.positionInWindowUs = positionInWindowUs;
this.adGroupTimesUs = adGroupTimesUs; this.adPlaybackState = adPlaybackState;
this.adCounts = adCounts;
this.adsLoadedCounts = adsLoadedCounts;
this.adsPlayedCounts = adsPlayedCounts;
this.adDurationsUs = adDurationsUs;
this.adResumePositionUs = adResumePositionUs;
return this; return this;
} }
@ -381,7 +366,7 @@ public abstract class Timeline {
* Returns the number of ad groups in the period. * Returns the number of ad groups in the period.
*/ */
public int getAdGroupCount() { public int getAdGroupCount() {
return adGroupTimesUs == null ? 0 : adGroupTimesUs.length; return adPlaybackState.adGroupCount;
} }
/** /**
@ -392,17 +377,19 @@ public abstract class Timeline {
* @return The time of the ad group at the index, in microseconds. * @return The time of the ad group at the index, in microseconds.
*/ */
public long getAdGroupTimeUs(int adGroupIndex) { public long getAdGroupTimeUs(int adGroupIndex) {
return adGroupTimesUs[adGroupIndex]; return adPlaybackState.adGroupTimesUs[adGroupIndex];
} }
/** /**
* Returns the number of ads that have been played in the specified ad group in the period. * Returns the index of the next ad to play in the specified ad group, or the number of ads in
* the ad group if the ad group does not have any ads remaining to play.
* *
* @param adGroupIndex The ad group index. * @param adGroupIndex The ad group index.
* @return The number of ads that have been played. * @return The index of the next ad that should be played, or the number of ads in the ad group
* if the ad group does not have any ads remaining to play.
*/ */
public int getPlayedAdCount(int adGroupIndex) { public int getNextAdIndexToPlay(int adGroupIndex) {
return adsPlayedCounts[adGroupIndex]; return adPlaybackState.adGroups[adGroupIndex].nextAdIndexToPlay;
} }
/** /**
@ -412,8 +399,8 @@ public abstract class Timeline {
* @return Whether the ad group at index {@code adGroupIndex} has been played. * @return Whether the ad group at index {@code adGroupIndex} has been played.
*/ */
public boolean hasPlayedAdGroup(int adGroupIndex) { public boolean hasPlayedAdGroup(int adGroupIndex) {
return adCounts[adGroupIndex] != C.INDEX_UNSET AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
&& adsPlayedCounts[adGroupIndex] == adCounts[adGroupIndex]; return adGroup.nextAdIndexToPlay == adGroup.count;
} }
/** /**
@ -425,6 +412,7 @@ public abstract class Timeline {
* @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[] adGroupTimesUs = adPlaybackState.adGroupTimesUs;
if (adGroupTimesUs == null) { if (adGroupTimesUs == null) {
return C.INDEX_UNSET; return C.INDEX_UNSET;
} }
@ -446,6 +434,7 @@ public abstract class Timeline {
* @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) {
long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs;
if (adGroupTimesUs == null) { if (adGroupTimesUs == null) {
return C.INDEX_UNSET; return C.INDEX_UNSET;
} }
@ -467,7 +456,7 @@ public abstract class Timeline {
* @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known. * @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known.
*/ */
public int getAdCountInAdGroup(int adGroupIndex) { public int getAdCountInAdGroup(int adGroupIndex) {
return adCounts[adGroupIndex]; return adPlaybackState.adGroups[adGroupIndex].count;
} }
/** /**
@ -478,7 +467,9 @@ public abstract class Timeline {
* @return Whether the URL for the specified ad is known. * @return Whether the URL for the specified ad is known.
*/ */
public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) { public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) {
return adIndexInAdGroup < adsLoadedCounts[adGroupIndex]; AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
return adGroup.count != C.LENGTH_UNSET
&& adGroup.states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE;
} }
/** /**
@ -490,10 +481,7 @@ public abstract class Timeline {
* @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known. * @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known.
*/ */
public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) { public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) {
if (adIndexInAdGroup >= adDurationsUs[adGroupIndex].length) { return adPlaybackState.adGroups[adGroupIndex].durationsUs[adIndexInAdGroup];
return C.TIME_UNSET;
}
return adDurationsUs[adGroupIndex][adIndexInAdGroup];
} }
/** /**
@ -501,7 +489,7 @@ public abstract class Timeline {
* microseconds. * microseconds.
*/ */
public long getAdResumePositionUs() { public long getAdResumePositionUs() {
return adResumePositionUs; return adPlaybackState.adResumePositionUs;
} }
} }

View File

@ -16,49 +16,216 @@
package com.google.android.exoplayer2.source.ads; package com.google.android.exoplayer2.source.ads;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.CheckResult;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays; import java.util.Arrays;
/** /**
* Represents the structure of ads to play and the state of loaded/played ads. * Represents ad group times relative to the start of the media 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.
*/ */
public final class AdPlaybackState { public final class AdPlaybackState {
/** /**
* The number of ad groups. * Represents a group of ads, with information about their states.
*
* <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the
* required changes.
*/ */
public static final class AdGroup {
/** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */
public final int count;
/** The URI of each ad in the ad group. */
public final Uri[] uris;
/** The state of each ad in the ad group. */
public final @AdState int[] states;
/** The durations of each ad in the ad group, in microseconds. */
public final long[] durationsUs;
/** The index of the next ad that should be played, or {@link #count} if all ads were played. */
public final int nextAdIndexToPlay;
/** Creates a new ad group with an unspecified number of ads. */
public AdGroup() {
this(
/* count= */ C.LENGTH_UNSET,
/* states= */ new int[0],
/* uris= */ new Uri[0],
/* durationsUs= */ new long[0]);
}
private AdGroup(int count, @AdState int[] states, Uri[] uris, long[] durationsUs) {
Assertions.checkArgument(states.length == uris.length);
this.count = count;
this.states = states;
this.uris = uris;
this.durationsUs = durationsUs;
int nextAdIndexToPlay;
for (nextAdIndexToPlay = 0; nextAdIndexToPlay < states.length; nextAdIndexToPlay++) {
if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE
|| states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) {
break;
}
}
this.nextAdIndexToPlay = nextAdIndexToPlay;
}
/**
* Returns a new instance with the ad count set to {@code count}. This method may only be called
* if this instance's ad count has not yet been specified.
*/
@CheckResult
public AdGroup withAdCount(int count) {
Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count);
long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
Uri[] uris = Arrays.copyOf(this.uris, count);
return new AdGroup(count, states, uris, durationsUs);
}
/**
* Returns a new instance with the specified {@code uri} set for the specified ad, and the ad
* marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link
* #AD_STATE_UNAVAILABLE}, which is the default state.
*
* <p>This instance's ad count may be unknown, in which case {@code index} must be less than the
* ad count specified later. Otherwise, {@code index} must be less than the current ad count.
*/
@CheckResult
public AdGroup withAdUri(Uri uri, int index) {
Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
Assertions.checkArgument(states[index] == AD_STATE_UNAVAILABLE);
long[] durationsUs =
this.durationsUs.length == states.length
? this.durationsUs
: copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
Uri[] uris = Arrays.copyOf(this.uris, states.length);
uris[index] = uri;
states[index] = AD_STATE_AVAILABLE;
return new AdGroup(count, states, uris, durationsUs);
}
/**
* Returns a new instance with the specified ad set to the specified {@code state}. The ad
* specified must currently either be in {@link #AD_STATE_UNAVAILABLE} or {@link
* #AD_STATE_AVAILABLE}.
*
* <p>This instance's ad count may be unknown, in which case {@code index} must be less than the
* ad count specified later. Otherwise, {@code index} must be less than the current ad count.
*/
@CheckResult
public AdGroup withAdState(@AdState int state, int index) {
Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
Assertions.checkArgument(
states[index] == AD_STATE_UNAVAILABLE || states[index] == AD_STATE_AVAILABLE);
long[] durationsUs =
this.durationsUs.length == states.length
? this.durationsUs
: copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
Uri[] uris =
this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length);
states[index] = state;
return new AdGroup(count, states, uris, durationsUs);
}
/** Returns a new instance with the specified ad durations, in microseconds. */
@CheckResult
public AdGroup withAdDurationsUs(long[] durationsUs) {
Assertions.checkArgument(count == C.LENGTH_UNSET || durationsUs.length <= this.uris.length);
if (durationsUs.length < this.uris.length) {
durationsUs = copyDurationsUsWithSpaceForAdCount(durationsUs, uris.length);
}
return new AdGroup(count, states, uris, durationsUs);
}
/**
* Returns an instance with all unavailable and available ads marked as skipped. If the ad count
* hasn't been set, it will be set to zero.
*/
@CheckResult
public AdGroup withAllAdsSkipped() {
if (count == C.LENGTH_UNSET) {
return new AdGroup(
/* count= */ 0,
/* states= */ new int[0],
/* uris= */ new Uri[0],
/* durationsUs= */ new long[0]);
}
int count = this.states.length;
@AdState int[] states = Arrays.copyOf(this.states, count);
for (int i = 0; i < count; i++) {
if (states[i] == AD_STATE_AVAILABLE || states[i] == AD_STATE_UNAVAILABLE) {
states[i] = AD_STATE_SKIPPED;
}
}
return new AdGroup(count, states, uris, durationsUs);
}
@CheckResult
private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) {
int oldStateCount = states.length;
int newStateCount = Math.max(count, oldStateCount);
states = Arrays.copyOf(states, newStateCount);
Arrays.fill(states, oldStateCount, newStateCount, AD_STATE_UNAVAILABLE);
return states;
}
@CheckResult
private static long[] copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int count) {
int oldDurationsUsCount = durationsUs.length;
int newDurationsUsCount = Math.max(count, oldDurationsUsCount);
durationsUs = Arrays.copyOf(durationsUs, newDurationsUsCount);
Arrays.fill(durationsUs, oldDurationsUsCount, newDurationsUsCount, C.TIME_UNSET);
return durationsUs;
}
}
/** Represents the state of an ad in an ad group. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
AD_STATE_UNAVAILABLE,
AD_STATE_AVAILABLE,
AD_STATE_SKIPPED,
AD_STATE_PLAYED,
AD_STATE_ERROR,
})
public @interface AdState {}
/** State for an ad that does not yet have a URL. */
public static final int AD_STATE_UNAVAILABLE = 0;
/** State for an ad that has a URL but has not yet been played. */
public static final int AD_STATE_AVAILABLE = 1;
/** State for an ad that was skipped. */
public static final int AD_STATE_SKIPPED = 2;
/** State for an ad that was played in full. */
public static final int AD_STATE_PLAYED = 3;
/** State for an ad that could not be loaded. */
public static final int AD_STATE_ERROR = 4;
/** Ad playback state with no ads. */
public static final AdPlaybackState NONE = new AdPlaybackState(new long[0]);
/** 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 * The times of ad groups, in microseconds. A final element with the value {@link
* {@link C#TIME_END_OF_SOURCE} indicates a postroll ad. * C#TIME_END_OF_SOURCE} indicates a postroll ad.
*/ */
public final long[] adGroupTimesUs; public final long[] adGroupTimesUs;
/** /** The ad groups. */
* The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET} if the number of public final AdGroup[] adGroups;
* ads is not yet known. /** The position offset in the first unplayed ad at which to begin playback, in microseconds. */
*/ public final long adResumePositionUs;
public final int[] adCounts; /** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */
/** public final long contentDurationUs;
* The number of ads loaded so far in each ad group.
*/
public final int[] adsLoadedCounts;
/**
* The number of ads played so far in each ad group.
*/
public final int[] adsPlayedCounts;
/**
* The URI of each ad in each ad group.
*/
public final Uri[][] adUris;
/**
* The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise.
*/
public long contentDurationUs;
/**
* The position offset in the first unplayed ad at which to begin playback, in microseconds.
*/
public long adResumePositionUs;
/** /**
* Creates a new ad playback state with the specified ad group times. * Creates a new ad playback state with the specified ad group times.
@ -67,84 +234,104 @@ public final class AdPlaybackState {
* {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad.
*/ */
public AdPlaybackState(long[] adGroupTimesUs) { public AdPlaybackState(long[] adGroupTimesUs) {
this.adGroupTimesUs = adGroupTimesUs; int count = adGroupTimesUs.length;
adGroupCount = adGroupTimesUs.length; adGroupCount = count;
adsPlayedCounts = new int[adGroupCount]; this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count);
adCounts = new int[adGroupCount]; this.adGroups = new AdGroup[count];
Arrays.fill(adCounts, C.LENGTH_UNSET); for (int i = 0; i < count; i++) {
adUris = new Uri[adGroupCount][]; adGroups[i] = new AdGroup();
Arrays.fill(adUris, new Uri[0]); }
adsLoadedCounts = new int[adGroupTimesUs.length]; adResumePositionUs = 0;
contentDurationUs = C.TIME_UNSET; contentDurationUs = C.TIME_UNSET;
} }
private AdPlaybackState(long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts, private AdPlaybackState(
int[] adsPlayedCounts, Uri[][] adUris, long contentDurationUs, long adResumePositionUs) { long[] adGroupTimesUs, AdGroup[] adGroups, long adResumePositionUs, long contentDurationUs) {
adGroupCount = adGroups.length;
this.adGroupTimesUs = adGroupTimesUs; this.adGroupTimesUs = adGroupTimesUs;
this.adCounts = adCounts; this.adGroups = adGroups;
this.adsLoadedCounts = adsLoadedCounts; this.adResumePositionUs = adResumePositionUs;
this.adsPlayedCounts = adsPlayedCounts;
this.adUris = adUris;
this.contentDurationUs = contentDurationUs; this.contentDurationUs = contentDurationUs;
this.adResumePositionUs = adResumePositionUs;
adGroupCount = adGroupTimesUs.length;
} }
/** /**
* Returns a deep copy of this instance. * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}.
* The ad count must be greater than zero.
*/ */
public AdPlaybackState copy() { @CheckResult
Uri[][] adUris = new Uri[adGroupTimesUs.length][]; public AdPlaybackState withAdCount(int adGroupIndex, int adCount) {
for (int i = 0; i < this.adUris.length; i++) { Assertions.checkArgument(adCount > 0);
adUris[i] = Arrays.copyOf(this.adUris[i], this.adUris[i].length); if (adGroups[adGroupIndex].count == adCount) {
return this;
} }
return new AdPlaybackState(Arrays.copyOf(adGroupTimesUs, adGroupCount), AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
Arrays.copyOf(adCounts, adGroupCount), Arrays.copyOf(adsLoadedCounts, adGroupCount), adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount);
Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, contentDurationUs, return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
adResumePositionUs); }
/** Returns an instance with the specified ad URI. */
@CheckResult
public AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri) {
AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdUri(uri, adIndexInAdGroup);
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
/** Returns an instance with the specified ad marked as played. */
@CheckResult
public AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) {
AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_PLAYED, adIndexInAdGroup);
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
/** Returns an instance with the specified ad marked as having a load error. */
@CheckResult
public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {
AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup);
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
} }
/** /**
* Sets the number of ads in the specified ad group. * Returns an instance with all ads in the specified ad group skipped (except for those already
* marked as played or in the error state).
*/ */
public void setAdCount(int adGroupIndex, int adCount) { @CheckResult
adCounts[adGroupIndex] = adCount; public AdPlaybackState withSkippedAdGroup(int adGroupIndex) {
AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
adGroups[adGroupIndex] = adGroups[adGroupIndex].withAllAdsSkipped();
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
} }
/** /** Returns an instance with the specified ad durations, in microseconds. */
* Adds an ad to the specified ad group. @CheckResult
*/ public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) {
public void addAdUri(int adGroupIndex, Uri uri) { AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
int adIndexInAdGroup = adUris[adGroupIndex].length; for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) {
adUris[adGroupIndex] = Arrays.copyOf(adUris[adGroupIndex], adIndexInAdGroup + 1); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]);
adUris[adGroupIndex][adIndexInAdGroup] = uri;
adsLoadedCounts[adGroupIndex]++;
}
/**
* Marks the last ad in the specified ad group as played.
*/
public void playedAd(int adGroupIndex) {
adResumePositionUs = 0;
adsPlayedCounts[adGroupIndex]++;
}
/**
* Marks all ads in the specified ad group as played.
*/
public void playedAdGroup(int adGroupIndex) {
adResumePositionUs = 0;
if (adCounts[adGroupIndex] == C.LENGTH_UNSET) {
adCounts[adGroupIndex] = 0;
} }
adsPlayedCounts[adGroupIndex] = adCounts[adGroupIndex]; return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
} }
/** /** Returns an instance with the specified ad resume position, in microseconds. */
* Sets the position offset in the first unplayed ad at which to begin playback, in microseconds. @CheckResult
*/ public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) {
public void setAdResumePositionUs(long adResumePositionUs) { if (this.adResumePositionUs == adResumePositionUs) {
this.adResumePositionUs = adResumePositionUs; return this;
} else {
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
}
/** Returns an instance with the specified content duration, in microseconds. */
@CheckResult
public AdPlaybackState withContentDurationUs(long contentDurationUs) {
if (this.contentDurationUs == contentDurationUs) {
return this;
} else {
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
} }
} }

View File

@ -222,7 +222,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
int adGroupIndex = id.adGroupIndex; int adGroupIndex = id.adGroupIndex;
int adIndexInAdGroup = id.adIndexInAdGroup; int adIndexInAdGroup = id.adIndexInAdGroup;
if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {
Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; Uri adUri = adPlaybackState.adGroups[id.adGroupIndex].uris[id.adIndexInAdGroup];
MediaSource adMediaSource = MediaSource adMediaSource =
adMediaSourceFactory.createMediaSource(adUri, eventHandler, eventListener); adMediaSourceFactory.createMediaSource(adUri, eventHandler, eventListener);
int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; int oldAdCount = adGroupMediaSources[id.adGroupIndex].length;
@ -337,11 +337,11 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
private void maybeUpdateSourceInfo() { private void maybeUpdateSourceInfo() {
if (adPlaybackState != null && contentTimeline != null) { if (adPlaybackState != null && contentTimeline != null) {
Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
: new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs, Timeline timeline =
adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts, adPlaybackState.adGroupCount == 0
adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs, ? contentTimeline
adPlaybackState.contentDurationUs); : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState);
listener.onSourceInfoRefreshed(this, timeline, contentManifest); listener.onSourceInfoRefreshed(this, timeline, contentManifest);
} }
} }

View File

@ -25,54 +25,32 @@ import com.google.android.exoplayer2.util.Assertions;
*/ */
/* package */ final class SinglePeriodAdTimeline extends ForwardingTimeline { /* package */ final class SinglePeriodAdTimeline extends ForwardingTimeline {
private final long[] adGroupTimesUs; private final AdPlaybackState adPlaybackState;
private final int[] adCounts;
private final int[] adsLoadedCounts;
private final int[] adsPlayedCounts;
private final long[][] adDurationsUs;
private final long adResumePositionUs;
private final long contentDurationUs;
/** /**
* Creates a new timeline with a single period containing the specified ads. * Creates a new timeline with a single period containing ads.
* *
* @param contentTimeline The timeline of the content alongside which ads will be played. It must * @param contentTimeline The timeline of the content alongside which ads will be played. It must
* have one window and one period. * have one window and one period.
* @param adGroupTimesUs The times of ad groups relative to the start of the period, in * @param adPlaybackState The state of the period's ads.
* microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
* the period has a postroll ad.
* @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET}
* if the number of ads is not yet known.
* @param adsLoadedCounts The number of ads loaded so far in each ad group.
* @param adsPlayedCounts The number of ads played so far in each ad group.
* @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element
* may be {@link C#TIME_UNSET} if the duration is not yet known.
* @param adResumePositionUs The position offset in the earliest unplayed ad at which to begin
* playback, in microseconds.
* @param contentDurationUs The content duration in microseconds, if known. {@link C#TIME_UNSET}
* otherwise.
*/ */
public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts, public SinglePeriodAdTimeline(Timeline contentTimeline, AdPlaybackState adPlaybackState) {
int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs,
long contentDurationUs) {
super(contentTimeline); super(contentTimeline);
Assertions.checkState(contentTimeline.getPeriodCount() == 1); Assertions.checkState(contentTimeline.getPeriodCount() == 1);
Assertions.checkState(contentTimeline.getWindowCount() == 1); Assertions.checkState(contentTimeline.getWindowCount() == 1);
this.adGroupTimesUs = adGroupTimesUs; this.adPlaybackState = adPlaybackState;
this.adCounts = adCounts;
this.adsLoadedCounts = adsLoadedCounts;
this.adsPlayedCounts = adsPlayedCounts;
this.adDurationsUs = adDurationsUs;
this.adResumePositionUs = adResumePositionUs;
this.contentDurationUs = contentDurationUs;
} }
@Override @Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
timeline.getPeriod(periodIndex, period, setIds); timeline.getPeriod(periodIndex, period, setIds);
period.set(period.id, period.uid, period.windowIndex, period.durationUs, period.set(
period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts, period.id,
adDurationsUs, adResumePositionUs); period.uid,
period.windowIndex,
period.durationUs,
period.getPositionInWindowUs(),
adPlaybackState);
return period; return period;
} }
@ -81,7 +59,7 @@ import com.google.android.exoplayer2.util.Assertions;
long defaultPositionProjectionUs) { long defaultPositionProjectionUs) {
window = super.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); window = super.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
if (window.durationUs == C.TIME_UNSET) { if (window.durationUs == C.TIME_UNSET) {
window.durationUs = contentDurationUs; window.durationUs = adPlaybackState.contentDurationUs;
} }
return window; return window;
} }

View File

@ -0,0 +1,125 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.ads;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Unit test for {@link AdPlaybackState}. */
@RunWith(RobolectricTestRunner.class)
@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
public final class AdPlaybackStateTest {
private static final long[] TEST_AD_GROUP_TMES_US = new long[] {0, C.msToUs(10_000)};
private static final Uri TEST_URI = Uri.EMPTY;
private AdPlaybackState state;
@Before
public void setUp() {
state = new AdPlaybackState(TEST_AD_GROUP_TMES_US);
}
@Test
public void testSetAdCount() {
assertThat(state.adGroups[0].count).isEqualTo(C.LENGTH_UNSET);
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
assertThat(state.adGroups[0].count).isEqualTo(1);
}
@Test
public void testSetAdUriBeforeAdCount() {
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2);
assertThat(state.adGroups[0].uris[0]).isNull();
assertThat(state.adGroups[0].states[0]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(state.adGroups[0].uris[1]).isSameAs(TEST_URI);
assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE);
}
@Test
public void testSetAdErrorBeforeAdCount() {
state = state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2);
assertThat(state.adGroups[0].uris[0]).isNull();
assertThat(state.adGroups[0].states[0]).isEqualTo(AdPlaybackState.AD_STATE_ERROR);
assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
}
@Test
public void testInitialNextAdIndexToPlay() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);
assertThat(state.adGroups[0].nextAdIndexToPlay).isEqualTo(0);
}
@Test
public void testNextAdIndexToPlayWithPlayedAd() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
assertThat(state.adGroups[0].nextAdIndexToPlay).isEqualTo(1);
assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE);
}
@Test
public void testNextAdIndexToPlaySkipsErrorAds() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
state = state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);
assertThat(state.adGroups[0].nextAdIndexToPlay).isEqualTo(2);
}
@Test
public void testSetAdStateTwiceThrows() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
try {
state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
fail();
} catch (Exception e) {
// Expected.
}
}
@Test
public void testSkipAllWithoutAdCount() {
state = state.withSkippedAdGroup(0);
state = state.withSkippedAdGroup(1);
assertThat(state.adGroups[0].count).isEqualTo(0);
assertThat(state.adGroups[1].count).isEqualTo(0);
}
}

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
@ -177,21 +178,21 @@ public final class FakeTimeline extends Timeline {
} else { } else {
int adGroups = windowDefinition.adGroupsPerPeriodCount; int adGroups = windowDefinition.adGroupsPerPeriodCount;
long[] adGroupTimesUs = new long[adGroups]; long[] adGroupTimesUs = new long[adGroups];
int[] adCounts = new int[adGroups];
int[] adLoadedAndPlayedCounts = new int[adGroups];
long[][] adDurationsUs = new long[adGroups][];
long adResumePositionUs = 0;
long adGroupOffset = adGroups > 1 ? periodDurationUs / (adGroups - 1) : 0; long adGroupOffset = adGroups > 1 ? periodDurationUs / (adGroups - 1) : 0;
for (int i = 0; i < adGroups; i++) { for (int i = 0; i < adGroups; i++) {
adGroupTimesUs[i] = i * adGroupOffset; adGroupTimesUs[i] = i * adGroupOffset;
adCounts[i] = windowDefinition.adsPerAdGroupCount; }
adLoadedAndPlayedCounts[i] = 0; AdPlaybackState adPlaybackState = new AdPlaybackState(adGroupTimesUs);
adDurationsUs[i] = new long[adCounts[i]]; long[][] adDurationsUs = new long[adGroups][];
for (int i = 0; i < adGroups; i++) {
int adCount = windowDefinition.adsPerAdGroupCount;
adPlaybackState = adPlaybackState.withAdCount(i, adCount);
adDurationsUs[i] = new long[adCount];
Arrays.fill(adDurationsUs[i], AD_DURATION_US); Arrays.fill(adDurationsUs[i], AD_DURATION_US);
} }
return period.set(id, uid, windowIndex, periodDurationUs, positionInWindowUs, adGroupTimesUs, adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
adCounts, adLoadedAndPlayedCounts, adLoadedAndPlayedCounts, adDurationsUs, return period.set(
adResumePositionUs); id, uid, windowIndex, periodDurationUs, positionInWindowUs, adPlaybackState);
} }
} }