Ignore live SSAI postroll placeholders in MediaPeriodQueue

This change makes sure that the `AdPlaybackState` of any period can
contain an empty postroll placeholder.

The placeholder postroll should be represented in the `MediaPeriodId`
of a content period as `nextAdGroupIndex`, but should be ignored when
building the list of `MediaPeriodInfo` in the `MediaPeriodQueue`. This
is required to allow to add an ad group to ad playback state of the
content period that is currently being played, instantly insert an ad
period into the media period queue and immediately transition playback
to the new period.

This change makes sure and tests that

- a live server side inserted postroll placeholder can be inserted to
  a `AdPlaybackState` in well-defined and tested way (helper method)
- a postroll placeholder is NOT ignored when
  `AdPlaybackState.getAdGroupIndexAfterPositionUs` is called (this
   is required when evaluating the `nextAdGroupIndex`).
- a postroll placeholder is ignored when
  `AdPlaybackState.getAdGroupIndexForPositionUs` is called (this is
  required to not attempt to play the ad and is analogous to ignore the
  post roll placeholder in a single period timeline).
- `MediaPeriod.getFollowingMediaPeriodInfo()` does not include a
  `MediaPeriodInfo` for the placeholder postroll when building the
   queue.

PiperOrigin-RevId: 515079136
This commit is contained in:
bachinger 2023-03-08 18:40:48 +00:00 committed by tonihei
parent 6514d8066d
commit 37aab2b7e9
8 changed files with 819 additions and 92 deletions

View File

@ -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.

View File

@ -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.
*
* <p>Adding such a placeholder is only required for periods of server side ad insertion live
* streams.
*
* <p>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}.
*
* <p>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.

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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<Object, Long> 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<Object, Long> 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) {

View File

@ -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<Object, Object>(((Pair<Object, Object>) 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<Object, Object>(((Pair<Object, Object>) 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<Object, Object>(((Pair<Object, Object>) 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<Object, Object>(((Pair<Object, Object>) 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<Object, Object>(((Pair<Object, Object>) 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<Object, Object>(((Pair<Object, Object>) 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<Object, Object>(
((Pair<Object, Object>) firstPeriodUid).first, "uid-4[a]"),
/* adGroupIndex= */ 0,
/* adDurationUs= */ adPeriodDurationUs,
/* contentPositionUs= */ 0,
/* isFollowedByTransitionToSameStream= */ true);
advance();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
new Pair<Object, Object>(((Pair<Object, Object>) 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<Object, Object>(
((Pair<Object, Object>) firstPeriodUid).first, "uid-5[a]"),
/* adGroupIndex= */ 0,
/* adDurationUs= */ adPeriodDurationUs,
/* contentPositionUs= */ 0,
/* isFollowedByTransitionToSameStream= */ true);
advance();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
new Pair<Object, Object>(((Pair<Object, Object>) 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<Object, Object>(((Pair<Object, Object>) 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,

View File

@ -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<PeriodData> 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;
}
}
}

View File

@ -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);