diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 743e68a9a3..b632e7ba84 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -48,6 +48,7 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; 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.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; @@ -393,7 +394,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A maybeNotifyAdError(); if (adPlaybackState != null) { // Pass the ad playback state to the player, and resume ads if necessary. - eventListener.onAdPlaybackState(adPlaybackState.copy()); + eventListener.onAdPlaybackState(adPlaybackState); if (imaPausedContent && player.getPlayWhenReady()) { adsManager.resume(); } @@ -409,7 +410,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void detachPlayer() { if (adsManager != null && imaPausedContent) { - adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0); + adPlaybackState = + adPlaybackState.withAdResumePositionUs( + playingAd ? C.msToUs(player.getCurrentPosition()) : 0); adsManager.pause(); } lastAdProgress = getAdProgress(); @@ -474,7 +477,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); } - adPlaybackState.setAdCount(adGroupIndex, adCount); + adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); updateAdPlaybackState(); if (adGroupIndex != expectedAdGroupIndex) { Log.w( @@ -589,8 +592,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "loadAd in ad group " + adGroupIndex); } - adPlaybackState.addAdUri(adGroupIndex, Uri.parse(adUriString)); - if (adPlaybackState.adsLoadedCounts[adGroupIndex] == adPlaybackState.adCounts[adGroupIndex]) { + int adIndexInAdGroup = getAdIndexInAdGroupToLoad(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 // unexpectedly not triggered. expectedAdGroupIndex++; @@ -693,7 +698,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long contentDurationUs = timeline.getPeriod(0, period).durationUs; contentDurationMs = C.usToMs(contentDurationUs); if (contentDurationUs != C.TIME_UNSET) { - adPlaybackState.contentDurationUs = contentDurationUs; + adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); } updateImaStateForPlayerState(); } @@ -748,7 +753,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (sentContentComplete) { for (int i = 0; i < adPlaybackState.adGroupCount; i++) { if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { - adPlaybackState.playedAdGroup(i); + adPlaybackState = adPlaybackState.withSkippedAdGroup(i); } } updateAdPlaybackState(); @@ -790,7 +795,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } else /* adGroupIndexForPosition > 0 */ { // Skip ad groups before the one at or immediately before the playback position. 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 // 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) { - adPlaybackState.playedAdGroup(adGroupIndex); + adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex); adGroupIndex = C.INDEX_UNSET; updateAdPlaybackState(); } @@ -879,7 +884,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void stopAdInternal() { Assertions.checkState(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(); if (!playingAd) { adGroupIndex = C.INDEX_UNSET; @@ -901,7 +909,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void updateAdPlaybackState() { // Ignore updates while detached. When a player is attached it will receive the latest state. 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; } + + /** + * 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; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 34547a1c25..8bc93ae243 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -326,7 +326,7 @@ import com.google.android.exoplayer2.util.Assertions; if (adGroupIndex == C.INDEX_UNSET) { return new MediaPeriodId(periodIndex); } else { - int adIndexInAdGroup = period.getPlayedAdCount(adGroupIndex); + int adIndexInAdGroup = period.getNextAdIndexToPlay(adGroupIndex); return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); } } @@ -502,7 +502,7 @@ import com.google.android.exoplayer2.util.Assertions; .getPeriod(id.periodIndex, period) .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); long startPositionUs = - adIndexInAdGroup == period.getPlayedAdCount(adGroupIndex) + adIndexInAdGroup == period.getNextAdIndexToPlay(adGroupIndex) ? period.getAdResumePositionUs() : 0; return new MediaPeriodInfo( @@ -547,7 +547,7 @@ import com.google.android.exoplayer2.util.Assertions; boolean isLastAd = 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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 4a1f2bfd1e..26c2cc3e83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import android.util.Pair; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.util.Assertions; /** @@ -278,12 +279,7 @@ public abstract class Timeline { public long durationUs; private long positionInWindowUs; - private long[] adGroupTimesUs; - private int[] adCounts; - private int[] adsLoadedCounts; - private int[] adsPlayedCounts; - private long[][] adDurationsUs; - private long adResumePositionUs; + private AdPlaybackState adPlaybackState; /** * 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, long positionInWindowUs) { - return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null, - null, C.TIME_UNSET); + return set(id, uid, windowIndex, durationUs, positionInWindowUs, AdPlaybackState.NONE); } /** @@ -315,33 +310,23 @@ public abstract class Timeline { * @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 * period is not within the window. - * @param adGroupTimesUs The times of ad groups relative to the start of the period, in - * 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 first unplayed ad at which to begin - * playback, in microseconds. + * @param adPlaybackState The state of the period's ads, or {@link AdPlaybackState#NONE} if + * there are no ads. * @return This period, for convenience. */ - public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs, long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts, - int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs) { + public Period set( + Object id, + Object uid, + int windowIndex, + long durationUs, + long positionInWindowUs, + AdPlaybackState adPlaybackState) { this.id = id; this.uid = uid; this.windowIndex = windowIndex; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; - this.adGroupTimesUs = adGroupTimesUs; - this.adCounts = adCounts; - this.adsLoadedCounts = adsLoadedCounts; - this.adsPlayedCounts = adsPlayedCounts; - this.adDurationsUs = adDurationsUs; - this.adResumePositionUs = adResumePositionUs; + this.adPlaybackState = adPlaybackState; return this; } @@ -381,7 +366,7 @@ public abstract class Timeline { * Returns the number of ad groups in the period. */ 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. */ 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. - * @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) { - return adsPlayedCounts[adGroupIndex]; + public int getNextAdIndexToPlay(int 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. */ public boolean hasPlayedAdGroup(int adGroupIndex) { - return adCounts[adGroupIndex] != C.INDEX_UNSET - && adsPlayedCounts[adGroupIndex] == adCounts[adGroupIndex]; + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[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}. */ public int getAdGroupIndexForPositionUs(long positionUs) { + long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs; if (adGroupTimesUs == null) { return C.INDEX_UNSET; } @@ -446,6 +434,7 @@ public abstract class Timeline { * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexAfterPositionUs(long positionUs) { + long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs; if (adGroupTimesUs == null) { 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. */ 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. */ 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. */ public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) { - if (adIndexInAdGroup >= adDurationsUs[adGroupIndex].length) { - return C.TIME_UNSET; - } - return adDurationsUs[adGroupIndex][adIndexInAdGroup]; + return adPlaybackState.adGroups[adGroupIndex].durationsUs[adIndexInAdGroup]; } /** @@ -501,7 +489,7 @@ public abstract class Timeline { * microseconds. */ public long getAdResumePositionUs() { - return adResumePositionUs; + return adPlaybackState.adResumePositionUs; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 58fa149b59..0bd6c9f29f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -16,49 +16,216 @@ package com.google.android.exoplayer2.source.ads; 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.util.Assertions; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; 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. + * + *
Instances are immutable. Call the {@code with*} methods to get new instances that have the + * required changes. */ public final class AdPlaybackState { /** - * The number of ad groups. + * Represents a group of ads, with information about their states. + * + *
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. + * + *
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}. + * + *
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;
/**
- * 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. A final element with the value {@link
+ * C#TIME_END_OF_SOURCE} indicates a postroll ad.
*/
public final long[] adGroupTimesUs;
- /**
- * 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.
- */
- public final int[] adCounts;
- /**
- * 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;
+ /** The ad groups. */
+ public final AdGroup[] adGroups;
+ /** The position offset in the first unplayed ad at which to begin playback, in microseconds. */
+ public final long adResumePositionUs;
+ /** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */
+ public final long contentDurationUs;
/**
* 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.
*/
public AdPlaybackState(long[] adGroupTimesUs) {
- this.adGroupTimesUs = adGroupTimesUs;
- adGroupCount = adGroupTimesUs.length;
- adsPlayedCounts = new int[adGroupCount];
- adCounts = new int[adGroupCount];
- Arrays.fill(adCounts, C.LENGTH_UNSET);
- adUris = new Uri[adGroupCount][];
- Arrays.fill(adUris, new Uri[0]);
- adsLoadedCounts = new int[adGroupTimesUs.length];
+ int count = adGroupTimesUs.length;
+ adGroupCount = count;
+ this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count);
+ this.adGroups = new AdGroup[count];
+ for (int i = 0; i < count; i++) {
+ adGroups[i] = new AdGroup();
+ }
+ adResumePositionUs = 0;
contentDurationUs = C.TIME_UNSET;
}
- private AdPlaybackState(long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts,
- int[] adsPlayedCounts, Uri[][] adUris, long contentDurationUs, long adResumePositionUs) {
+ private AdPlaybackState(
+ long[] adGroupTimesUs, AdGroup[] adGroups, long adResumePositionUs, long contentDurationUs) {
+ adGroupCount = adGroups.length;
this.adGroupTimesUs = adGroupTimesUs;
- this.adCounts = adCounts;
- this.adsLoadedCounts = adsLoadedCounts;
- this.adsPlayedCounts = adsPlayedCounts;
- this.adUris = adUris;
+ this.adGroups = adGroups;
+ this.adResumePositionUs = adResumePositionUs;
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() {
- Uri[][] adUris = new Uri[adGroupTimesUs.length][];
- for (int i = 0; i < this.adUris.length; i++) {
- adUris[i] = Arrays.copyOf(this.adUris[i], this.adUris[i].length);
+ @CheckResult
+ public AdPlaybackState withAdCount(int adGroupIndex, int adCount) {
+ Assertions.checkArgument(adCount > 0);
+ if (adGroups[adGroupIndex].count == adCount) {
+ return this;
}
- return new AdPlaybackState(Arrays.copyOf(adGroupTimesUs, adGroupCount),
- Arrays.copyOf(adCounts, adGroupCount), Arrays.copyOf(adsLoadedCounts, adGroupCount),
- Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, contentDurationUs,
- adResumePositionUs);
+ AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** 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) {
- adCounts[adGroupIndex] = adCount;
+ @CheckResult
+ 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);
}
- /**
- * Adds an ad to the specified ad group.
- */
- public void addAdUri(int adGroupIndex, Uri uri) {
- int adIndexInAdGroup = adUris[adGroupIndex].length;
- adUris[adGroupIndex] = Arrays.copyOf(adUris[adGroupIndex], adIndexInAdGroup + 1);
- 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;
+ /** Returns an instance with the specified ad durations, in microseconds. */
+ @CheckResult
+ public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) {
+ AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
+ for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) {
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]);
}
- adsPlayedCounts[adGroupIndex] = adCounts[adGroupIndex];
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
- /**
- * Sets the position offset in the first unplayed ad at which to begin playback, in microseconds.
- */
- public void setAdResumePositionUs(long adResumePositionUs) {
- this.adResumePositionUs = adResumePositionUs;
+ /** Returns an instance with the specified ad resume position, in microseconds. */
+ @CheckResult
+ public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) {
+ if (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);
+ }
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
index bf76bbf9ab..cadd293bee 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
@@ -222,7 +222,7 @@ public final class AdsMediaSource extends CompositeMediaSource