diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java index 209b1de832..479c3ba506 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -820,6 +820,17 @@ public abstract class Timeline implements Bundleable { : AD_STATE_UNAVAILABLE; } + /** + * Returns whether the ad group at the given ad group index is a live postroll placeholder. + * + * @param adGroupIndex The ad group index. + * @return True if the ad group at the given index is a live postroll placeholder. + */ + public boolean isLivePostrollPlaceholder(int adGroupIndex) { + return adGroupIndex == getAdGroupCount() - 1 + && adPlaybackState.isLivePostrollPlaceholder(adGroupIndex); + } + /** * Returns the position offset in the first unplayed ad at which to begin playback, in * microseconds. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 6054def4ff..61e4162322 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -173,6 +173,10 @@ public final class AdPlaybackState implements Bundleable { return false; } + private boolean isLivePostrollPlaceholder() { + return isServerSideInserted && timeUs == C.TIME_END_OF_SOURCE && count == C.LENGTH_UNSET; + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { @@ -630,6 +634,7 @@ public final class AdPlaybackState implements Bundleable { // 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 = adGroupCount - 1; + index -= isLivePostrollPlaceholder(index) ? 1 : 0; while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) { index--; } @@ -977,6 +982,49 @@ public final class AdPlaybackState implements Bundleable { adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); } + /** + * Appends a live postroll placeholder ad group to the ad playback state. + * + *

Adding such a placeholder is only required for periods of server side ad insertion live + * streams. + * + *

When building the media period queue, it sets {@link MediaPeriodId#nextAdGroupIndex} of a + * content period to the index of the placeholder. However, the placeholder will not produce a + * period in the media period queue. This only happens when an actual ad group is inserted at the + * given {@code nextAdGroupIndex}. In this case the newly inserted ad group will be used to insert + * an ad period into the media period queue following the content period with the given {@link + * MediaPeriodId#nextAdGroupIndex}. + * + *

See {@link #endsWithLivePostrollPlaceHolder()} also. + * + * @return The new ad playback state instance ending with a live postroll placeholder. + */ + public AdPlaybackState withLivePostrollPlaceholderAppended() { + return withNewAdGroup(adGroupCount, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE) + .withIsServerSideInserted(adGroupCount, true); + } + + /** + * Returns whether the last ad group is a live postroll placeholder as inserted by {@link + * #withLivePostrollPlaceholderAppended()}. + * + * @return Whether the ad playback state ends with a live postroll placeholder. + */ + public boolean endsWithLivePostrollPlaceHolder() { + int adGroupIndex = adGroupCount - 1; + return adGroupIndex >= 0 && isLivePostrollPlaceholder(adGroupIndex); + } + + /** + * Whether the {@link AdGroup} at the given ad group index is a live postroll placeholder. + * + * @param adGroupIndex The ad group index. + * @return True if the ad group at the given index is a live postroll placeholder, false if not. + */ + public boolean isLivePostrollPlaceholder(int adGroupIndex) { + return adGroupIndex == adGroupCount - 1 && getAdGroup(adGroupIndex).isLivePostrollPlaceholder(); + } + /** * Returns a copy of the ad playback state with the given ads ID. * @@ -1089,15 +1137,21 @@ public final class AdPlaybackState implements Bundleable { private boolean isPositionBeforeAdGroup( long positionUs, long periodDurationUs, 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. + // The end of the content is at (but not before) any postroll ad, and after any other ad. return false; } - long adGroupPositionUs = getAdGroup(adGroupIndex).timeUs; + AdGroup adGroup = getAdGroup(adGroupIndex); + long adGroupPositionUs = adGroup.timeUs; if (adGroupPositionUs == C.TIME_END_OF_SOURCE) { - return periodDurationUs == C.TIME_UNSET || positionUs < periodDurationUs; - } else { - return positionUs < adGroupPositionUs; + // Handling postroll: The requested position is considered before a postroll when a) + // the period duration is unknown (last period in a live stream), or when b) the postroll is a + // placeholder in a period of a multi-period live window, or when c) the position actually is + // before the given period duration. + return periodDurationUs == C.TIME_UNSET + || (adGroup.isServerSideInserted && adGroup.count == C.LENGTH_UNSET) + || positionUs < periodDurationUs; } + return positionUs < adGroupPositionUs; } // Bundleable implementation. diff --git a/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java index 4d8e305f2c..e3594deded 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem.LiveConfiguration; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.testutil.FakeMultiPeriodLiveTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; @@ -432,6 +433,30 @@ public class TimelineTest { /* expectedPeriod= */ period, /* actualPeriod= */ restoredPeriod); } + @Test + public void periodIsLivePostrollPlaceholder_recognizesLivePostrollPlaceholder() { + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 60_000_000, + /* nowUs= */ 60_000_000, + /* adSequencePattern= */ new boolean[] {false, true, true}, + /* isContentTimeline= */ false, + /* populateAds= */ true); + + assertThat(timeline.getPeriodCount()).isEqualTo(4); + assertThat( + timeline + .getPeriod(/* periodIndex= */ 1, new Timeline.Period()) + .isLivePostrollPlaceholder(/* adGroupIndex= */ 0)) + .isFalse(); + assertThat( + timeline + .getPeriod(/* periodIndex= */ 1, new Timeline.Period()) + .isLivePostrollPlaceholder(/* adGroupIndex= */ 1)) + .isTrue(); + } + @SuppressWarnings("deprecation") // Populates the deprecated window.tag property. private static Timeline.Window populateWindow( @Nullable MediaItem mediaItem, @Nullable Object tag) { diff --git a/library/common/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java b/library/common/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java index 63a41f83a3..3d75b27d7a 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java @@ -488,6 +488,65 @@ public class AdPlaybackStateTest { assertThat(AdPlaybackState.AdGroup.CREATOR.fromBundle(adGroup.toBundle())).isEqualTo(adGroup); } + @Test + public void withLivePostrollPlaceholderAppended_emptyAdPlaybackState_insertsPlaceholder() { + AdPlaybackState adPlaybackState = + new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + + assertThat(adPlaybackState.adGroupCount).isEqualTo(1); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs) + .isEqualTo(C.TIME_END_OF_SOURCE); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).count).isEqualTo(C.LENGTH_UNSET); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).isServerSideInserted).isTrue(); + } + + @Test + public void withLivePostrollPlaceholderAppended_withExistingAdGroups_appendsPlaceholder() { + AdPlaybackState adPlaybackState = + new AdPlaybackState("state", /* adGroupTimesUs...= */ 0L, 10_000_000L) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true) + .withIsServerSideInserted(/* adGroupIndex= */ 1, true) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ 10_000_000L) + .withAdDurationsUs(/* adGroupIndex= */ 1, /* adDurationsUs...= */ 5_000_000L); + + adPlaybackState = adPlaybackState.withLivePostrollPlaceholderAppended(); + + assertThat(adPlaybackState.adGroupCount).isEqualTo(3); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 2).timeUs) + .isEqualTo(C.TIME_END_OF_SOURCE); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 2).count).isEqualTo(C.LENGTH_UNSET); + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 2).isServerSideInserted).isTrue(); + } + + @Test + public void endsWithLivePostrollPlaceHolder_withExistingAdGroups_postrollDetected() { + AdPlaybackState adPlaybackState = + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 10_000_000L) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true) + .withIsServerSideInserted(/* adGroupIndex= */ 1, true) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ 10_000_000L) + .withAdDurationsUs(/* adGroupIndex= */ 1, /* adDurationsUs...= */ 5_000_000L); + + boolean endsWithLivePostrollPlaceHolder = adPlaybackState.endsWithLivePostrollPlaceHolder(); + + assertThat(endsWithLivePostrollPlaceHolder).isFalse(); + + adPlaybackState = adPlaybackState.withLivePostrollPlaceholderAppended(); + endsWithLivePostrollPlaceHolder = adPlaybackState.endsWithLivePostrollPlaceHolder(); + + assertThat(endsWithLivePostrollPlaceHolder).isTrue(); + } + + @Test + public void endsWithLivePostrollPlaceHolder_emptyAdPlaybackState_postrollNotDetected() { + assertThat(AdPlaybackState.NONE.endsWithLivePostrollPlaceHolder()).isFalse(); + assertThat(new AdPlaybackState("adsId").endsWithLivePostrollPlaceHolder()).isFalse(); + } + @Test public void getAdGroupIndexAfterPositionUs_withClientSideInsertedAds_returnsNextAdGroupWithUnplayedAds() { @@ -635,4 +694,103 @@ public class AdPlaybackStateTest { /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 5000)) .isEqualTo(C.INDEX_UNSET); } + + @Test + public void + getAdGroupIndexAfterPositionUs_withServerSidePostrollPlaceholderForLive_placeholderAsNextAdGroupIndex() { + AdPlaybackState state = + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 2000) + .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withLivePostrollPlaceholderAppended(); + + assertThat( + state.getAdGroupIndexAfterPositionUs( + /* positionUs= */ 1999, /* periodDurationUs= */ 5000)) + .isEqualTo(0); + assertThat( + state.getAdGroupIndexAfterPositionUs( + /* positionUs= */ 2000, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(1); + assertThat( + state.getAdGroupIndexAfterPositionUs( + /* positionUs= */ 2000, /* periodDurationUs= */ 5000)) + .isEqualTo(1); + assertThat( + state.getAdGroupIndexAfterPositionUs( + /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + state.getAdGroupIndexAfterPositionUs( + /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 5000)) + .isEqualTo(C.INDEX_UNSET); + } + + @Test + public void + getAdGroupIndexForPositionUs_withServerSidePostrollPlaceholderForLive_ignoresPlaceholder() { + AdPlaybackState state = + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 5_000_000L, C.TIME_END_OF_SOURCE) + .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true) + .withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true) + .withIsServerSideInserted(/* adGroupIndex= */ 2, /* isServerSideInserted= */ true) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 4_999_999L, /* periodDurationUs= */ 10_000_000L)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 4_999_999L, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 5_000_000L, /* periodDurationUs= */ 10_000_000L)) + .isEqualTo(1); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 5_000_000L, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(1); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 10_000_000L)) + .isEqualTo(1); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(1); + } + + @Test + public void + getAdGroupIndexForPositionUs_withOnlyServerSidePostrollPlaceholderForLive_ignoresPlaceholder() { + AdPlaybackState state = + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE) + .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true); + + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 5_000_000L, /* periodDurationUs= */ 10_000_000L)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 5_000_000L, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 10_000_001L, /* periodDurationUs= */ 10_000_000L)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 10_000_000L)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(C.INDEX_UNSET); + } } 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 0651dd8bf8..a02ba0afbc 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 @@ -661,7 +661,7 @@ import com.google.common.collect.ImmutableList; playbackInfo.timeline, playbackInfo.periodId, playbackInfo.requestedContentPositionUs, - playbackInfo.positionUs); + /* startPositionUs= */ playbackInfo.positionUs); } /** @@ -687,69 +687,100 @@ import com.google.common.collect.ImmutableList; // the start position for transitions to new windows. long bufferedDurationUs = mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs; - if (mediaPeriodInfo.isLastInTimelinePeriod) { - int currentPeriodIndex = timeline.getIndexOfPeriod(mediaPeriodInfo.id.periodUid); - int nextPeriodIndex = - timeline.getNextPeriodIndex( - currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled); - if (nextPeriodIndex == C.INDEX_UNSET) { - // We can't create a next period yet. + return mediaPeriodInfo.isLastInTimelinePeriod + ? getFirstMediaPeriodInfoOfNextPeriod(timeline, mediaPeriodHolder, bufferedDurationUs) + : getFollowingMediaPeriodInfoOfCurrentPeriod( + timeline, mediaPeriodHolder, bufferedDurationUs); + } + + /** + * Returns the first {@link MediaPeriodInfo} that follows the given {@linkplain MediaPeriodHolder + * media period holder}, or null if there is no following info. This can be the first info of the + * next period in the current (multi-period) window, or the first info in the next window in the + * timeline. + * + * @param timeline The timeline with period and window information + * @param mediaPeriodHolder The media period holder for which to get the following info. + * @param bufferedDurationUs The buffered duration, in microseconds. + * @return The first media period info of the next period in the timeline, or null. + */ + @Nullable + private MediaPeriodInfo getFirstMediaPeriodInfoOfNextPeriod( + Timeline timeline, MediaPeriodHolder mediaPeriodHolder, long bufferedDurationUs) { + MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info; + int currentPeriodIndex = timeline.getIndexOfPeriod(mediaPeriodInfo.id.periodUid); + int nextPeriodIndex = + timeline.getNextPeriodIndex( + currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled); + if (nextPeriodIndex == C.INDEX_UNSET) { + // We can't create a next period yet. + return null; + } + long startPositionUs = 0; + long contentPositionUs = 0; + int nextWindowIndex = + timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; + Object nextPeriodUid = checkNotNull(period.uid); + long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber; + if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) { + // We're starting to buffer a new window. When playback transitions to this window we'll + // want it to be from its default start position, so project the default start position + // forward by the duration of the buffer, and start buffering from this point. + contentPositionUs = C.TIME_UNSET; + @Nullable + Pair defaultPositionUs = + timeline.getPeriodPositionUs( + window, + period, + nextWindowIndex, + /* windowPositionUs= */ C.TIME_UNSET, + /* defaultPositionProjectionUs= */ max(0, bufferedDurationUs)); + if (defaultPositionUs == null) { return null; } - // We either start a new period in the same window or the first period in the next window. - long startPositionUs = 0; - long contentPositionUs = 0; - int nextWindowIndex = - timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; - Object nextPeriodUid = checkNotNull(period.uid); - long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber; - if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) { - // We're starting to buffer a new window. When playback transitions to this window we'll - // want it to be from its default start position, so project the default start position - // forward by the duration of the buffer, and start buffering from this point. - contentPositionUs = C.TIME_UNSET; - @Nullable - Pair defaultPositionUs = - timeline.getPeriodPositionUs( - window, - period, - nextWindowIndex, - /* windowPositionUs= */ C.TIME_UNSET, - /* defaultPositionProjectionUs= */ max(0, bufferedDurationUs)); - if (defaultPositionUs == null) { - return null; - } - nextPeriodUid = defaultPositionUs.first; - startPositionUs = defaultPositionUs.second; - @Nullable MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext(); - if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) { - windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber; - } else { - windowSequenceNumber = nextWindowSequenceNumber++; - } + nextPeriodUid = defaultPositionUs.first; + startPositionUs = defaultPositionUs.second; + @Nullable MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext(); + if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) { + windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber; + } else { + windowSequenceNumber = nextWindowSequenceNumber++; } - - @Nullable - MediaPeriodId periodId = - resolveMediaPeriodIdForAds( - timeline, nextPeriodUid, startPositionUs, windowSequenceNumber, window, period); - if (contentPositionUs != C.TIME_UNSET - && mediaPeriodInfo.requestedContentPositionUs != C.TIME_UNSET) { - boolean isPrecedingPeriodAnAd = - timeline.getPeriodByUid(mediaPeriodInfo.id.periodUid, period).getAdGroupCount() > 0 - && period.isServerSideInsertedAdGroup(period.getRemovedAdGroupCount()); - // Handle the requested content position for period transitions within the same window. - if (periodId.isAd() && isPrecedingPeriodAnAd) { - // Propagate the requested position to the following ad period in the same window. - contentPositionUs = mediaPeriodInfo.requestedContentPositionUs; - } else if (isPrecedingPeriodAnAd) { - // Use the requested content position of the preceding ad period as the start position. - startPositionUs = mediaPeriodInfo.requestedContentPositionUs; - } - } - return getMediaPeriodInfo(timeline, periodId, contentPositionUs, startPositionUs); } + @Nullable + MediaPeriodId periodId = + resolveMediaPeriodIdForAds( + timeline, nextPeriodUid, startPositionUs, windowSequenceNumber, window, period); + if (contentPositionUs != C.TIME_UNSET + && mediaPeriodInfo.requestedContentPositionUs != C.TIME_UNSET) { + boolean precedingPeriodHasServerSideInsertedAds = + hasServerSideInsertedAds(mediaPeriodInfo.id.periodUid, timeline); + // Handle the requested content position for period transitions within the same window. + if (periodId.isAd() && precedingPeriodHasServerSideInsertedAds) { + // Propagate the requested position to the following ad period in the same window. + contentPositionUs = mediaPeriodInfo.requestedContentPositionUs; + } else if (precedingPeriodHasServerSideInsertedAds) { + // Use the requested content position of the preceding ad period as the start position. + startPositionUs = mediaPeriodInfo.requestedContentPositionUs; + } + } + return getMediaPeriodInfo(timeline, periodId, contentPositionUs, startPositionUs); + } + + /** + * Gets the {@link MediaPeriodInfo} that follows {@code mediaPeriodHolder} within the current + * period. + * + * @param timeline The timeline with period and window information + * @param mediaPeriodHolder The media period holder for which to get the following info. + * @param bufferedDurationUs The buffered duration, in microseconds. + * @return The following {@link MediaPeriodInfo} in the current period. + */ + @Nullable + private MediaPeriodInfo getFollowingMediaPeriodInfoOfCurrentPeriod( + Timeline timeline, MediaPeriodHolder mediaPeriodHolder, long bufferedDurationUs) { + MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info; MediaPeriodId currentPeriodId = mediaPeriodInfo.id; timeline.getPeriodByUid(currentPeriodId.periodUid, period); if (currentPeriodId.isAd()) { @@ -798,6 +829,10 @@ import com.google.common.collect.ImmutableList; mediaPeriodInfo.requestedContentPositionUs, currentPeriodId.windowSequenceNumber); } + } else if (currentPeriodId.nextAdGroupIndex != C.INDEX_UNSET + && period.isLivePostrollPlaceholder(currentPeriodId.nextAdGroupIndex)) { + // The next ad group is the postroll placeholder. Ignore and try the next timeline period. + return getFirstMediaPeriodInfoOfNextPeriod(timeline, mediaPeriodHolder, bufferedDurationUs); } else { // Play the next ad group if it's still available. int adIndexInAdGroup = period.getFirstAdIndexToPlay(currentPeriodId.nextAdGroupIndex); @@ -822,13 +857,21 @@ import com.google.common.collect.ImmutableList; return getMediaPeriodInfoForAd( timeline, currentPeriodId.periodUid, - currentPeriodId.nextAdGroupIndex, + /* adGroupIndex= */ currentPeriodId.nextAdGroupIndex, adIndexInAdGroup, /* contentPositionUs= */ mediaPeriodInfo.durationUs, currentPeriodId.windowSequenceNumber); } } + private boolean hasServerSideInsertedAds(Object periodUid, Timeline timeline) { + int adGroupCount = timeline.getPeriodByUid(periodUid, period).getAdGroupCount(); + int firstAdGroupIndex = period.getRemovedAdGroupCount(); + return adGroupCount > 0 + && period.isServerSideInsertedAdGroup(firstAdGroupIndex) + && (adGroupCount > 1 || period.getAdGroupTimeUs(firstAdGroupIndex) != C.TIME_END_OF_SOURCE); + } + @Nullable private MediaPeriodInfo getMediaPeriodInfo( Timeline timeline, MediaPeriodId id, long requestedContentPositionUs, long startPositionUs) { @@ -877,7 +920,7 @@ import com.google.common.collect.ImmutableList; return new MediaPeriodInfo( id, startPositionUs, - contentPositionUs, + /* requestedContentPositionUs= */ contentPositionUs, /* endPositionUs= */ C.TIME_UNSET, durationUs, isFollowedByTransitionToSameStream, @@ -894,6 +937,8 @@ import com.google.common.collect.ImmutableList; long windowSequenceNumber) { timeline.getPeriodByUid(periodUid, period); int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); + boolean isNextAdGroupPostrollPlaceholder = + nextAdGroupIndex != C.INDEX_UNSET && period.isLivePostrollPlaceholder(nextAdGroupIndex); boolean clipPeriodAtContentDuration = false; if (nextAdGroupIndex == C.INDEX_UNSET) { // Clip SSAI streams when at the end of the period. @@ -901,21 +946,23 @@ import com.google.common.collect.ImmutableList; period.getAdGroupCount() > 0 && period.isServerSideInsertedAdGroup(period.getRemovedAdGroupCount()); } else if (period.isServerSideInsertedAdGroup(nextAdGroupIndex) - && period.getAdGroupTimeUs(nextAdGroupIndex) == period.durationUs) { - if (period.hasPlayedAdGroup(nextAdGroupIndex)) { - // Clip period before played SSAI post-rolls. - nextAdGroupIndex = C.INDEX_UNSET; - clipPeriodAtContentDuration = true; - } + && period.getAdGroupTimeUs(nextAdGroupIndex) == period.durationUs + && period.hasPlayedAdGroup(nextAdGroupIndex)) { + // Clip period before played SSAI post-rolls. + nextAdGroupIndex = C.INDEX_UNSET; + clipPeriodAtContentDuration = true; } + MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex); boolean isLastInPeriod = isLastInPeriod(id); boolean isLastInWindow = isLastInWindow(timeline, id); boolean isLastInTimeline = isLastInTimeline(timeline, id, isLastInPeriod); boolean isFollowedByTransitionToSameStream = - nextAdGroupIndex != C.INDEX_UNSET && period.isServerSideInsertedAdGroup(nextAdGroupIndex); - long endPositionUs = nextAdGroupIndex != C.INDEX_UNSET + && period.isServerSideInsertedAdGroup(nextAdGroupIndex) + && !isNextAdGroupPostrollPlaceholder; + long endPositionUs = + nextAdGroupIndex != C.INDEX_UNSET && !isNextAdGroupPostrollPlaceholder ? period.getAdGroupTimeUs(nextAdGroupIndex) : clipPeriodAtContentDuration ? period.durationUs : C.TIME_UNSET; long durationUs = @@ -936,7 +983,7 @@ import com.google.common.collect.ImmutableList; isFollowedByTransitionToSameStream, isLastInPeriod, isLastInWindow, - isLastInTimeline); + /* isFinal= */ isLastInTimeline); } private boolean isLastInPeriod(MediaPeriodId id) { 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 index 6e5a4bed85..10cbf1044a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -27,6 +27,7 @@ import static org.robolectric.Shadows.shadowOf; import android.net.Uri; import android.os.Looper; import android.util.Pair; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.analytics.AnalyticsCollector; @@ -39,6 +40,7 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionMediaSource; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeMultiPeriodLiveTimeline; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; @@ -117,6 +119,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ true, + /* isFinal= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @@ -125,6 +128,7 @@ public final class MediaPeriodQueueTest { setupAdTimeline(/* adGroupTimesUs...= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 0, AD_DURATION_US, /* contentPositionUs= */ C.TIME_UNSET, @@ -139,6 +143,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ true, + /* isFinal= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @@ -154,15 +159,18 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 0); advance(); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 0, /* adDurationUs= */ C.TIME_UNSET, /* contentPositionUs= */ FIRST_AD_START_TIME_US, /* isFollowedByTransitionToSameStream= */ false); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 0, AD_DURATION_US, /* contentPositionUs= */ FIRST_AD_START_TIME_US, @@ -177,10 +185,12 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 1); advance(); setAdGroupLoaded(/* adGroupIndex= */ 1); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 1, AD_DURATION_US, /* contentPositionUs= */ SECOND_AD_START_TIME_US, @@ -195,6 +205,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ true, + /* isFinal= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @@ -210,10 +221,12 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 0); advance(); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 0, AD_DURATION_US, /* contentPositionUs= */ FIRST_AD_START_TIME_US, @@ -228,10 +241,12 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 1); advance(); setAdGroupLoaded(/* adGroupIndex= */ 1); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 1, AD_DURATION_US, /* contentPositionUs= */ CONTENT_DURATION_US, @@ -246,6 +261,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ true, + /* isFinal= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @@ -267,6 +283,7 @@ public final class MediaPeriodQueueTest { setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 0, AD_DURATION_US, /* contentPositionUs= */ C.TIME_UNSET, @@ -281,10 +298,12 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 1); advance(); setAdGroupLoaded(/* adGroupIndex= */ 1); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 1, AD_DURATION_US, /* contentPositionUs= */ FIRST_AD_START_TIME_US, @@ -299,10 +318,12 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 2); advance(); setAdGroupLoaded(/* adGroupIndex= */ 2); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 2, AD_DURATION_US, /* contentPositionUs= */ CONTENT_DURATION_US, @@ -317,6 +338,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ true, + /* isFinal= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @@ -338,6 +360,7 @@ public final class MediaPeriodQueueTest { setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 0, AD_DURATION_US, /* contentPositionUs= */ C.TIME_UNSET, @@ -352,10 +375,12 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ true, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 1); advance(); setAdGroupLoaded(/* adGroupIndex= */ 1); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 1, AD_DURATION_US, /* contentPositionUs= */ FIRST_AD_START_TIME_US, @@ -370,10 +395,12 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ true, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 2); advance(); setAdGroupLoaded(/* adGroupIndex= */ 2); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 2, AD_DURATION_US, /* contentPositionUs= */ SECOND_AD_START_TIME_US, @@ -388,9 +415,223 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ true, + /* isFinal= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } + @Test + @SuppressWarnings("unchecked") + public void getNextMediaPeriodInfo_multiPeriodTimelineWithNoAdsAndNoPostrollPlaceholder() { + long contentPeriodDurationUs = FakeMultiPeriodLiveTimeline.PERIOD_DURATION_US; + long adPeriodDurationUs = FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_US; + // Multi period timeline without ad playback state. + FakeMultiPeriodLiveTimeline multiPeriodLiveTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 60_000_000, + /* nowUs= */ 110_000_000, + new boolean[] {false, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false); + setupTimeline(multiPeriodLiveTimeline); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ contentPeriodDurationUs, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ C.INDEX_UNSET); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-4[a]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ adPeriodDurationUs, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ C.INDEX_UNSET); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-5[a]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ adPeriodDurationUs, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ C.INDEX_UNSET); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-6[c]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ C.TIME_UNSET, // last period in live timeline + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ true, + /* isFinal= */ false, // a dynamic window never has a final period + /* nextAdGroupIndex= */ C.INDEX_UNSET); + advance(); + assertThat(getNextMediaPeriodInfo()).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + public void getNextMediaPeriodInfo_multiPeriodTimelineWithPostrollPlaceHolder() { + long contentPeriodDurationUs = FakeMultiPeriodLiveTimeline.PERIOD_DURATION_US; + long adPeriodDurationUs = FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_US; + // Multi period timeline without ad playback state. + FakeMultiPeriodLiveTimeline multiPeriodLiveTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 60_000_000, + /* nowUs= */ 110_000_000, + new boolean[] {false, true, true}, + /* isContentTimeline= */ false, + /* populateAds= */ false); + setupTimeline(multiPeriodLiveTimeline); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ contentPeriodDurationUs, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ 0); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-4[a]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ adPeriodDurationUs, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ 0); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-5[a]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ adPeriodDurationUs, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ 0); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-6[c]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ C.TIME_UNSET, // last period in live timeline + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ 0); + advance(); + assertThat(getNextMediaPeriodInfo()).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + public void getNextMediaPeriodInfo_multiPeriodTimelineWithAdsAndWithPostRollPlaceHolder() { + long contentPeriodDurationUs = FakeMultiPeriodLiveTimeline.PERIOD_DURATION_US; + long adPeriodDurationUs = FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_US; + FakeMultiPeriodLiveTimeline multiPeriodLiveTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 60_000_000, + /* nowUs= */ 110_000_000, + new boolean[] {false, true, true}, + /* isContentTimeline= */ false, + /* populateAds= */ true); + setupTimeline(multiPeriodLiveTimeline); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ contentPeriodDurationUs, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ 0); + advance(); + assertNextMediaPeriodInfoIsAd( + /* periodUid= */ new Pair( + ((Pair) firstPeriodUid).first, "uid-4[a]"), + /* adGroupIndex= */ 0, + /* adDurationUs= */ adPeriodDurationUs, + /* contentPositionUs= */ 0, + /* isFollowedByTransitionToSameStream= */ true); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-4[a]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ 0, + /* durationUs= */ 0, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ C.INDEX_UNSET); + advance(); + assertNextMediaPeriodInfoIsAd( + /* periodUid= */ new Pair( + ((Pair) firstPeriodUid).first, "uid-5[a]"), + /* adGroupIndex= */ 0, + /* adDurationUs= */ adPeriodDurationUs, + /* contentPositionUs= */ 0, + /* isFollowedByTransitionToSameStream= */ true); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-5[a]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ 0, + /* durationUs= */ 0, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ C.INDEX_UNSET); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-6[c]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ C.TIME_UNSET, // Last period in stream. + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ 0); + advance(); + assertThat(getNextMediaPeriodInfo()).isNull(); + } + @Test public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { setupAdTimeline(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE); @@ -403,6 +644,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 0); advance(); setAdGroupFailedToLoad(/* adGroupIndex= */ 0); @@ -415,6 +657,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ true, + /* isFinal= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @@ -425,6 +668,7 @@ public final class MediaPeriodQueueTest { setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 2); assertNextMediaPeriodInfoIsAd( + firstPeriodUid, /* adGroupIndex= */ 0, AD_DURATION_US, /* contentPositionUs= */ C.TIME_UNSET, @@ -440,6 +684,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 1); setAdGroupPlayed(/* adGroupIndex= */ 1); clear(); @@ -452,6 +697,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ false, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ 2); setAdGroupPlayed(/* adGroupIndex= */ 2); clear(); @@ -464,6 +710,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ true, + /* isFinal= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @@ -487,6 +734,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ false, + /* isFinal= */ false, /* nextAdGroupIndex= */ C.INDEX_UNSET); advance(); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( @@ -498,6 +746,7 @@ public final class MediaPeriodQueueTest { /* isFollowedByTransitionToSameStream= */ false, /* isLastInPeriod= */ true, /* isLastInWindow= */ true, + /* isFinal= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @@ -1154,6 +1403,7 @@ public final class MediaPeriodQueueTest { /* staticMetadata= */ ImmutableList.of()); } + @Nullable private MediaPeriodInfo getNextMediaPeriodInfo() { return mediaPeriodQueue.getNextMediaPeriodInfo(/* rendererPositionUs= */ 0, playbackInfo); } @@ -1214,6 +1464,7 @@ public final class MediaPeriodQueueTest { boolean isFollowedByTransitionToSameStream, boolean isLastInPeriod, boolean isLastInWindow, + boolean isFinal, int nextAdGroupIndex) { assertThat(getNextMediaPeriodInfo()) .isEqualTo( @@ -1226,10 +1477,11 @@ public final class MediaPeriodQueueTest { isFollowedByTransitionToSameStream, isLastInPeriod, isLastInWindow, - /* isFinal= */ isLastInWindow)); + isFinal)); } private void assertNextMediaPeriodInfoIsAd( + Object periodUid, int adGroupIndex, long adDurationUs, long contentPositionUs, @@ -1238,12 +1490,12 @@ public final class MediaPeriodQueueTest { .isEqualTo( new MediaPeriodInfo( new MediaPeriodId( - firstPeriodUid, + periodUid, adGroupIndex, /* adIndexInAdGroup= */ 0, /* windowSequenceNumber= */ 0), /* startPositionUs= */ 0, - contentPositionUs, + /* requestedContentPositionUs= */ contentPositionUs, /* endPositionUs= */ C.TIME_UNSET, adDurationUs, isFollowedByTransitionToSameStream, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimeline.java index 89632eca11..1afdfbc52c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimeline.java @@ -22,6 +22,7 @@ import static com.google.android.exoplayer2.util.Util.usToMs; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.common.collect.ImmutableList; import java.util.Arrays; @@ -52,6 +53,8 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { private final MediaItem mediaItem; private final long availabilityStartTimeUs; private final long liveWindowDurationUs; + private final boolean isContentTimeline; + private final boolean populateAds; private long nowUs; private ImmutableList periods; @@ -64,19 +67,34 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { * @param nowUs The current time that determines the end of the live window. * @param adSequencePattern The repeating pattern of periods starting at {@code * availabilityStartTimeUs}. True is an ad period, and false a content period. + * @param isContentTimeline Whether the timeline is a content timeline without {@link + * AdPlaybackState}s. + * @param populateAds Whether to populate ads like after the ad event has been received. This + * parameter is ignored if the timeline is a content timeline. */ public FakeMultiPeriodLiveTimeline( long availabilityStartTimeUs, long liveWindowDurationUs, long nowUs, - boolean[] adSequencePattern) { + boolean[] adSequencePattern, + boolean isContentTimeline, + boolean populateAds) { checkArgument(nowUs - liveWindowDurationUs >= availabilityStartTimeUs); this.availabilityStartTimeUs = availabilityStartTimeUs; this.liveWindowDurationUs = liveWindowDurationUs; this.nowUs = nowUs; this.adSequencePattern = Arrays.copyOf(adSequencePattern, adSequencePattern.length); + this.isContentTimeline = isContentTimeline; + this.populateAds = populateAds; mediaItem = new MediaItem.Builder().build(); - periods = invalidate(availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern); + periods = + invalidate( + availabilityStartTimeUs, + liveWindowDurationUs, + nowUs, + adSequencePattern, + isContentTimeline, + populateAds); } /** Calculates the total duration of the given ad period sequence. */ @@ -91,7 +109,14 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { /** Advances the live window by the given duration, in microseconds. */ public void advanceNowUs(long durationUs) { nowUs += durationUs; - periods = invalidate(availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern); + periods = + invalidate( + availabilityStartTimeUs, + liveWindowDurationUs, + nowUs, + adSequencePattern, + isContentTimeline, + populateAds); } @Override @@ -134,7 +159,9 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { periodData.uid, /* windowIndex= */ 0, /* durationUs= */ periodIndex < getPeriodCount() - 1 ? periodData.durationUs : C.TIME_UNSET, - periodData.positionInWindowUs); + periodData.positionInWindowUs, + periodData.adPlaybackState, + /* isPlaceholder= */ false); return period; } @@ -157,7 +184,9 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { long availabilityStartTimeUs, long liveWindowDurationUs, long now, - boolean[] adSequencePattern) { + boolean[] adSequencePattern, + boolean isContentTimeline, + boolean populateAds) { long windowStartTimeUs = now - liveWindowDurationUs; int sequencePeriodCount = adSequencePattern.length; long sequenceDurationUs = calculateAdSequencePatternDurationUs(adSequencePattern); @@ -182,12 +211,28 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { while (lastPeriodStartTimeUs < now) { isAd = adSequencePattern[lastPeriodIndex % sequencePeriodCount]; long periodDurationUs = isAd ? AD_PERIOD_DURATION_US : PERIOD_DURATION_US; + long adPeriodDurationUs = periodDurationUs; + AdPlaybackState adPlaybackState = AdPlaybackState.NONE; + if (!isContentTimeline) { + adPlaybackState = new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + if (isAd && populateAds) { + adPlaybackState = + adPlaybackState + .withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ 0) + .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdDurationsUs( + /* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs); + adPeriodDurationUs = 0; + } + } liveWindow.add( new PeriodData( /* id= */ lastPeriodIndex++, + adPeriodDurationUs, + /* positionInWindowUs= */ lastPeriodStartTimeUs - windowStartTimeUs, isAd, - periodDurationUs, - /* positionInWindowUs= */ lastPeriodStartTimeUs - windowStartTimeUs)); + adPlaybackState)); lastPeriodStartTimeUs += periodDurationUs; } return liveWindow.build(); @@ -199,13 +244,20 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { private final Object uid; private final long durationUs; private final long positionInWindowUs; + private final AdPlaybackState adPlaybackState; /** Creates an instance. */ - public PeriodData(int id, boolean isAd, long durationUs, long positionInWindowUs) { + public PeriodData( + int id, + long durationUs, + long positionInWindowUs, + boolean isAd, + AdPlaybackState adPlaybackState) { this.id = id; this.uid = "uid-" + id + "[" + (isAd ? "a" : "c") + "]"; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; + this.adPlaybackState = adPlaybackState; } } } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimelineTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimelineTest.java index 6b59af57bf..d43778dae7 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimelineTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMultiPeriodLiveTimelineTest.java @@ -41,7 +41,9 @@ public class FakeMultiPeriodLiveTimelineTest { /* availabilityStartTimeUs= */ 0L, /* liveWindowDurationUs= */ 60_000_000L, /* nowUs= */ 60_000_000L, - adSequencePattern); + adSequencePattern, + /* isContentTimeline= */ true, + /* populateAds= */ false); Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); @@ -67,6 +69,116 @@ public class FakeMultiPeriodLiveTimelineTest { adSequencePattern); } + @Test + public void newInstance_timelineWithAdsPopulated_correctPlaybackStates() { + boolean[] adSequencePattern = {false, true, true}; + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 50_000_000L, + /* nowUs= */ 100_000_000L, + adSequencePattern, + /* isContentTimeline= */ false, + /* populateAds= */ true); + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + + assertThat(timeline.getPeriodCount()).isEqualTo(3); + assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(50_000L); + assertThat(timeline.getPeriod(0, period).uid).isEqualTo("uid-3[c]"); + assertThat(timeline.getPeriod(1, period).uid).isEqualTo("uid-4[a]"); + assertThat(timeline.getPeriod(1, period).getAdGroupCount()).isEqualTo(2); + assertThat(timeline.getPeriod(1, period).getAdGroupTimeUs(/* adGroupIndex= */ 0)).isEqualTo(0L); + assertThat(timeline.getPeriod(1, period).getAdCountInAdGroup(/* adGroupIndex= */ 0)) + .isEqualTo(1); + assertThat( + timeline + .getPeriod(1, period) + .getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)) + .isEqualTo(10_000_000L); + assertThat(timeline.getPeriod(1, period).getAdGroupTimeUs(/* adGroupIndex= */ 1)) + .isEqualTo(C.TIME_END_OF_SOURCE); + assertThat(timeline.getPeriod(1, period).getAdCountInAdGroup(/* adGroupIndex= */ 1)) + .isEqualTo(C.LENGTH_UNSET); + assertThat(timeline.getPeriod(1, period).isServerSideInsertedAdGroup(/* adGroupIndex= */ 1)) + .isTrue(); + assertThat(timeline.getPeriod(2, period).uid).isEqualTo("uid-5[a]"); + assertThat(timeline.getPeriod(2, period).getAdGroupCount()).isEqualTo(2); + assertThat(timeline.getPeriod(2, period).getAdGroupTimeUs(/* adGroupIndex= */ 0)).isEqualTo(0L); + assertThat(timeline.getPeriod(2, period).getAdCountInAdGroup(/* adGroupIndex= */ 0)) + .isEqualTo(1); + assertThat(timeline.getPeriod(2, period).isServerSideInsertedAdGroup(/* adGroupIndex= */ 1)) + .isTrue(); + assertThat( + timeline + .getPeriod(2, period) + .getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)) + .isEqualTo(10_000_000L); + assertThat(timeline.getPeriod(2, period).getAdGroupTimeUs(/* adGroupIndex= */ 1)) + .isEqualTo(C.TIME_END_OF_SOURCE); + assertThat(timeline.getPeriod(2, period).getAdCountInAdGroup(/* adGroupIndex= */ 1)) + .isEqualTo(C.LENGTH_UNSET); + assertExpectedWindow( + timeline, + calculateExpectedWindow( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 50_000_000L, + /* nowUs= */ 100_000_000L, + adSequencePattern), + adSequencePattern); + } + + @Test + public void newInstance_timelineWithAdsNotPopulated_correctPlaybackStates() { + boolean[] adSequencePattern = {false, true, true}; + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 50_000_000L, + /* nowUs= */ 100_000_000L, + adSequencePattern, + /* isContentTimeline= */ false, + /* populateAds= */ false); + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + + // Assert that each period has no ads but a fake postroll ad group at the end. + assertThat(timeline.getPeriodCount()).isEqualTo(3); + assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(50_000L); + assertThat(timeline.getPeriod(0, period).uid).isEqualTo("uid-3[c]"); + assertThat(timeline.getPeriod(0, period).getAdGroupCount()).isEqualTo(1); + assertThat(timeline.getPeriod(0, period).getAdGroupTimeUs(/* adGroupIndex= */ 0)) + .isEqualTo(C.TIME_END_OF_SOURCE); + assertThat(timeline.getPeriod(0, period).getAdCountInAdGroup(/* adGroupIndex= */ 0)) + .isEqualTo(C.LENGTH_UNSET); + assertThat(timeline.getPeriod(0, period).isServerSideInsertedAdGroup(/* adGroupIndex= */ 0)) + .isTrue(); + assertThat(timeline.getPeriod(1, period).uid).isEqualTo("uid-4[a]"); + assertThat(timeline.getPeriod(1, period).getAdGroupCount()).isEqualTo(1); + assertThat(timeline.getPeriod(1, period).getAdGroupTimeUs(/* adGroupIndex= */ 0)) + .isEqualTo(C.TIME_END_OF_SOURCE); + assertThat(timeline.getPeriod(1, period).getAdCountInAdGroup(/* adGroupIndex= */ 0)) + .isEqualTo(C.LENGTH_UNSET); + assertThat(timeline.getPeriod(1, period).isServerSideInsertedAdGroup(/* adGroupIndex= */ 0)) + .isTrue(); + assertThat(timeline.getPeriod(2, period).uid).isEqualTo("uid-5[a]"); + assertThat(timeline.getPeriod(2, period).getAdGroupCount()).isEqualTo(1); + assertThat(timeline.getPeriod(2, period).getAdGroupTimeUs(/* adGroupIndex= */ 0)) + .isEqualTo(C.TIME_END_OF_SOURCE); + assertThat(timeline.getPeriod(2, period).getAdCountInAdGroup(/* adGroupIndex= */ 0)) + .isEqualTo(C.LENGTH_UNSET); + assertThat(timeline.getPeriod(2, period).isServerSideInsertedAdGroup(/* adGroupIndex= */ 0)) + .isTrue(); + assertExpectedWindow( + timeline, + calculateExpectedWindow( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 50_000_000L, + /* nowUs= */ 100_000_000L, + adSequencePattern), + adSequencePattern); + } + @Test public void advanceTimeUs_availabilitySinceStartOfUnixEpoch_correctPeriodsInLiveWindow() { boolean[] adSequencePattern = {false, true, true}; @@ -75,7 +187,9 @@ public class FakeMultiPeriodLiveTimelineTest { /* availabilityStartTimeUs= */ 0L, /* liveWindowDurationUs= */ 60_000_000L, /* nowUs= */ 60_000_123L, - adSequencePattern); + adSequencePattern, + /* isContentTimeline= */ true, + /* populateAds= */ false); Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); @@ -154,7 +268,12 @@ public class FakeMultiPeriodLiveTimelineTest { FakeMultiPeriodLiveTimeline timeline = new FakeMultiPeriodLiveTimeline( - availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern); + availabilityStartTimeUs, + liveWindowDurationUs, + nowUs, + adSequencePattern, + /* isContentTimeline= */ true, + /* populateAds= */ false); assertThat(timeline.getWindow(0, new Timeline.Window()).windowStartTimeMs) .isEqualTo(Util.usToMs(nowUs - liveWindowDurationUs)); @@ -209,7 +328,12 @@ public class FakeMultiPeriodLiveTimelineTest { FakeMultiPeriodLiveTimeline timeline = new FakeMultiPeriodLiveTimeline( - availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern); + availabilityStartTimeUs, + liveWindowDurationUs, + nowUs, + adSequencePattern, + /* isContentTimeline= */ true, + /* populateAds= */ false); assertThat(timeline.getWindow(0, new Timeline.Window()).windowStartTimeMs) .isEqualTo(Util.usToMs(nowUs - liveWindowDurationUs)); @@ -227,7 +351,9 @@ public class FakeMultiPeriodLiveTimelineTest { /* availabilityStartTimeUs= */ 0L, /* liveWindowDurationUs= */ 120_000_000L, /* nowUs= */ 120_000_000L, - new boolean[] {false, true, true, true}); + new boolean[] {false, true, true, true}, + /* isContentTimeline= */ true, + /* populateAds= */ false); Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); @@ -262,7 +388,9 @@ public class FakeMultiPeriodLiveTimelineTest { /* availabilityStartTimeUs= */ 0L, /* liveWindowDurationUs= */ 220_000_000L, /* nowUs= */ 250_000_000L, - new boolean[] {false, true, false, true, false}); + new boolean[] {false, true, false, true, false}, + /* isContentTimeline= */ true, + /* populateAds= */ false); assertThat(timeline.getPeriodCount()).isEqualTo(10); assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(30_000L);