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 bebc6224ec..670353c4f9 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 @@ -1376,12 +1376,34 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - if (!queue.updateQueuedPeriods(rendererPositionUs)) { + if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) { seekToCurrentPosition(/* sendDiscontinuity= */ false); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } + private long getMaxRendererReadPositionUs() { + MediaPeriodHolder readingHolder = queue.getReadingPeriod(); + if (readingHolder == null) { + return 0; + } + long maxReadPositionUs = readingHolder.getRendererOffset(); + for (int i = 0; i < renderers.length; i++) { + if (renderers[i].getState() == Renderer.STATE_DISABLED + || renderers[i].getStream() != readingHolder.sampleStreams[i]) { + // Ignore disabled renderers and renderers with sample streams from previous periods. + continue; + } + long readingPositionUs = renderers[i].getReadingPositionUs(); + if (readingPositionUs == C.TIME_END_OF_SOURCE) { + return C.TIME_END_OF_SOURCE; + } else { + maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs); + } + } + return maxReadPositionUs; + } + private void handleSourceInfoRefreshEndedPlayback() { setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. 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 d6ff320295..64719a0ab4 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 @@ -61,8 +61,8 @@ import com.google.android.exoplayer2.util.Assertions; } /** - * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long)} to update the queued media - * periods to take into account the new timeline. + * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued + * media periods to take into account the new timeline. */ public void setTimeline(Timeline timeline) { this.timeline = timeline; @@ -293,9 +293,12 @@ import com.google.android.exoplayer2.util.Assertions; * consistent with the new timeline. * * @param rendererPositionUs The current renderer position in microseconds. + * @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read + * the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they + * have read to the end. * @return Whether the timeline change has been handled completely. */ - public boolean updateQueuedPeriods(long rendererPositionUs) { + public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) { // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be // handled here. @@ -327,8 +330,18 @@ import com.google.android.exoplayer2.util.Assertions; periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs); if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) { - // The period duration changed. Remove all subsequent periods. - return !removeAfter(periodHolder); + // The period duration changed. Remove all subsequent periods and check whether we read + // beyond the new duration. + long newDurationInRendererTime = + newPeriodInfo.durationUs == C.TIME_UNSET + ? Long.MAX_VALUE + : periodHolder.toRendererTime(newPeriodInfo.durationUs); + boolean isReadingAndReadBeyondNewDuration = + periodHolder == reading + && (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE + || maxRendererReadPositionUs >= newDurationInRendererTime); + boolean readingPeriodRemoved = removeAfter(periodHolder); + return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration; } previousPeriodHolder = periodHolder; 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 6016ec1db7..37f8a05790 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 @@ -37,6 +37,7 @@ import org.robolectric.RobolectricTestRunner; public final class MediaPeriodQueueTest { private static final long CONTENT_DURATION_US = 30 * C.MICROS_PER_SECOND; + private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND; private static final long FIRST_AD_START_TIME_US = 10 * C.MICROS_PER_SECOND; private static final long SECOND_AD_START_TIME_US = 20 * C.MICROS_PER_SECOND; @@ -65,8 +66,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { - setupInitialTimeline(/* initialPositionUs= */ 0); + public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { + setupTimeline(/* initialPositionUs= */ 0); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_UNSET, @@ -76,8 +77,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { - setupInitialTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0); + public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { + setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0); advance(); @@ -90,8 +91,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { - setupInitialTimeline( + public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { + setupTimeline( /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); @@ -128,8 +129,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { - setupInitialTimeline( + public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { + setupTimeline( /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, C.TIME_END_OF_SOURCE); @@ -164,8 +165,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { - setupInitialTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); + public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { + setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_END_OF_SOURCE, @@ -182,7 +183,168 @@ public final class MediaPeriodQueueTest { /* nextAdGroupIndex= */ C.INDEX_UNSET); } - private void setupInitialTimeline(long initialPositionUs, long... adGroupTimesUs) { + @Test + public void + updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + + // Change position of second ad (= change duration of content between ads). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US + 1); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0); + + assertThat(changeHandled).isTrue(); + assertThat(getQueueLength()).isEqualTo(3); + } + + @Test + public void + updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + advanceReading(); // Reading first ad. + + // Change position of first ad (= change duration of content before first ad). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US); + + assertThat(changeHandled).isFalse(); + assertThat(getQueueLength()).isEqualTo(1); + } + + @Test + public void + updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + advanceReading(); // Reading first ad. + advanceReading(); // Reading content between ads. + + // Change position of second ad (= change duration of content between ads). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US; + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, + /* maxRendererReadPositionUs= */ readingPositionAtStartOfContentBetweenAds); + + assertThat(changeHandled).isTrue(); + assertThat(getQueueLength()).isEqualTo(3); + } + + @Test + public void + updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + advanceReading(); // Reading first ad. + advanceReading(); // Reading content between ads. + + // Change position of second ad (= change duration of content between ads). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US; + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, + /* maxRendererReadPositionUs= */ readingPositionAtEndOfContentBetweenAds); + + assertThat(changeHandled).isFalse(); + assertThat(getQueueLength()).isEqualTo(3); + } + + @Test + public void + updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + advanceReading(); // Reading first ad. + advanceReading(); // Reading content between ads. + + // Change position of second ad (= change duration of content between ads). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE); + + assertThat(changeHandled).isFalse(); + assertThat(getQueueLength()).isEqualTo(3); + } + + private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { adPlaybackState = new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); @@ -206,9 +368,21 @@ public final class MediaPeriodQueueTest { } private void advance() { + enqueueNext(); + advancePlaying(); + } + + private void advancePlaying() { + mediaPeriodQueue.advancePlayingPeriod(); + } + + private void advanceReading() { + mediaPeriodQueue.advanceReadingPeriod(); + } + + private void enqueueNext() { mediaPeriodQueue.enqueueNextMediaPeriod( rendererCapabilities, trackSelector, allocator, mediaSource, getNextMediaPeriodInfo()); - mediaPeriodQueue.advancePlayingPeriod(); } private MediaPeriodInfo getNextMediaPeriodInfo() { @@ -216,10 +390,16 @@ public final class MediaPeriodQueueTest { } private void setAdGroupLoaded(int adGroupIndex) { + long[][] newDurations = new long[adPlaybackState.adGroupCount][]; + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + newDurations[i] = + i == adGroupIndex ? new long[] {AD_DURATION_US} : adPlaybackState.adGroups[i].durationsUs; + } adPlaybackState = adPlaybackState .withAdCount(adGroupIndex, /* adCount= */ 1) - .withAdUri(adGroupIndex, /* adIndexInAdGroup= */ 0, AD_URI); + .withAdUri(adGroupIndex, /* adIndexInAdGroup= */ 0, AD_URI) + .withAdDurationsUs(newDurations); updateTimeline(); } @@ -266,8 +446,18 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ 0, contentPositionUs, /* endPositionUs= */ C.TIME_UNSET, - /* durationUs= */ C.TIME_UNSET, + /* durationUs= */ AD_DURATION_US, /* isLastInTimelinePeriod= */ false, /* isFinal= */ false)); } + + private int getQueueLength() { + int length = 0; + MediaPeriodHolder periodHolder = mediaPeriodQueue.getFrontPeriod(); + while (periodHolder != null) { + length++; + periodHolder = periodHolder.getNext(); + } + return length; + } }