diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java new file mode 100644 index 0000000000..83b5b1bc40 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.ads; + +import android.net.Uri; +import androidx.annotation.CheckResult; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * Represents ad group times relative to the start of the media and information on the state and + * URIs of ads within each ad group. + * + * <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the + * required changes. + */ +public final class AdPlaybackState { + + /** + * 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 @NullableType Uri[] uris; + /** The state of each ad in the ad group. */ + @AdState public final int[] states; + /** The durations of each ad in the ad group, in microseconds. */ + public final long[] durationsUs; + + /** 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, @NullableType Uri[] uris, long[] durationsUs) { + Assertions.checkArgument(states.length == uris.length); + this.count = count; + this.states = states; + this.uris = uris; + this.durationsUs = durationsUs; + } + + /** + * Returns the index of the first ad in the ad group that should be played, or {@link #count} if + * no ads should be played. + */ + public int getFirstAdIndexToPlay() { + return getNextAdIndexToPlay(-1); + } + + /** + * Returns the index of the next ad in the ad group that should be played after playing {@code + * lastPlayedAdIndex}, or {@link #count} if no later ads should be played. + */ + public int getNextAdIndexToPlay(int lastPlayedAdIndex) { + int nextAdIndexToPlay = lastPlayedAdIndex + 1; + while (nextAdIndexToPlay < states.length) { + if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE + || states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) { + break; + } + nextAdIndexToPlay++; + } + return nextAdIndexToPlay; + } + + /** Returns whether the ad group has at least one ad that still needs to be played. */ + public boolean hasUnplayedAds() { + return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AdGroup adGroup = (AdGroup) o; + return count == adGroup.count + && Arrays.equals(uris, adGroup.uris) + && Arrays.equals(states, adGroup.states) + && Arrays.equals(durationsUs, adGroup.durationsUs); + } + + @Override + public int hashCode() { + int result = count; + result = 31 * result + Arrays.hashCode(uris); + result = 31 * result + Arrays.hashCode(states); + result = 31 * result + Arrays.hashCode(durationsUs); + return result; + } + + /** + * 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); + @NullableType 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); + @NullableType 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 + || states[index] == state); + long[] durationsUs = + this.durationsUs.length == states.length + ? this.durationsUs + : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length); + @NullableType + 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. One of {@link #AD_STATE_UNAVAILABLE}, {@link + * #AD_STATE_AVAILABLE}, {@link #AD_STATE_SKIPPED}, {@link #AD_STATE_PLAYED} or {@link + * #AD_STATE_ERROR}. + */ + @Documented + @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(); + + /** 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. + */ + public final long[] adGroupTimesUs; + /** 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. + * + * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value + * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. + */ + public AdPlaybackState(long... adGroupTimesUs) { + 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, AdGroup[] adGroups, long adResumePositionUs, long contentDurationUs) { + adGroupCount = adGroups.length; + this.adGroupTimesUs = adGroupTimesUs; + this.adGroups = adGroups; + this.adResumePositionUs = adResumePositionUs; + this.contentDurationUs = contentDurationUs; + } + + /** + * Returns the index of the ad group at or before {@code positionUs}, if that ad group is + * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no + * ads remaining to be played, or if there is no such ad group. + * + * @param positionUs The position at or before which to find an ad group, in microseconds, or + * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any + * unplayed postroll ad group will be returned). + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexForPositionUs(long positionUs) { + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = adGroupTimesUs.length - 1; + while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) { + index--; + } + return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET; + } + + /** + * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be + * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. + * + * @param positionUs The position after which to find an ad group, in microseconds, or {@link + * C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad group + * after the position). + * @param periodDurationUs The duration of the containing period in microseconds, or {@link + * C#TIME_UNSET} if not known. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) { + if (positionUs == C.TIME_END_OF_SOURCE + || (periodDurationUs != C.TIME_UNSET && positionUs >= periodDurationUs)) { + return C.INDEX_UNSET; + } + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = 0; + while (index < adGroupTimesUs.length + && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE + && (positionUs >= adGroupTimesUs[index] || !adGroups[index].hasUnplayedAds())) { + index++; + } + return index < adGroupTimesUs.length ? index : C.INDEX_UNSET; + } + + /** + * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}. + * The ad count must be greater than zero. + */ + @CheckResult + public AdPlaybackState withAdCount(int adGroupIndex, int adCount) { + Assertions.checkArgument(adCount > 0); + if (adGroups[adGroupIndex].count == adCount) { + return this; + } + AdGroup[] adGroups = Util.nullSafeArrayCopy(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 = Util.nullSafeArrayCopy(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 = Util.nullSafeArrayCopy(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 skipped. */ + @CheckResult + public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) { + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, 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 = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup); + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + + /** + * Returns an instance with all ads in the specified ad group skipped (except for those already + * marked as played or in the error state). + */ + @CheckResult + public AdPlaybackState withSkippedAdGroup(int adGroupIndex) { + AdGroup[] adGroups = Util.nullSafeArrayCopy(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. */ + @CheckResult + public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) { + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) { + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]); + } + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + + /** 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); + } + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AdPlaybackState that = (AdPlaybackState) o; + return adGroupCount == that.adGroupCount + && adResumePositionUs == that.adResumePositionUs + && contentDurationUs == that.contentDurationUs + && Arrays.equals(adGroupTimesUs, that.adGroupTimesUs) + && Arrays.equals(adGroups, that.adGroups); + } + + @Override + public int hashCode() { + int result = adGroupCount; + result = 31 * result + (int) adResumePositionUs; + result = 31 * result + (int) contentDurationUs; + result = 31 * result + Arrays.hashCode(adGroupTimesUs); + result = 31 * result + Arrays.hashCode(adGroups); + return result; + } + + private boolean isPositionBeforeAdGroup(long positionUs, int adGroupIndex) { + if (positionUs == C.TIME_END_OF_SOURCE) { + // The end of the content is at (but not before) any postroll ad, and after any other ads. + return false; + } + long adGroupPositionUs = adGroupTimesUs[adGroupIndex]; + if (adGroupPositionUs == C.TIME_END_OF_SOURCE) { + return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs; + } else { + return positionUs < adGroupPositionUs; + } + } +} |