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 cc621c6218..71bc048902 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 @@ -666,7 +666,8 @@ public final class ImaAdsLoader // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered // just after an ad group isn't incorrectly attributed to the next ad group. int nextAdGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs)); + adPlaybackState.getAdGroupIndexAfterPositionUs( + C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) { long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]); if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index 4fc21475d9..7becac7b55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -88,16 +89,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; this.info = info; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator); - if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) { - mediaPeriod = - new ClippingMediaPeriod( - mediaPeriod, - /* enableInitialDiscontinuity= */ true, - /* startUs= */ 0, - info.id.endPositionUs); - } - this.mediaPeriod = mediaPeriod; + mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator); } /** @@ -302,16 +294,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void release() { disableTrackSelectionsInResult(); trackSelectorResult = null; - try { - if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) { - mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); - } else { - mediaSource.releasePeriod(mediaPeriod); - } - } catch (RuntimeException e) { - // There's nothing we can do. - Log.e(TAG, "Period release failed.", e); - } + releaseMediaPeriod(info.id, mediaSource, mediaPeriod); } /** @@ -413,4 +396,34 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private boolean isLoadingMediaPeriod() { return next == null; } + + /** Returns a media period corresponding to the given {@code id}. */ + private static MediaPeriod createMediaPeriod( + MediaPeriodId id, MediaSource mediaSource, Allocator allocator) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator); + if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) { + mediaPeriod = + new ClippingMediaPeriod( + mediaPeriod, + /* enableInitialDiscontinuity= */ true, + /* startUs= */ 0, + id.endPositionUs); + } + return mediaPeriod; + } + + /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ + private static void releaseMediaPeriod( + MediaPeriodId id, MediaSource mediaSource, MediaPeriod mediaPeriod) { + try { + if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) { + mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + } else { + mediaSource.releasePeriod(mediaPeriod); + } + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Period release failed.", e); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java index ba19b54c3f..e57100931e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.util.Util; /** Stores the information required to load and play a {@link MediaPeriod}. */ /* package */ final class MediaPeriodInfo { @@ -32,8 +34,8 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; public final long contentPositionUs; /** * The duration of the media period, like {@link MediaPeriodId#endPositionUs} but with {@link - * C#TIME_END_OF_SOURCE} resolved to the timeline period duration. May be {@link C#TIME_UNSET} if - * the end position is not known. + * C#TIME_END_OF_SOURCE} and {@link C#TIME_UNSET} resolved to the timeline period duration if + * known. */ public final long durationUs; /** @@ -72,4 +74,33 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; isLastInTimelinePeriod, isFinal); } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MediaPeriodInfo that = (MediaPeriodInfo) o; + return startPositionUs == that.startPositionUs + && contentPositionUs == that.contentPositionUs + && durationUs == that.durationUs + && isLastInTimelinePeriod == that.isLastInTimelinePeriod + && isFinal == that.isFinal + && Util.areEqual(id, that.id); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + id.hashCode(); + result = 31 * result + (int) startPositionUs; + result = 31 * result + (int) contentPositionUs; + result = 31 * result + (int) durationUs; + result = 31 * result + (isLastInTimelinePeriod ? 1 : 0); + result = 31 * result + (isFinal ? 1 : 0); + return result; + } } 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 40bc658953..de09fc13a1 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 @@ -351,17 +351,18 @@ import com.google.android.exoplayer2.util.Assertions; * @return The updated media period info for the current timeline. */ public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info) { - boolean isLastInPeriod = isLastInPeriod(info.id); - boolean isLastInTimeline = isLastInTimeline(info.id, isLastInPeriod); + MediaPeriodId id = info.id; + boolean isLastInPeriod = isLastInPeriod(id); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); timeline.getPeriodByUid(info.id.periodUid, period); long durationUs = - info.id.isAd() - ? period.getAdDurationUs(info.id.adGroupIndex, info.id.adIndexInAdGroup) - : (info.id.endPositionUs == C.TIME_END_OF_SOURCE + id.isAd() + ? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup) + : (id.endPositionUs == C.TIME_UNSET || id.endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() - : info.id.endPositionUs); + : id.endPositionUs); return new MediaPeriodInfo( - info.id, + id, info.startPositionUs, info.contentPositionUs, durationUs, @@ -404,7 +405,7 @@ import com.google.android.exoplayer2.util.Assertions; int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs); long endPositionUs = nextAdGroupIndex == C.INDEX_UNSET - ? C.TIME_END_OF_SOURCE + ? C.TIME_UNSET : period.getAdGroupTimeUs(nextAdGroupIndex); return new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs); } else { @@ -580,7 +581,8 @@ import com.google.android.exoplayer2.util.Assertions; } MediaPeriodId periodId = resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); - return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs); + return getMediaPeriodInfo( + periodId, /* contentPositionUs= */ startPositionUs, startPositionUs); } MediaPeriodId currentPeriodId = mediaPeriodInfo.id; @@ -626,14 +628,14 @@ import com.google.android.exoplayer2.util.Assertions; return getMediaPeriodInfoForContent( currentPeriodId.periodUid, startPositionUs, currentPeriodId.windowSequenceNumber); } - } else if (mediaPeriodInfo.id.endPositionUs != C.TIME_END_OF_SOURCE) { + } else { // Play the next ad group if it's available. int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.id.endPositionUs); if (nextAdGroupIndex == C.INDEX_UNSET) { - // The next ad group can't be played. Play content from the ad group position instead. + // The next ad group can't be played. Play content from the previous end position instead. return getMediaPeriodInfoForContent( currentPeriodId.periodUid, - mediaPeriodInfo.id.endPositionUs, + /* startPositionUs= */ mediaPeriodInfo.durationUs, currentPeriodId.windowSequenceNumber); } int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex); @@ -643,30 +645,8 @@ import com.google.android.exoplayer2.util.Assertions; currentPeriodId.periodUid, nextAdGroupIndex, adIndexInAdGroup, - mediaPeriodInfo.id.endPositionUs, + /* contentPositionUs= */ mediaPeriodInfo.durationUs, currentPeriodId.windowSequenceNumber); - } else { - // Check if the postroll ad should be played. - int adGroupCount = period.getAdGroupCount(); - if (adGroupCount == 0) { - return null; - } - int adGroupIndex = adGroupCount - 1; - if (period.getAdGroupTimeUs(adGroupIndex) != C.TIME_END_OF_SOURCE - || period.hasPlayedAdGroup(adGroupIndex)) { - return null; - } - int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); - if (!period.isAdAvailable(adGroupIndex, adIndexInAdGroup)) { - return null; - } - long contentDurationUs = period.getDurationUs(); - return getMediaPeriodInfoForAd( - currentPeriodId.periodUid, - adGroupIndex, - adIndexInAdGroup, - contentDurationUs, - currentPeriodId.windowSequenceNumber); } } @@ -696,8 +676,6 @@ import com.google.android.exoplayer2.util.Assertions; long windowSequenceNumber) { MediaPeriodId id = new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); - boolean isLastInPeriod = isLastInPeriod(id); - boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); long durationUs = timeline .getPeriodByUid(id.periodUid, period) @@ -711,49 +689,35 @@ import com.google.android.exoplayer2.util.Assertions; startPositionUs, contentPositionUs, durationUs, - isLastInPeriod, - isLastInTimeline); + /* isLastInTimelinePeriod= */ false, + /* isFinal= */ false); } private MediaPeriodInfo getMediaPeriodInfoForContent( Object periodUid, long startPositionUs, long windowSequenceNumber) { int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); long endPositionUs = - nextAdGroupIndex == C.INDEX_UNSET - ? C.TIME_END_OF_SOURCE - : period.getAdGroupTimeUs(nextAdGroupIndex); + nextAdGroupIndex != C.INDEX_UNSET + ? period.getAdGroupTimeUs(nextAdGroupIndex) + : C.TIME_UNSET; MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs); - timeline.getPeriodByUid(id.periodUid, period); boolean isLastInPeriod = isLastInPeriod(id); boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); long durationUs = - endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs; + endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE + ? period.durationUs + : endPositionUs; return new MediaPeriodInfo( - id, startPositionUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline); + id, + startPositionUs, + /* contentPositionUs= */ C.TIME_UNSET, + durationUs, + isLastInPeriod, + isLastInTimeline); } private boolean isLastInPeriod(MediaPeriodId id) { - int adGroupCount = timeline.getPeriodByUid(id.periodUid, period).getAdGroupCount(); - if (adGroupCount == 0) { - return true; - } - - int lastAdGroupIndex = adGroupCount - 1; - boolean isAd = id.isAd(); - if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) { - // There's no postroll ad. - return !isAd && id.endPositionUs == C.TIME_END_OF_SOURCE; - } - - int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex); - if (postrollAdCount == C.LENGTH_UNSET) { - // We won't know if this is the last ad until we know how many postroll ads there are. - return false; - } - - boolean isLastAd = - isAd && id.adGroupIndex == lastAdGroupIndex && id.adIndexInAdGroup == postrollAdCount - 1; - return isLastAd || (!isAd && period.getFirstAdIndexToPlay(lastAdGroupIndex) == postrollAdCount); + return !id.isAd() && id.endPositionUs == C.TIME_UNSET; } 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 1639920aaa..61a31f7db5 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 @@ -441,7 +441,7 @@ public abstract class Timeline { * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexAfterPositionUs(long positionUs) { - return adPlaybackState.getAdGroupIndexAfterPositionUs(positionUs); + return adPlaybackState.getAdGroupIndexAfterPositionUs(positionUs, durationUs); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 2a94e884a6..d8335131f9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -89,11 +89,10 @@ public interface MediaSource { public final long windowSequenceNumber; /** - * The end position of the media to play within the media period, in microseconds, or {@link - * C#TIME_END_OF_SOURCE} if the end position is the end of the media period. - * - *
Note that this only applies if the media period is for content (i.e., not for an ad) and
- * is clipped to the position of the next ad group.
+ * The end position to which the media period's content is clipped in order to play a following
+ * ad group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if
+ * this media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll
+ * ad follows at the end of this content media period.
*/
public final long endPositionUs;
@@ -115,7 +114,7 @@ public interface MediaSource {
* windows this media period is part of.
*/
public MediaPeriodId(Object periodUid, long windowSequenceNumber) {
- this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, C.TIME_END_OF_SOURCE);
+ this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, C.TIME_UNSET);
}
/**
@@ -143,7 +142,7 @@ public interface MediaSource {
*/
public MediaPeriodId(
Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {
- this(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, C.TIME_END_OF_SOURCE);
+ this(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, C.TIME_UNSET);
}
private MediaPeriodId(
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 41adb78906..fc1355df6a 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
@@ -310,7 +310,9 @@ public final class AdPlaybackState {
* 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.
+ * @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) {
@@ -327,10 +329,18 @@ public final class AdPlaybackState {
* 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.
+ * @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) {
+ 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;
@@ -457,6 +467,10 @@ public final class AdPlaybackState {
}
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;
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java
new file mode 100644
index 0000000000..e8f43e3fe6
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.source.SinglePeriodTimeline;
+import com.google.android.exoplayer2.source.ads.AdPlaybackState;
+import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.upstream.Allocator;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Unit tests for {@link MediaPeriodQueue}. */
+@RunWith(RobolectricTestRunner.class)
+public final class MediaPeriodQueueTest {
+
+ private static final long CONTENT_DURATION_US = 30 * C.MICROS_PER_SECOND;
+ private static final long FIRST_AD_START_TIME_US = 10 * C.MICROS_PER_SECOND;
+ private static final long SECOND_AD_START_TIME_US = 20 * C.MICROS_PER_SECOND;
+
+ private static final Timeline CONTENT_TIMELINE =
+ new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false);
+ private static final Uri AD_URI = Uri.EMPTY;
+
+ private MediaPeriodQueue mediaPeriodQueue;
+ private AdPlaybackState adPlaybackState;
+ private Timeline timeline;
+ private Object periodUid;
+
+ private PlaybackInfo playbackInfo;
+ private RendererCapabilities[] rendererCapabilities;
+ private TrackSelector trackSelector;
+ private Allocator allocator;
+ private MediaSource mediaSource;
+
+ @Before
+ public void setUp() {
+ mediaPeriodQueue = new MediaPeriodQueue();
+ mediaSource = mock(MediaSource.class);
+ rendererCapabilities = new RendererCapabilities[0];
+ trackSelector = mock(TrackSelector.class);
+ allocator = mock(Allocator.class);
+ }
+
+ @Test
+ public void testGetNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() {
+ setupInitialTimeline(/* initialPositionUs= */ 0);
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ 0,
+ /* endPositionUs= */ C.TIME_UNSET,
+ /* durationUs= */ CONTENT_DURATION_US,
+ /* isLast= */ true);
+ }
+
+ @Test
+ public void testGetNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() {
+ setupInitialTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0);
+ setAdGroupLoaded(/* adGroupIndex= */ 0);
+ assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0);
+ advance();
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ 0,
+ /* endPositionUs= */ C.TIME_UNSET,
+ /* durationUs= */ CONTENT_DURATION_US,
+ /* isLast= */ true);
+ }
+
+ @Test
+ public void testGetNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() {
+ setupInitialTimeline(
+ /* initialPositionUs= */ 0,
+ /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
+ SECOND_AD_START_TIME_US);
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ 0,
+ /* endPositionUs= */ FIRST_AD_START_TIME_US,
+ /* durationUs= */ FIRST_AD_START_TIME_US,
+ /* isLast= */ false);
+ // The next media period info should be null as we haven't loaded the ad yet.
+ advance();
+ assertNull(getNextMediaPeriodInfo());
+ setAdGroupLoaded(/* adGroupIndex= */ 0);
+ assertNextMediaPeriodInfoIsAd(
+ /* adGroupIndex= */ 0, /* contentPositionUs= */ FIRST_AD_START_TIME_US);
+ advance();
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ FIRST_AD_START_TIME_US,
+ /* endPositionUs= */ SECOND_AD_START_TIME_US,
+ /* durationUs= */ SECOND_AD_START_TIME_US,
+ /* isLast= */ false);
+ advance();
+ setAdGroupLoaded(/* adGroupIndex= */ 1);
+ assertNextMediaPeriodInfoIsAd(
+ /* adGroupIndex= */ 1, /* contentPositionUs= */ SECOND_AD_START_TIME_US);
+ advance();
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ SECOND_AD_START_TIME_US,
+ /* endPositionUs= */ C.TIME_UNSET,
+ /* durationUs= */ CONTENT_DURATION_US,
+ /* isLast= */ true);
+ }
+
+ @Test
+ public void testGetNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() {
+ setupInitialTimeline(
+ /* initialPositionUs= */ 0,
+ /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
+ C.TIME_END_OF_SOURCE);
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ 0,
+ /* endPositionUs= */ FIRST_AD_START_TIME_US,
+ /* durationUs= */ FIRST_AD_START_TIME_US,
+ /* isLast= */ false);
+ advance();
+ setAdGroupLoaded(/* adGroupIndex= */ 0);
+ assertNextMediaPeriodInfoIsAd(
+ /* adGroupIndex= */ 0, /* contentPositionUs= */ FIRST_AD_START_TIME_US);
+ advance();
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ FIRST_AD_START_TIME_US,
+ /* endPositionUs= */ C.TIME_END_OF_SOURCE,
+ /* durationUs= */ CONTENT_DURATION_US,
+ /* isLast= */ false);
+ advance();
+ setAdGroupLoaded(/* adGroupIndex= */ 1);
+ assertNextMediaPeriodInfoIsAd(
+ /* adGroupIndex= */ 1, /* contentPositionUs= */ CONTENT_DURATION_US);
+ advance();
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ CONTENT_DURATION_US,
+ /* endPositionUs= */ C.TIME_UNSET,
+ /* durationUs= */ CONTENT_DURATION_US,
+ /* isLast= */ true);
+ }
+
+ @Test
+ public void testGetNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() {
+ setupInitialTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE);
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ 0,
+ /* endPositionUs= */ C.TIME_END_OF_SOURCE,
+ /* durationUs= */ CONTENT_DURATION_US,
+ /* isLast= */ false);
+ advance();
+ setAdGroupFailedToLoad(/* adGroupIndex= */ 0);
+ assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ /* startPositionUs= */ CONTENT_DURATION_US,
+ /* endPositionUs= */ C.TIME_UNSET,
+ /* durationUs= */ CONTENT_DURATION_US,
+ /* isLast= */ true);
+ }
+
+ private void setupInitialTimeline(long initialPositionUs, long... adGroupTimesUs) {
+ adPlaybackState =
+ new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US);
+ timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
+ periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
+ mediaPeriodQueue.setTimeline(timeline);
+ playbackInfo =
+ new PlaybackInfo(
+ timeline,
+ /* manifest= */ null,
+ mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs),
+ /* startPositionUs= */ 0,
+ /* contentPositionUs= */ 0,
+ Player.STATE_READY,
+ /* isLoading= */ false,
+ /* trackGroups= */ null,
+ /* trackSelectorResult= */ null,
+ /* loadingMediaPeriodId= */ null,
+ /* bufferedPositionUs= */ 0,
+ /* totalBufferedDurationUs= */ 0,
+ /* positionUs= */ 0);
+ }
+
+ private void advance() {
+ mediaPeriodQueue.enqueueNextMediaPeriod(
+ rendererCapabilities, trackSelector, allocator, mediaSource, getNextMediaPeriodInfo());
+ mediaPeriodQueue.advancePlayingPeriod();
+ }
+
+ private MediaPeriodInfo getNextMediaPeriodInfo() {
+ return mediaPeriodQueue.getNextMediaPeriodInfo(/* rendererPositionUs= */ 0, playbackInfo);
+ }
+
+ private void setAdGroupLoaded(int adGroupIndex) {
+ adPlaybackState =
+ adPlaybackState
+ .withAdCount(adGroupIndex, /* adCount= */ 1)
+ .withAdUri(adGroupIndex, /* adIndexInAdGroup= */ 0, AD_URI);
+ updateTimeline();
+ }
+
+ private void setAdGroupFailedToLoad(int adGroupIndex) {
+ adPlaybackState =
+ adPlaybackState
+ .withAdCount(adGroupIndex, /* adCount= */ 1)
+ .withAdLoadError(adGroupIndex, /* adIndexInAdGroup= */ 0);
+ updateTimeline();
+ }
+
+ private void updateTimeline() {
+ timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
+ mediaPeriodQueue.setTimeline(timeline);
+ }
+
+ private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
+ long startPositionUs, long endPositionUs, long durationUs, boolean isLast) {
+ assertThat(getNextMediaPeriodInfo())
+ .isEqualTo(
+ new MediaPeriodInfo(
+ new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, endPositionUs),
+ startPositionUs,
+ /* contentPositionUs= */ C.TIME_UNSET,
+ durationUs,
+ /* isLastInTimelinePeriod= */ isLast,
+ /* isFinal= */ isLast));
+ }
+
+ private void assertNextMediaPeriodInfoIsAd(int adGroupIndex, long contentPositionUs) {
+ assertThat(getNextMediaPeriodInfo())
+ .isEqualTo(
+ new MediaPeriodInfo(
+ new MediaPeriodId(
+ periodUid,
+ adGroupIndex,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0),
+ /* startPositionUs= */ 0,
+ contentPositionUs,
+ /* durationUs= */ C.TIME_UNSET,
+ /* isLastInTimelinePeriod= */ false,
+ /* isFinal= */ false));
+ }
+}
diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml
index 5b24387028..729d3b9609 100644
--- a/library/ui/src/main/res/values-af/strings.xml
+++ b/library/ui/src/main/res/values-af/strings.xml
@@ -12,7 +12,7 @@