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 0327c4f1a8..b8a2d2aae0 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 @@ -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(); 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 760cb32b17..32c84a0f8d 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 @@ -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) 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 1596b8ccda..f7134232bd 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 @@ -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); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index eef91d7088..48cf3defe3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -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()) { 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 9691ac6cb3..e32135fd50 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 @@ -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); + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index f1a48e58d7..d8cccbc297 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -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 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) { 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 7af94058b5..fcbeb09658 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 @@ -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() {