Update player logic to handle server-side inserted ads.

There are two main changes that need to be made:
 1. Whenever we determine the next ad to play, we need to select a
    server-side inserted ad even if it has been played already (because
    it's part of the stream).
 2. When the Timeline is updated in the player, we need to avoid changes
    that would unnecessarily reset the renderers. Whenever a Timeline
    change replaces content with a server-side inserted ad at the same
    position we can just keep the existing MediaPeriod and also if the
    duration of the current MediaPeriod is reduced but it is followed by
    a MediaPeriod in the same SSAI stream, we can don't need to reset
    the renderers as we keep playing the same stream.

PiperOrigin-RevId: 373745031
This commit is contained in:
tonihei 2021-05-14 09:45:44 +01:00 committed by Oliver Woodman
parent bd4ba4c583
commit 795210d7bc
7 changed files with 365 additions and 12 deletions

View File

@ -738,10 +738,12 @@ public abstract class Timeline implements Bundleable {
}
/**
* Returns whether the ad group at index {@code adGroupIndex} has been played.
* Returns whether all ads in the ad group at index {@code adGroupIndex} have been played,
* skipped or failed.
*
* @param adGroupIndex The ad group index.
* @return Whether the ad group at index {@code adGroupIndex} has been played.
* @return Whether all ads in the ad group at index {@code adGroupIndex} have been played,
* skipped or failed.
*/
public boolean hasPlayedAdGroup(int adGroupIndex) {
return !adPlaybackState.adGroups[adGroupIndex].hasUnplayedAds();

View File

@ -108,7 +108,8 @@ public final class AdPlaybackState implements Bundleable {
public int getNextAdIndexToPlay(int lastPlayedAdIndex) {
int nextAdIndexToPlay = lastPlayedAdIndex + 1;
while (nextAdIndexToPlay < states.length) {
if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE
if (isServerSideInserted
|| states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE
|| states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) {
break;
}
@ -117,11 +118,26 @@ public final class AdPlaybackState implements Bundleable {
return nextAdIndexToPlay;
}
/** Returns whether the ad group has at least one ad that still needs to be played. */
public boolean hasUnplayedAds() {
/** Returns whether the ad group has at least one ad that should be played. */
public boolean shouldPlayAdGroup() {
return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count;
}
/**
* Returns whether the ad group has at least one ad that is neither played, skipped, nor failed.
*/
public boolean hasUnplayedAds() {
if (count == C.LENGTH_UNSET) {
return true;
}
for (int i = 0; i < count; i++) {
if (states[i] == AD_STATE_UNAVAILABLE || states[i] == AD_STATE_AVAILABLE) {
return true;
}
}
return false;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
@ -473,7 +489,7 @@ public final class AdPlaybackState implements Bundleable {
int index = 0;
while (index < adGroupTimesUs.length
&& ((adGroupTimesUs[index] != C.TIME_END_OF_SOURCE && adGroupTimesUs[index] <= positionUs)
|| !adGroups[index].hasUnplayedAds())) {
|| !adGroups[index].shouldPlayAdGroup())) {
index++;
}
return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
@ -501,7 +517,7 @@ public final class AdPlaybackState implements Bundleable {
* @return The updated ad playback state.
*/
@CheckResult
public AdPlaybackState withAdGroupTimesUs(long[] adGroupTimesUs) {
public AdPlaybackState withAdGroupTimesUs(long... adGroupTimesUs) {
AdGroup[] adGroups =
adGroupTimesUs.length < adGroupCount
? Util.nullSafeArrayCopy(this.adGroups, adGroupTimesUs.length)

View File

@ -161,6 +161,35 @@ public class AdPlaybackStateTest {
assertThat(state.adGroups[0].getNextAdIndexToPlay(0)).isEqualTo(2);
}
@Test
public void getFirstAdIndexToPlay_withPlayedServerSideInsertedAds_returnsFirstIndex() {
state = state.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(0);
}
@Test
public void getNextAdIndexToPlay_withPlayedServerSideInsertedAds_returnsNextIndex() {
state = state.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2);
assertThat(state.adGroups[0].getNextAdIndexToPlay(/* lastPlayedAdIndex= */ 0)).isEqualTo(1);
assertThat(state.adGroups[0].getNextAdIndexToPlay(/* lastPlayedAdIndex= */ 1)).isEqualTo(2);
}
@Test
public void setAdStateTwiceThrows() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
@ -226,4 +255,152 @@ public class AdPlaybackStateTest {
assertThat(AdPlaybackState.AdGroup.CREATOR.fromBundle(adGroup.toBundle())).isEqualTo(adGroup);
}
@Test
public void
getAdGroupIndexAfterPositionUs_withClientSideInsertedAds_returnsNextAdGroupWithUnplayedAds() {
AdPlaybackState state =
new AdPlaybackState(
/* adsId= */ new Object(),
/* adGroupTimesUs...= */ 0,
1000,
2000,
3000,
4000,
C.TIME_END_OF_SOURCE)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 3, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 4, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 5, /* adCount= */ 1)
.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 3, /* adIndexInAdGroup= */ 0);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 0, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(/* positionUs= */ 0, /* periodDurationUs= */ 5000))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1999, /* periodDurationUs= */ 5000))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(4);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ 5000))
.isEqualTo(4);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 3999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(4);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 3999, /* periodDurationUs= */ 5000))
.isEqualTo(4);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 4000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 4000, /* periodDurationUs= */ 5000))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 4999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 4999, /* periodDurationUs= */ 5000))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 5000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 5000, /* periodDurationUs= */ 5000))
.isEqualTo(C.INDEX_UNSET);
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 getAdGroupIndexAfterPositionUs_withServerSideInsertedAds_returnsNextAdGroup() {
AdPlaybackState state =
new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 0, 1000, 2000)
.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)
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 1)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 0, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(/* positionUs= */ 0, /* periodDurationUs= */ 5000))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 999, /* periodDurationUs= */ 5000))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1000, /* periodDurationUs= */ 5000))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1999, /* periodDurationUs= */ 5000))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ 5000))
.isEqualTo(C.INDEX_UNSET);
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);
}
}

