1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.source.ads;
17 
18 import android.net.Uri;
19 import androidx.annotation.CheckResult;
20 import androidx.annotation.IntDef;
21 import androidx.annotation.Nullable;
22 import com.google.android.exoplayer2.C;
23 import com.google.android.exoplayer2.util.Assertions;
24 import com.google.android.exoplayer2.util.Util;
25 import java.lang.annotation.Documented;
26 import java.lang.annotation.Retention;
27 import java.lang.annotation.RetentionPolicy;
28 import java.util.Arrays;
29 import org.checkerframework.checker.nullness.compatqual.NullableType;
30 
31 /**
32  * Represents ad group times and information on the state and URIs of ads within each ad group.
33  *
34  * <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the
35  * required changes.
36  */
37 public final class AdPlaybackState {
38 
39   /**
40    * Represents a group of ads, with information about their states.
41    *
42    * <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the
43    * required changes.
44    */
45   public static final class AdGroup {
46 
47     /** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */
48     public final int count;
49     /** The URI of each ad in the ad group. */
50     public final @NullableType Uri[] uris;
51     /** The state of each ad in the ad group. */
52     @AdState public final int[] states;
53     /** The durations of each ad in the ad group, in microseconds. */
54     public final long[] durationsUs;
55 
56     /** Creates a new ad group with an unspecified number of ads. */
AdGroup()57     public AdGroup() {
58       this(
59           /* count= */ C.LENGTH_UNSET,
60           /* states= */ new int[0],
61           /* uris= */ new Uri[0],
62           /* durationsUs= */ new long[0]);
63     }
64 
AdGroup( int count, @AdState int[] states, @NullableType Uri[] uris, long[] durationsUs)65     private AdGroup(
66         int count, @AdState int[] states, @NullableType Uri[] uris, long[] durationsUs) {
67       Assertions.checkArgument(states.length == uris.length);
68       this.count = count;
69       this.states = states;
70       this.uris = uris;
71       this.durationsUs = durationsUs;
72     }
73 
74     /**
75      * Returns the index of the first ad in the ad group that should be played, or {@link #count} if
76      * no ads should be played.
77      */
getFirstAdIndexToPlay()78     public int getFirstAdIndexToPlay() {
79       return getNextAdIndexToPlay(-1);
80     }
81 
82     /**
83      * Returns the index of the next ad in the ad group that should be played after playing {@code
84      * lastPlayedAdIndex}, or {@link #count} if no later ads should be played.
85      */
getNextAdIndexToPlay(int lastPlayedAdIndex)86     public int getNextAdIndexToPlay(int lastPlayedAdIndex) {
87       int nextAdIndexToPlay = lastPlayedAdIndex + 1;
88       while (nextAdIndexToPlay < states.length) {
89         if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE
90             || states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) {
91           break;
92         }
93         nextAdIndexToPlay++;
94       }
95       return nextAdIndexToPlay;
96     }
97 
98     /** Returns whether the ad group has at least one ad that still needs to be played. */
hasUnplayedAds()99     public boolean hasUnplayedAds() {
100       return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count;
101     }
102 
103     @Override
equals(@ullable Object o)104     public boolean equals(@Nullable Object o) {
105       if (this == o) {
106         return true;
107       }
108       if (o == null || getClass() != o.getClass()) {
109         return false;
110       }
111       AdGroup adGroup = (AdGroup) o;
112       return count == adGroup.count
113           && Arrays.equals(uris, adGroup.uris)
114           && Arrays.equals(states, adGroup.states)
115           && Arrays.equals(durationsUs, adGroup.durationsUs);
116     }
117 
118     @Override
hashCode()119     public int hashCode() {
120       int result = count;
121       result = 31 * result + Arrays.hashCode(uris);
122       result = 31 * result + Arrays.hashCode(states);
123       result = 31 * result + Arrays.hashCode(durationsUs);
124       return result;
125     }
126 
127     /**
128      * Returns a new instance with the ad count set to {@code count}. This method may only be called
129      * if this instance's ad count has not yet been specified.
130      */
131     @CheckResult
withAdCount(int count)132     public AdGroup withAdCount(int count) {
133       Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count);
134       @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count);
135       long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
136       @NullableType Uri[] uris = Arrays.copyOf(this.uris, count);
137       return new AdGroup(count, states, uris, durationsUs);
138     }
139 
140     /**
141      * Returns a new instance with the specified {@code uri} set for the specified ad, and the ad
142      * marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link
143      * #AD_STATE_UNAVAILABLE}, which is the default state.
144      *
145      * <p>This instance's ad count may be unknown, in which case {@code index} must be less than the
146      * ad count specified later. Otherwise, {@code index} must be less than the current ad count.
147      */
148     @CheckResult
withAdUri(Uri uri, int index)149     public AdGroup withAdUri(Uri uri, int index) {
150       Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
151       @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
152       Assertions.checkArgument(states[index] == AD_STATE_UNAVAILABLE);
153       long[] durationsUs =
154           this.durationsUs.length == states.length
155               ? this.durationsUs
156               : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
157       @NullableType Uri[] uris = Arrays.copyOf(this.uris, states.length);
158       uris[index] = uri;
159       states[index] = AD_STATE_AVAILABLE;
160       return new AdGroup(count, states, uris, durationsUs);
161     }
162 
163     /**
164      * Returns a new instance with the specified ad set to the specified {@code state}. The ad
165      * specified must currently either be in {@link #AD_STATE_UNAVAILABLE} or {@link
166      * #AD_STATE_AVAILABLE}.
167      *
168      * <p>This instance's ad count may be unknown, in which case {@code index} must be less than the
169      * ad count specified later. Otherwise, {@code index} must be less than the current ad count.
170      */
171     @CheckResult
withAdState(@dState int state, int index)172     public AdGroup withAdState(@AdState int state, int index) {
173       Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
174       @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
175       Assertions.checkArgument(
176           states[index] == AD_STATE_UNAVAILABLE
177               || states[index] == AD_STATE_AVAILABLE
178               || states[index] == state);
179       long[] durationsUs =
180           this.durationsUs.length == states.length
181               ? this.durationsUs
182               : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
183       @NullableType
184       Uri[] uris =
185           this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length);
186       states[index] = state;
187       return new AdGroup(count, states, uris, durationsUs);
188     }
189 
190     /** Returns a new instance with the specified ad durations, in microseconds. */
191     @CheckResult
withAdDurationsUs(long[] durationsUs)192     public AdGroup withAdDurationsUs(long[] durationsUs) {
193       Assertions.checkArgument(count == C.LENGTH_UNSET || durationsUs.length <= this.uris.length);
194       if (durationsUs.length < this.uris.length) {
195         durationsUs = copyDurationsUsWithSpaceForAdCount(durationsUs, uris.length);
196       }
197       return new AdGroup(count, states, uris, durationsUs);
198     }
199 
200     /**
201      * Returns an instance with all unavailable and available ads marked as skipped. If the ad count
202      * hasn't been set, it will be set to zero.
203      */
204     @CheckResult
withAllAdsSkipped()205     public AdGroup withAllAdsSkipped() {
206       if (count == C.LENGTH_UNSET) {
207         return new AdGroup(
208             /* count= */ 0,
209             /* states= */ new int[0],
210             /* uris= */ new Uri[0],
211             /* durationsUs= */ new long[0]);
212       }
213       int count = this.states.length;
214       @AdState int[] states = Arrays.copyOf(this.states, count);
215       for (int i = 0; i < count; i++) {
216         if (states[i] == AD_STATE_AVAILABLE || states[i] == AD_STATE_UNAVAILABLE) {
217           states[i] = AD_STATE_SKIPPED;
218         }
219       }
220       return new AdGroup(count, states, uris, durationsUs);
221     }
222 
223     @CheckResult
copyStatesWithSpaceForAdCount(@dState int[] states, int count)224     private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) {
225       int oldStateCount = states.length;
226       int newStateCount = Math.max(count, oldStateCount);
227       states = Arrays.copyOf(states, newStateCount);
228       Arrays.fill(states, oldStateCount, newStateCount, AD_STATE_UNAVAILABLE);
229       return states;
230     }
231 
232     @CheckResult
copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int count)233     private static long[] copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int count) {
234       int oldDurationsUsCount = durationsUs.length;
235       int newDurationsUsCount = Math.max(count, oldDurationsUsCount);
236       durationsUs = Arrays.copyOf(durationsUs, newDurationsUsCount);
237       Arrays.fill(durationsUs, oldDurationsUsCount, newDurationsUsCount, C.TIME_UNSET);
238       return durationsUs;
239     }
240   }
241 
242   /**
243    * Represents the state of an ad in an ad group. One of {@link #AD_STATE_UNAVAILABLE}, {@link
244    * #AD_STATE_AVAILABLE}, {@link #AD_STATE_SKIPPED}, {@link #AD_STATE_PLAYED} or {@link
245    * #AD_STATE_ERROR}.
246    */
247   @Documented
248   @Retention(RetentionPolicy.SOURCE)
249   @IntDef({
250     AD_STATE_UNAVAILABLE,
251     AD_STATE_AVAILABLE,
252     AD_STATE_SKIPPED,
253     AD_STATE_PLAYED,
254     AD_STATE_ERROR,
255   })
256   public @interface AdState {}
257   /** State for an ad that does not yet have a URL. */
258   public static final int AD_STATE_UNAVAILABLE = 0;
259   /** State for an ad that has a URL but has not yet been played. */
260   public static final int AD_STATE_AVAILABLE = 1;
261   /** State for an ad that was skipped. */
262   public static final int AD_STATE_SKIPPED = 2;
263   /** State for an ad that was played in full. */
264   public static final int AD_STATE_PLAYED = 3;
265   /** State for an ad that could not be loaded. */
266   public static final int AD_STATE_ERROR = 4;
267 
268   /** Ad playback state with no ads. */
269   public static final AdPlaybackState NONE = new AdPlaybackState();
270 
271   /** The number of ad groups. */
272   public final int adGroupCount;
273   /**
274    * The times of ad groups, in microseconds, relative to the start of the {@link
275    * com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with the value
276    * {@link C#TIME_END_OF_SOURCE} indicates a postroll ad.
277    */
278   public final long[] adGroupTimesUs;
279   /** The ad groups. */
280   public final AdGroup[] adGroups;
281   /** The position offset in the first unplayed ad at which to begin playback, in microseconds. */
282   public final long adResumePositionUs;
283   /** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */
284   public final long contentDurationUs;
285 
286   /**
287    * Creates a new ad playback state with the specified ad group times.
288    *
289    * @param adGroupTimesUs The times of ad groups in microseconds, relative to the start of the
290    *     {@link com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with
291    *     the value {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad.
292    */
AdPlaybackState(long... adGroupTimesUs)293   public AdPlaybackState(long... adGroupTimesUs) {
294     int count = adGroupTimesUs.length;
295     adGroupCount = count;
296     this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count);
297     this.adGroups = new AdGroup[count];
298     for (int i = 0; i < count; i++) {
299       adGroups[i] = new AdGroup();
300     }
301     adResumePositionUs = 0;
302     contentDurationUs = C.TIME_UNSET;
303   }
304 
AdPlaybackState( long[] adGroupTimesUs, AdGroup[] adGroups, long adResumePositionUs, long contentDurationUs)305   private AdPlaybackState(
306       long[] adGroupTimesUs, AdGroup[] adGroups, long adResumePositionUs, long contentDurationUs) {
307     adGroupCount = adGroups.length;
308     this.adGroupTimesUs = adGroupTimesUs;
309     this.adGroups = adGroups;
310     this.adResumePositionUs = adResumePositionUs;
311     this.contentDurationUs = contentDurationUs;
312   }
313 
314   /**
315    * Returns the index of the ad group at or before {@code positionUs}, if that ad group is
316    * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no
317    * ads remaining to be played, or if there is no such ad group.
318    *
319    * @param positionUs The period position at or before which to find an ad group, in microseconds,
320    *     or {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any
321    *     unplayed postroll ad group will be returned).
322    * @param periodDurationUs The duration of the containing timeline period, in microseconds, or
323    *     {@link C#TIME_UNSET} if not known.
324    * @return The index of the ad group, or {@link C#INDEX_UNSET}.
325    */
getAdGroupIndexForPositionUs(long positionUs, long periodDurationUs)326   public int getAdGroupIndexForPositionUs(long positionUs, long periodDurationUs) {
327     // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
328     // In practice we expect there to be few ad groups so the search shouldn't be expensive.
329     int index = adGroupTimesUs.length - 1;
330     while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) {
331       index--;
332     }
333     return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET;
334   }
335 
336   /**
337    * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be
338    * played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
339    *
340    * @param positionUs The period position after which to find an ad group, in microseconds, or
341    *     {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad
342    *     group after the position).
343    * @param periodDurationUs The duration of the containing timeline period, in microseconds, or
344    *     {@link C#TIME_UNSET} if not known.
345    * @return The index of the ad group, or {@link C#INDEX_UNSET}.
346    */
getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs)347   public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) {
348     if (positionUs == C.TIME_END_OF_SOURCE
349         || (periodDurationUs != C.TIME_UNSET && positionUs >= periodDurationUs)) {
350       return C.INDEX_UNSET;
351     }
352     // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
353     // In practice we expect there to be few ad groups so the search shouldn't be expensive.
354     int index = 0;
355     while (index < adGroupTimesUs.length
356         && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE
357         && (positionUs >= adGroupTimesUs[index] || !adGroups[index].hasUnplayedAds())) {
358       index++;
359     }
360     return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
361   }
362 
363   /**
364    * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}.
365    * The ad count must be greater than zero.
366    */
367   @CheckResult
withAdCount(int adGroupIndex, int adCount)368   public AdPlaybackState withAdCount(int adGroupIndex, int adCount) {
369     Assertions.checkArgument(adCount > 0);
370     if (adGroups[adGroupIndex].count == adCount) {
371       return this;
372     }
373     AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
374     adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount);
375     return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
376   }
377 
378   /** Returns an instance with the specified ad URI. */
379   @CheckResult
withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri)380   public AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri) {
381     AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
382     adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdUri(uri, adIndexInAdGroup);
383     return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
384   }
385 
386   /** Returns an instance with the specified ad marked as played. */
387   @CheckResult
withPlayedAd(int adGroupIndex, int adIndexInAdGroup)388   public AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) {
389     AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
390     adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_PLAYED, adIndexInAdGroup);
391     return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
392   }
393 
394   /** Returns an instance with the specified ad marked as skipped. */
395   @CheckResult
withSkippedAd(int adGroupIndex, int adIndexInAdGroup)396   public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) {
397     AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
398     adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup);
399     return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
400   }
401 
402   /** Returns an instance with the specified ad marked as having a load error. */
403   @CheckResult
withAdLoadError(int adGroupIndex, int adIndexInAdGroup)404   public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {
405     AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
406     adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup);
407     return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
408   }
409 
410   /**
411    * Returns an instance with all ads in the specified ad group skipped (except for those already
412    * marked as played or in the error state).
413    */
414   @CheckResult
withSkippedAdGroup(int adGroupIndex)415   public AdPlaybackState withSkippedAdGroup(int adGroupIndex) {
416     AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
417     adGroups[adGroupIndex] = adGroups[adGroupIndex].withAllAdsSkipped();
418     return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
419   }
420 
421   /** Returns an instance with the specified ad durations, in microseconds. */
422   @CheckResult
withAdDurationsUs(long[][] adDurationUs)423   public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) {
424     AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
425     for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) {
426       adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]);
427     }
428     return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
429   }
430 
431   /**
432    * Returns an instance with the specified ad resume position, in microseconds, relative to the
433    * start of the current ad.
434    */
435   @CheckResult
withAdResumePositionUs(long adResumePositionUs)436   public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) {
437     if (this.adResumePositionUs == adResumePositionUs) {
438       return this;
439     } else {
440       return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
441     }
442   }
443 
444   /** Returns an instance with the specified content duration, in microseconds. */
445   @CheckResult
withContentDurationUs(long contentDurationUs)446   public AdPlaybackState withContentDurationUs(long contentDurationUs) {
447     if (this.contentDurationUs == contentDurationUs) {
448       return this;
449     } else {
450       return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
451     }
452   }
453 
454   @Override
equals(@ullable Object o)455   public boolean equals(@Nullable Object o) {
456     if (this == o) {
457       return true;
458     }
459     if (o == null || getClass() != o.getClass()) {
460       return false;
461     }
462     AdPlaybackState that = (AdPlaybackState) o;
463     return adGroupCount == that.adGroupCount
464         && adResumePositionUs == that.adResumePositionUs
465         && contentDurationUs == that.contentDurationUs
466         && Arrays.equals(adGroupTimesUs, that.adGroupTimesUs)
467         && Arrays.equals(adGroups, that.adGroups);
468   }
469 
470   @Override
hashCode()471   public int hashCode() {
472     int result = adGroupCount;
473     result = 31 * result + (int) adResumePositionUs;
474     result = 31 * result + (int) contentDurationUs;
475     result = 31 * result + Arrays.hashCode(adGroupTimesUs);
476     result = 31 * result + Arrays.hashCode(adGroups);
477     return result;
478   }
479 
isPositionBeforeAdGroup( long positionUs, long periodDurationUs, int adGroupIndex)480   private boolean isPositionBeforeAdGroup(
481       long positionUs, long periodDurationUs, int adGroupIndex) {
482     if (positionUs == C.TIME_END_OF_SOURCE) {
483       // The end of the content is at (but not before) any postroll ad, and after any other ads.
484       return false;
485     }
486     long adGroupPositionUs = adGroupTimesUs[adGroupIndex];
487     if (adGroupPositionUs == C.TIME_END_OF_SOURCE) {
488       return periodDurationUs == C.TIME_UNSET || positionUs < periodDurationUs;
489     } else {
490       return positionUs < adGroupPositionUs;
491     }
492   }
493 }
494