From 795210d7bc715b8745ee04e9dfa38fd0336f992e Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 14 May 2021 09:45:44 +0100 Subject: [PATCH] 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 --- .../google/android/exoplayer2/Timeline.java | 6 +- .../source/ads/AdPlaybackState.java | 26 ++- .../source/ads/AdPlaybackStateTest.java | 177 ++++++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 17 +- .../android/exoplayer2/MediaPeriodQueue.java | 17 ++ .../android/exoplayer2/ExoPlayerTest.java | 69 +++++++ .../exoplayer2/MediaPeriodQueueTest.java | 65 ++++++- 7 files changed, 365 insertions(+), 12 deletions(-) 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() {