View File

@ -2601,12 +2601,25 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and
// the only change is that MediaPeriodId.nextAdGroupIndex increased. This postpones a potential
// discontinuity until we reach the former next ad group position.
boolean sameOldAndNewPeriodUid = oldPeriodId.periodUid.equals(newPeriodUid);
boolean onlyNextAdGroupIndexIncreased =
oldPeriodId.periodUid.equals(newPeriodUid)
sameOldAndNewPeriodUid
&& !oldPeriodId.isAd()
&& !periodIdWithAds.isAd()
&& earliestCuePointIsUnchangedOrLater;
MediaPeriodId newPeriodId = onlyNextAdGroupIndexIncreased ? oldPeriodId : periodIdWithAds;
// Drop update if the change is from/to server-side inserted ads at the same content position to
// avoid any unintentional renderer reset.
timeline.getPeriodByUid(newPeriodUid, period);
boolean isInStreamAdChange =
sameOldAndNewPeriodUid
&& !isUsingPlaceholderPeriod
&& oldContentPositionUs == newContentPositionUs
&& ((periodIdWithAds.isAd()
&& period.isServerSideInsertedAdGroup(periodIdWithAds.adGroupIndex))
|| (oldPeriodId.isAd()
&& period.isServerSideInsertedAdGroup(oldPeriodId.adGroupIndex)));
MediaPeriodId newPeriodId =
onlyNextAdGroupIndexIncreased || isInStreamAdChange ? oldPeriodId : periodIdWithAds;
long periodPositionUs = contentPositionForAdResolutionUs;
if (newPeriodId.isAd()) {

View File

@ -358,6 +358,7 @@ import com.google.common.collect.ImmutableList;
: periodHolder.toRendererTime(newPeriodInfo.durationUs);
boolean isReadingAndReadBeyondNewDuration =
periodHolder == reading
&& !isUsingSameStreamForNextMediaPeriod(timeline, periodHolder.info.id)
&& (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE
|| maxRendererReadPositionUs >= newDurationInRendererTime);
boolean readingPeriodRemoved = removeAfter(periodHolder);
@ -858,4 +859,20 @@ import com.google.common.collect.ImmutableList;
}
return startPositionUs + period.getContentResumeOffsetUs(adGroupIndex);
}
private boolean isUsingSameStreamForNextMediaPeriod(
Timeline timeline, MediaPeriodId mediaPeriodId) {
// Server-side inserted ads or content after them will use the same underlying stream.
if (mediaPeriodId.isAd()) {
return timeline
.getPeriodByUid(mediaPeriodId.periodUid, period)
.isServerSideInsertedAdGroup(mediaPeriodId.adGroupIndex);
} else if (mediaPeriodId.nextAdGroupIndex == C.INDEX_UNSET) {
return false;
} else {
return timeline
.getPeriodByUid(mediaPeriodId.periodUid, period)
.isServerSideInsertedAdGroup(mediaPeriodId.nextAdGroupIndex);
}
}
}

View File

