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