@ -39,6 +39,7 @@ import static com.google.android.exoplayer2.Player.COMMAND_SET_SPEED_AND_PITCH;
import static com.google.android.exoplayer2.Player.COMMAND_SET_VIDEO_SURFACE;
import static com.google.android.exoplayer2.Player.COMMAND_SET_VOLUME;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilPosition;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfWindow;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
@ -120,6 +121,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.FakeTrackSelection;
import com.google.android.exoplayer2.testutil.FakeTrackSelector;
import com.google.android.exoplayer2.testutil.FakeVideoRenderer;
import com.google.android.exoplayer2.testutil.NoUidTimeline;
import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@ -10446,6 +10448,73 @@ public final class ExoPlayerTest {
player.release();
}
@Test
public void newServerSideInsertedAdAtPlaybackPosition_keepsRenderersEnabled() throws Exception {
// Injecting renderer to count number of renderer resets.
AtomicReference<FakeVideoRenderer> videoRenderer = new AtomicReference<>();
SimpleExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> {
videoRenderer.set(new FakeVideoRenderer(handler, videoListener));
return new Renderer[] {videoRenderer.get()};
})
.build();
// Live stream timeline with unassigned next ad group.
AdPlaybackState initialAdPlaybackState =
new AdPlaybackState(
/* adsId= */ new Object(), /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdDurationsUs(new long[][] {new long[] {10 * C.MICROS_PER_SECOND}});
// Updated timeline with ad group at 18 seconds.
long firstSampleTimeUs = TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
Timeline initialTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ C.TIME_UNSET,
initialAdPlaybackState));
AdPlaybackState updatedAdPlaybackState =
initialAdPlaybackState.withAdGroupTimesUs(
/* adGroupTimesUs...= */ firstSampleTimeUs + 18 * C.MICROS_PER_SECOND);
Timeline updatedTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ C.TIME_UNSET,
updatedAdPlaybackState));
// Add samples to allow player to load and start playing (but no EOS as this is a live stream).
FakeMediaSource mediaSource =
new FakeMediaSource(
initialTimeline,
DrmSessionManager.DRM_UNSUPPORTED,
(format, mediaPeriodId) ->
ImmutableList.of(
oneByteSample(firstSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME),
oneByteSample(firstSampleTimeUs + 40 * C.MICROS_PER_SECOND)),
ExoPlayerTestRunner.VIDEO_FORMAT);
// Set updated ad group once we reach 20 seconds, and then continue playing until 40 seconds.
player
.createMessage((message, payload) -> mediaSource.setNewSourceInfo(updatedTimeline))
.setPosition(20_000)
.send();
player.setMediaSource(mediaSource);
player.prepare();
playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 40_000);
player.release();
// Assert that the renderer hasn't been reset despite the inserted ad group.
assertThat(videoRenderer.get().positionResetCount).isEqualTo(1);
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {

View File

@ -369,9 +369,9 @@ public final class MediaPeriodQueueTest {
updateQueuedPeriods_withDurationChangeInPlayingContent_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
enqueueNext(); // Content before first ad.
enqueueNext(); // First ad.
enqueueNext(); // Content between ads.
enqueueNext(); // Content before ad.
enqueueNext(); // Ad.
enqueueNext(); // Content after ad.
// Change position of first ad (= change duration of playing content before first ad).
updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000);
@ -389,6 +389,65 @@ public final class MediaPeriodQueueTest {
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
}
@Test
public void
updateQueuedPeriods_withDurationChangeInPlayingContentAfterReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
enqueueNext(); // Content before ad.
enqueueNext(); // Ad.
enqueueNext(); // Content after ad.
// Change position of first ad (= change duration of playing content before first ad).
updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000);
setAdGroupLoaded(/* adGroupIndex= */ 0);
long maxRendererReadPositionUs = FIRST_AD_START_TIME_US - 1000;
boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, /* rendererPositionUs= */ 0, maxRendererReadPositionUs);
assertThat(changeHandled).isFalse();
assertThat(getQueueLength()).isEqualTo(1);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.endPositionUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.durationUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
}
@Test
public void
updateQueuedPeriods_withDurationChangeInPlayingContentAfterReadingPositionInServerSideInsertedAd_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
adPlaybackState =
new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimes... */ FIRST_AD_START_TIME_US)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
SinglePeriodAdTimeline adTimeline =
new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
setupTimeline(adTimeline);
setAdGroupLoaded(/* adGroupIndex= */ 0);
enqueueNext(); // Content before ad.
enqueueNext(); // Ad.
enqueueNext(); // Content after ad.
// Change position of first ad (= change duration of playing content before first ad).
adPlaybackState =
new AdPlaybackState(
/* adsId= */ new Object(), /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
updateTimeline();
setAdGroupLoaded(/* adGroupIndex= */ 0);
long maxRendererReadPositionUs = FIRST_AD_START_TIME_US - 1000;
boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, /* rendererPositionUs= */ 0, maxRendererReadPositionUs);
assertThat(changeHandled).isTrue();
assertThat(getQueueLength()).isEqualTo(1);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.endPositionUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.durationUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
}
@Test
public void
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {