diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 2ddf09295e..8482a584d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Util.castNonNull; import android.annotation.SuppressLint; import android.os.Handler; @@ -418,16 +419,18 @@ import java.util.concurrent.TimeoutException; public void addMediaSources(int index, List mediaSources) { Assertions.checkArgument(index >= 0); validateMediaSources(mediaSources, /* mediaSourceReplacement= */ false); - int currentWindowIndex = getCurrentWindowIndex(); - long currentPositionMs = getCurrentPosition(); Timeline oldTimeline = getCurrentTimeline(); pendingOperationAcks++; List holders = addMediaSourceHolders(index, mediaSources); - PlaybackInfo playbackInfo = - maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); + Timeline newTimeline = createMaskingTimeline(); + PlaybackInfo newPlaybackInfo = + maskTimelineAndPosition( + playbackInfo, + newTimeline, + getPeriodPositionAfterTimelineChanged(oldTimeline, newTimeline)); internalPlayer.addMediaSources(index, holders, shuffleOrder); updatePlaybackInfo( - playbackInfo, + newPlaybackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, @@ -448,18 +451,20 @@ import java.util.concurrent.TimeoutException; && fromIndex <= toIndex && toIndex <= mediaSourceHolderSnapshots.size() && newFromIndex >= 0); - int currentWindowIndex = getCurrentWindowIndex(); - long currentPositionMs = getCurrentPosition(); Timeline oldTimeline = getCurrentTimeline(); pendingOperationAcks++; newFromIndex = Math.min(newFromIndex, mediaSourceHolderSnapshots.size() - (toIndex - fromIndex)); Util.moveItems(mediaSourceHolderSnapshots, fromIndex, toIndex, newFromIndex); - PlaybackInfo playbackInfo = - maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); + Timeline newTimeline = createMaskingTimeline(); + PlaybackInfo newPlaybackInfo = + maskTimelineAndPosition( + playbackInfo, + newTimeline, + getPeriodPositionAfterTimelineChanged(oldTimeline, newTimeline)); internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder); updatePlaybackInfo( - playbackInfo, + newPlaybackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, @@ -477,13 +482,18 @@ import java.util.concurrent.TimeoutException; @Override public void setShuffleOrder(ShuffleOrder shuffleOrder) { - PlaybackInfo playbackInfo = maskTimeline(); - maskWithCurrentPosition(); + Timeline timeline = createMaskingTimeline(); + PlaybackInfo newPlaybackInfo = + maskTimelineAndPosition( + playbackInfo, + timeline, + getPeriodPositionOrMaskWindowPosition( + timeline, getCurrentWindowIndex(), getCurrentPosition())); pendingOperationAcks++; this.shuffleOrder = shuffleOrder; internalPlayer.setShuffleOrder(shuffleOrder); updatePlaybackInfo( - playbackInfo, + newPlaybackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, @@ -521,7 +531,6 @@ import java.util.concurrent.TimeoutException; && playbackInfo.playbackSuppressionReason == playbackSuppressionReason) { return; } - maskWithCurrentPosition(); pendingOperationAcks++; PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); @@ -589,14 +598,18 @@ import java.util.concurrent.TimeoutException; new ExoPlayerImplInternal.PlaybackInfoUpdate(playbackInfo)); return; } - maskWindowIndexAndPositionForSeek(timeline, windowIndex, positionMs); @Player.State int newPlaybackState = getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING; - PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackState(newPlaybackState); + PlaybackInfo newPlaybackInfo = this.playbackInfo.copyWithPlaybackState(newPlaybackState); + newPlaybackInfo = + maskTimelineAndPosition( + newPlaybackInfo, + timeline, + getPeriodPositionOrMaskWindowPosition(timeline, windowIndex, positionMs)); internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); updatePlaybackInfo( - playbackInfo, + newPlaybackInfo, /* positionDiscontinuity= */ true, /* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK, /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, @@ -732,7 +745,7 @@ import java.util.concurrent.TimeoutException; @Override public int getCurrentPeriodIndex() { - if (shouldMaskPosition()) { + if (playbackInfo.timeline.isEmpty()) { return maskingPeriodIndex; } else { return playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); @@ -758,7 +771,7 @@ import java.util.concurrent.TimeoutException; @Override public long getCurrentPosition() { - if (shouldMaskPosition()) { + if (playbackInfo.timeline.isEmpty()) { return maskingWindowPositionMs; } else if (playbackInfo.periodId.isAd()) { return C.usToMs(playbackInfo.positionUs); @@ -784,7 +797,7 @@ import java.util.concurrent.TimeoutException; @Override public boolean isPlayingAd() { - return !shouldMaskPosition() && playbackInfo.periodId.isAd(); + return playbackInfo.periodId.isAd(); } @Override @@ -811,7 +824,7 @@ import java.util.concurrent.TimeoutException; @Override public long getContentBufferedPosition() { - if (shouldMaskPosition()) { + if (playbackInfo.timeline.isEmpty()) { return maskingWindowPositionMs; } if (playbackInfo.loadingMediaPeriodId.windowSequenceNumber @@ -864,7 +877,7 @@ import java.util.concurrent.TimeoutException; } private int getCurrentWindowIndexInternal() { - if (shouldMaskPosition()) { + if (playbackInfo.timeline.isEmpty()) { return maskingWindowIndex; } else { return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) @@ -909,7 +922,8 @@ import java.util.concurrent.TimeoutException; if (pendingOperationAcks == 0) { Timeline newTimeline = playbackInfoUpdate.playbackInfo.timeline; if (!this.playbackInfo.timeline.isEmpty() && newTimeline.isEmpty()) { - // Update the masking variables, which are used when the timeline becomes empty. + // Update the masking variables, which are used when the timeline becomes empty because a + // ConcatenatingMediaSource has been cleared. resetMaskingPosition(); } if (!newTimeline.isEmpty()) { @@ -938,8 +952,6 @@ import java.util.concurrent.TimeoutException; removeMediaSourceHolders( /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolderSnapshots.size()); resetMaskingPosition(); - } else { - maskWithCurrentPosition(); } Timeline timeline = playbackInfo.timeline; MediaPeriodId mediaPeriodId = playbackInfo.periodId; @@ -1006,8 +1018,7 @@ import java.util.concurrent.TimeoutException; } List holders = addMediaSourceHolders(/* index= */ 0, mediaSources); - PlaybackInfo playbackInfo = maskTimeline(); - Timeline timeline = playbackInfo.timeline; + Timeline timeline = createMaskingTimeline(); if (!timeline.isEmpty() && startWindowIndex >= timeline.getWindowCount()) { throw new IllegalSeekPositionException(timeline, startWindowIndex, startPositionMs); } @@ -1019,11 +1030,14 @@ import java.util.concurrent.TimeoutException; startWindowIndex = currentWindowIndex; startPositionMs = currentPositionMs; } - maskWindowIndexAndPositionForSeek( - timeline, startWindowIndex == C.INDEX_UNSET ? 0 : startWindowIndex, startPositionMs); + PlaybackInfo newPlaybackInfo = + maskTimelineAndPosition( + playbackInfo, + timeline, + getPeriodPositionOrMaskWindowPosition(timeline, startWindowIndex, startPositionMs)); // Mask the playback state. - int maskingPlaybackState = playbackInfo.playbackState; - if (startWindowIndex != C.INDEX_UNSET && playbackInfo.playbackState != STATE_IDLE) { + int maskingPlaybackState = newPlaybackInfo.playbackState; + if (startWindowIndex != C.INDEX_UNSET && newPlaybackInfo.playbackState != STATE_IDLE) { // Position reset to startWindowIndex (results in pending initial seek). if (timeline.isEmpty() || startWindowIndex >= timeline.getWindowCount()) { // Setting an empty timeline or invalid seek transitions to ended. @@ -1032,11 +1046,11 @@ import java.util.concurrent.TimeoutException; maskingPlaybackState = STATE_BUFFERING; } } - playbackInfo = playbackInfo.copyWithPlaybackState(maskingPlaybackState); + newPlaybackInfo = newPlaybackInfo.copyWithPlaybackState(maskingPlaybackState); internalPlayer.setMediaSources( holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder); updatePlaybackInfo( - playbackInfo, + newPlaybackInfo, /* positionDiscontinuity= */ false, /* ignored */ Player.DISCONTINUITY_REASON_INTERNAL, /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, @@ -1064,26 +1078,29 @@ import java.util.concurrent.TimeoutException; Assertions.checkArgument( fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolderSnapshots.size()); int currentWindowIndex = getCurrentWindowIndex(); - long currentPositionMs = getCurrentPosition(); Timeline oldTimeline = getCurrentTimeline(); int currentMediaSourceCount = mediaSourceHolderSnapshots.size(); pendingOperationAcks++; removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); - PlaybackInfo playbackInfo = - maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); + Timeline newTimeline = createMaskingTimeline(); + PlaybackInfo newPlaybackInfo = + maskTimelineAndPosition( + playbackInfo, + newTimeline, + getPeriodPositionAfterTimelineChanged(oldTimeline, newTimeline)); // Player transitions to STATE_ENDED if the current index is part of the removed tail. final boolean transitionsToEnded = - playbackInfo.playbackState != STATE_IDLE - && playbackInfo.playbackState != STATE_ENDED + newPlaybackInfo.playbackState != STATE_IDLE + && newPlaybackInfo.playbackState != STATE_ENDED && fromIndex < toIndex && toIndex == currentMediaSourceCount - && currentWindowIndex >= playbackInfo.timeline.getWindowCount(); + && currentWindowIndex >= newPlaybackInfo.timeline.getWindowCount(); if (transitionsToEnded) { - playbackInfo = playbackInfo.copyWithPlaybackState(STATE_ENDED); + newPlaybackInfo = newPlaybackInfo.copyWithPlaybackState(STATE_ENDED); } internalPlayer.removeMediaSources(fromIndex, toIndex, shuffleOrder); updatePlaybackInfo( - playbackInfo, + newPlaybackInfo, /* positionDiscontinuity= */ false, /* ignored */ Player.DISCONTINUITY_REASON_INTERNAL, /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, @@ -1131,107 +1148,163 @@ import java.util.concurrent.TimeoutException; } } - private PlaybackInfo maskTimeline() { - return playbackInfo.copyWithTimeline( - mediaSourceHolderSnapshots.isEmpty() - ? Timeline.EMPTY - : new PlaylistTimeline(mediaSourceHolderSnapshots, shuffleOrder)); + private Timeline createMaskingTimeline() { + return new PlaylistTimeline(mediaSourceHolderSnapshots, shuffleOrder); } - private PlaybackInfo maskTimelineAndWindowIndex( - int currentWindowIndex, long currentPositionMs, Timeline oldTimeline) { - PlaybackInfo playbackInfo = maskTimeline(); - Timeline maskingTimeline = playbackInfo.timeline; - if (oldTimeline.isEmpty()) { - // The index is the default index or was set by a seek in the empty old timeline. - maskingWindowIndex = currentWindowIndex; - if (!maskingTimeline.isEmpty() && currentWindowIndex >= maskingTimeline.getWindowCount()) { - // The seek is not valid in the new timeline. - maskWithDefaultPosition(maskingTimeline); - } + private PlaybackInfo maskTimelineAndPosition( + PlaybackInfo playbackInfo, Timeline timeline, @Nullable Pair periodPosition) { + Assertions.checkArgument(timeline.isEmpty() || periodPosition != null); + Timeline oldTimeline = playbackInfo.timeline; + // Mask the timeline. + playbackInfo = playbackInfo.copyWithTimeline(timeline); + + if (timeline.isEmpty()) { + // Reset periodId and loadingPeriodId. + MediaPeriodId dummyMediaPeriodId = PlaybackInfo.getDummyPeriodForEmptyTimeline(); + playbackInfo = + playbackInfo.copyWithNewPosition( + dummyMediaPeriodId, + /* positionUs= */ C.msToUs(maskingWindowPositionMs), + /* requestedContentPositionUs= */ C.msToUs(maskingWindowPositionMs), + /* totalBufferedDurationUs= */ 0, + TrackGroupArray.EMPTY, + emptyTrackSelectorResult); + playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(dummyMediaPeriodId); + playbackInfo.bufferedPositionUs = playbackInfo.positionUs; return playbackInfo; } - @Nullable - Pair periodPosition = - oldTimeline.getPeriodPosition( - window, - period, - currentWindowIndex, - C.msToUs(currentPositionMs), - /* defaultPositionProjectionUs= */ 0); - Object periodUid = Util.castNonNull(periodPosition).first; - if (maskingTimeline.getIndexOfPeriod(periodUid) != C.INDEX_UNSET) { - // Get the window index of the current period that exists in the new timeline also. - maskingWindowIndex = maskingTimeline.getPeriodByUid(periodUid, period).windowIndex; - maskingPeriodIndex = maskingTimeline.getIndexOfPeriod(periodUid); - maskingWindowPositionMs = currentPositionMs; - } else { - // Period uid not found in new timeline. Try to get subsequent period. - @Nullable - Object nextPeriodUid = - ExoPlayerImplInternal.resolveSubsequentPeriod( - window, - period, - repeatMode, - shuffleModeEnabled, - periodUid, - oldTimeline, - maskingTimeline); - if (nextPeriodUid != null) { - // Set masking to the default position of the window of the subsequent period. - maskingWindowIndex = maskingTimeline.getPeriodByUid(nextPeriodUid, period).windowIndex; - maskingPeriodIndex = maskingTimeline.getWindow(maskingWindowIndex, window).firstPeriodIndex; - maskingWindowPositionMs = window.getDefaultPositionMs(); - } else { - // Reset if no subsequent period is found. - maskWithDefaultPosition(maskingTimeline); + + Object oldPeriodUid = playbackInfo.periodId.periodUid; + boolean playingPeriodChanged = !oldPeriodUid.equals(castNonNull(periodPosition).first); + MediaPeriodId newPeriodId = + playingPeriodChanged ? new MediaPeriodId(periodPosition.first) : playbackInfo.periodId; + long newContentPositionUs = periodPosition.second; + long oldContentPositionUs = C.msToUs(getContentPosition()); + if (!oldTimeline.isEmpty()) { + oldContentPositionUs -= + oldTimeline.getPeriodByUid(oldPeriodUid, period).getPositionInWindowUs(); + } + + if (playingPeriodChanged || newContentPositionUs < oldContentPositionUs) { + checkState(!newPeriodId.isAd()); + // The playing period changes or a backwards seek within the playing period occurs. + playbackInfo = + playbackInfo.copyWithNewPosition( + newPeriodId, + /* positionUs= */ newContentPositionUs, + /* requestedContentPositionUs= */ newContentPositionUs, + /* totalBufferedDurationUs= */ 0, + playingPeriodChanged ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + playingPeriodChanged ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); + playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(newPeriodId); + playbackInfo.bufferedPositionUs = newContentPositionUs; + } else if (newContentPositionUs == oldContentPositionUs) { + // Period position remains unchanged. + int loadingPeriodIndex = + timeline.getIndexOfPeriod(playbackInfo.loadingMediaPeriodId.periodUid); + if (loadingPeriodIndex == C.INDEX_UNSET + || timeline.getPeriod(loadingPeriodIndex, period).windowIndex + != timeline.getPeriodByUid(newPeriodId.periodUid, period).windowIndex) { + // Discard periods after the playing period, if the loading period is discarded or the + // playing and loading period are not in the same window. + timeline.getPeriodByUid(newPeriodId.periodUid, period); + long maskedBufferedPositionUs = + newPeriodId.isAd() + ? period.getAdDurationUs(newPeriodId.adGroupIndex, newPeriodId.adIndexInAdGroup) + : period.durationUs; + playbackInfo = + playbackInfo.copyWithNewPosition( + newPeriodId, + /* positionUs= */ playbackInfo.positionUs, + /* requestedContentPositionUs= */ playbackInfo.positionUs, + /* totalBufferedDurationUs= */ maskedBufferedPositionUs - playbackInfo.positionUs, + playbackInfo.trackGroups, + playbackInfo.trackSelectorResult); + playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(newPeriodId); + playbackInfo.bufferedPositionUs = maskedBufferedPositionUs; } + } else { + checkState(!newPeriodId.isAd()); + // A forward seek within the playing period (timeline did not change). + long maskedTotalBufferedDurationUs = + Math.max( + 0, + playbackInfo.totalBufferedDurationUs - (newContentPositionUs - oldContentPositionUs)); + long maskedBufferedPositionUs = playbackInfo.bufferedPositionUs; + if (playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId)) { + maskedBufferedPositionUs = newContentPositionUs + maskedTotalBufferedDurationUs; + } + playbackInfo = + playbackInfo.copyWithNewPosition( + newPeriodId, + /* positionUs= */ newContentPositionUs, + /* requestedContentPositionUs= */ newContentPositionUs, + maskedTotalBufferedDurationUs, + playbackInfo.trackGroups, + playbackInfo.trackSelectorResult); + playbackInfo.bufferedPositionUs = maskedBufferedPositionUs; } return playbackInfo; } - private void maskWindowIndexAndPositionForSeek( - Timeline timeline, int windowIndex, long positionMs) { - maskingWindowIndex = windowIndex; - if (timeline.isEmpty()) { - maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; - maskingPeriodIndex = 0; - } else if (windowIndex >= timeline.getWindowCount()) { - // An initial seek now proves to be invalid in the actual timeline. - maskWithDefaultPosition(timeline); + @Nullable + private Pair getPeriodPositionAfterTimelineChanged( + Timeline oldTimeline, Timeline newTimeline) { + long currentPositionMs = getContentPosition(); + if (oldTimeline.isEmpty() || newTimeline.isEmpty()) { + boolean isCleared = !oldTimeline.isEmpty() && newTimeline.isEmpty(); + return getPeriodPositionOrMaskWindowPosition( + newTimeline, + isCleared ? C.INDEX_UNSET : getCurrentWindowIndexInternal(), + isCleared ? C.TIME_UNSET : currentPositionMs); + } + int currentWindowIndex = getCurrentWindowIndex(); + @Nullable + Pair oldPeriodPosition = + oldTimeline.getPeriodPosition( + window, period, currentWindowIndex, C.msToUs(currentPositionMs)); + Object periodUid = castNonNull(oldPeriodPosition).first; + if (newTimeline.getIndexOfPeriod(periodUid) != C.INDEX_UNSET) { + // The old period position is still available in the new timeline. + return oldPeriodPosition; + } + // Period uid not found in new timeline. Try to get subsequent period. + @Nullable + Object nextPeriodUid = + ExoPlayerImplInternal.resolveSubsequentPeriod( + window, period, repeatMode, shuffleModeEnabled, periodUid, oldTimeline, newTimeline); + if (nextPeriodUid != null) { + // Reset position to the default position of the window of the subsequent period. + newTimeline.getPeriodByUid(nextPeriodUid, period); + return getPeriodPositionOrMaskWindowPosition( + newTimeline, + period.windowIndex, + newTimeline.getWindow(period.windowIndex, window).getDefaultPositionMs()); } else { - long windowPositionUs = - positionMs == C.TIME_UNSET - ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() - : C.msToUs(positionMs); - Pair periodUidAndPosition = - timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); - maskingWindowPositionMs = C.usToMs(windowPositionUs); - maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first); + // No subsequent period found and the new timeline is not empty. Use the default position. + return getPeriodPositionOrMaskWindowPosition( + newTimeline, /* windowIndex= */ C.INDEX_UNSET, /* windowPositionMs= */ C.TIME_UNSET); } } - private void maskWithCurrentPosition() { - maskingWindowIndex = getCurrentWindowIndexInternal(); - maskingPeriodIndex = getCurrentPeriodIndex(); - maskingWindowPositionMs = getCurrentPosition(); - } - - private void maskWithDefaultPosition(Timeline timeline) { + @Nullable + private Pair getPeriodPositionOrMaskWindowPosition( + Timeline timeline, int windowIndex, long windowPositionMs) { if (timeline.isEmpty()) { - resetMaskingPosition(); - return; + // If empty we store the initial seek in the masking variables. + maskingWindowIndex = windowIndex; + maskingWindowPositionMs = windowPositionMs == C.TIME_UNSET ? 0 : windowPositionMs; + maskingPeriodIndex = 0; + return null; } - maskingWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); - timeline.getWindow(maskingWindowIndex, window); - maskingWindowPositionMs = window.getDefaultPositionMs(); - maskingPeriodIndex = window.firstPeriodIndex; - } - - private void resetMaskingPosition() { - maskingWindowIndex = C.INDEX_UNSET; - maskingWindowPositionMs = 0; - maskingPeriodIndex = 0; + if (windowIndex == C.INDEX_UNSET || windowIndex >= timeline.getWindowCount()) { + // Use default position of timeline if window index still unset or if a previous initial seek + // now turns out to be invalid. + windowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + windowPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs(); + } + return timeline.getPeriodPosition(window, period, windowIndex, C.msToUs(windowPositionMs)); } private void notifyListeners(ListenerInvocation listenerInvocation) { @@ -1251,6 +1324,12 @@ import java.util.concurrent.TimeoutException; } } + private void resetMaskingPosition() { + maskingWindowIndex = C.INDEX_UNSET; + maskingWindowPositionMs = 0; + maskingPeriodIndex = 0; + } + private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) { long positionMs = C.usToMs(positionUs); playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); @@ -1258,10 +1337,6 @@ import java.util.concurrent.TimeoutException; return positionMs; } - private boolean shouldMaskPosition() { - return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0; - } - private final class PlaybackUpdateListenerImpl implements ExoPlayerImplInternal.PlaybackUpdateListener, Handler.Callback { private static final int MSG_PLAYBACK_INFO_CHANGED = 0; 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 d1a9d82ad4..947a2975d0 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 @@ -32,6 +32,7 @@ import android.content.Context; import android.content.Intent; import android.graphics.SurfaceTexture; import android.media.AudioManager; +import android.net.Uri; import android.os.Looper; import android.view.Surface; import android.view.View; @@ -1062,131 +1063,172 @@ public final class ExoPlayerTest { } @Test - public void stopDoesNotResetPosition() throws Exception { + public void stop_withoutReset_doesNotResetPosition_correctMasking() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final long[] positionHolder = new long[1]; + int[] currentWindowIndex = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + long[] currentPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + long[] bufferedPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + long[] totalBufferedDuration = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) .pause() + .seek(/* windowIndex= */ 1, /* positionMs= */ 1000) .waitForPlaybackState(Player.STATE_READY) - .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 50) - .stop() .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { - positionHolder[0] = player.getCurrentPosition(); + currentWindowIndex[0] = player.getCurrentWindowIndex(); + currentPosition[0] = player.getCurrentPosition(); + bufferedPosition[0] = player.getBufferedPosition(); + totalBufferedDuration[0] = player.getTotalBufferedDuration(); + player.stop(/* reset= */ false); + currentWindowIndex[1] = player.getCurrentWindowIndex(); + currentPosition[1] = player.getCurrentPosition(); + bufferedPosition[1] = player.getBufferedPosition(); + totalBufferedDuration[1] = player.getTotalBufferedDuration(); + } + }) + .waitForPlaybackState(Player.STATE_IDLE) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndex[2] = player.getCurrentWindowIndex(); + currentPosition[2] = player.getCurrentPosition(); + bufferedPosition[2] = player.getBufferedPosition(); + totalBufferedDuration[2] = player.getTotalBufferedDuration(); } }) .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder(context) - .setTimeline(timeline) + .setMediaSources(mediaSource, mediaSource) .setActionSchedule(actionSchedule) .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(placeholderTimeline, timeline); testRunner.assertTimelineChangeReasonsEqual( Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - testRunner.assertNoPositionDiscontinuities(); - assertThat(positionHolder[0]).isAtLeast(50L); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + + assertThat(currentWindowIndex[0]).isEqualTo(1); + assertThat(currentPosition[0]).isEqualTo(1000); + assertThat(bufferedPosition[0]).isEqualTo(10000); + assertThat(totalBufferedDuration[0]).isEqualTo(9000); + + assertThat(currentWindowIndex[1]).isEqualTo(1); + assertThat(currentPosition[1]).isEqualTo(1000); + assertThat(bufferedPosition[1]).isEqualTo(1000); + assertThat(totalBufferedDuration[1]).isEqualTo(0); + + assertThat(currentWindowIndex[2]).isEqualTo(1); + assertThat(currentPosition[2]).isEqualTo(1000); + assertThat(bufferedPosition[2]).isEqualTo(1000); + assertThat(totalBufferedDuration[2]).isEqualTo(0); } @Test - public void stopWithoutResetDoesNotResetPosition() throws Exception { + public void stop_withoutReset_releasesMediaSource() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final long[] positionHolder = new long[1]; + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) - .pause() .waitForPlaybackState(Player.STATE_READY) - .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 50) .stop(/* reset= */ false) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - positionHolder[0] = player.getCurrentPosition(); - } - }) .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context) - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(placeholderTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - testRunner.assertNoPositionDiscontinuities(); - assertThat(positionHolder[0]).isAtLeast(50L); + + new ExoPlayerTestRunner.Builder(context) + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + mediaSource.assertReleased(); } @Test - public void stopWithResetDoesResetPosition() throws Exception { + public void stop_withReset_doesResetPosition_correctMasking() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final long[] positionHolder = new long[1]; + int[] currentWindowIndex = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + long[] currentPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + long[] bufferedPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + long[] totalBufferedDuration = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) .pause() + .seek(/* windowIndex= */ 1, /* positionMs= */ 1000) .waitForPlaybackState(Player.STATE_READY) - .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 50) - .stop(/* reset= */ true) .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { - positionHolder[0] = player.getCurrentPosition(); + currentWindowIndex[0] = player.getCurrentWindowIndex(); + currentPosition[0] = player.getCurrentPosition(); + bufferedPosition[0] = player.getBufferedPosition(); + totalBufferedDuration[0] = player.getTotalBufferedDuration(); + player.stop(/* reset= */ true); + currentWindowIndex[1] = player.getCurrentWindowIndex(); + currentPosition[1] = player.getCurrentPosition(); + bufferedPosition[1] = player.getBufferedPosition(); + totalBufferedDuration[1] = player.getTotalBufferedDuration(); + } + }) + .waitForPlaybackState(Player.STATE_IDLE) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndex[2] = player.getCurrentWindowIndex(); + currentPosition[2] = player.getCurrentPosition(); + bufferedPosition[2] = player.getBufferedPosition(); + totalBufferedDuration[2] = player.getTotalBufferedDuration(); } }) .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder(context) - .setTimeline(timeline) + .setMediaSources(mediaSource, mediaSource) .setActionSchedule(actionSchedule) .build() .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(placeholderTimeline, timeline, Timeline.EMPTY); + .blockUntilActionScheduleFinished(TIMEOUT_MS); + testRunner.assertTimelineChangeReasonsEqual( Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); - testRunner.assertNoPositionDiscontinuities(); - assertThat(positionHolder[0]).isEqualTo(0); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + + assertThat(currentWindowIndex[0]).isEqualTo(1); + assertThat(currentPosition[0]).isGreaterThan(0); + assertThat(bufferedPosition[0]).isEqualTo(10000); + assertThat(totalBufferedDuration[0]).isEqualTo(10000 - currentPosition[0]); + + assertThat(currentWindowIndex[1]).isEqualTo(0); + assertThat(currentPosition[1]).isEqualTo(0); + assertThat(bufferedPosition[1]).isEqualTo(0); + assertThat(totalBufferedDuration[1]).isEqualTo(0); + + assertThat(currentWindowIndex[2]).isEqualTo(0); + assertThat(currentPosition[2]).isEqualTo(0); + assertThat(bufferedPosition[2]).isEqualTo(0); + assertThat(totalBufferedDuration[2]).isEqualTo(0); } @Test - public void stopWithoutResetReleasesMediaSource() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = - new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .waitForPlaybackState(Player.STATE_READY) - .stop(/* reset= */ false) - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context) - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS); - mediaSource.assertReleased(); - testRunner.blockUntilEnded(TIMEOUT_MS); - } - - @Test - public void stopWithResetReleasesMediaSource() throws Exception { + public void stop_withReset_releasesMediaSource() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); final FakeMediaSource mediaSource = new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); @@ -1195,15 +1237,81 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_READY) .stop(/* reset= */ true) .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context) - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS); + + new ExoPlayerTestRunner.Builder(context) + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + mediaSource.assertReleased(); - testRunner.blockUntilEnded(TIMEOUT_MS); + } + + @Test + public void release_correctMasking() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + int[] currentWindowIndex = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + long[] currentPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + long[] bufferedPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + long[] totalBufferedDuration = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .seek(/* windowIndex= */ 1, /* positionMs= */ 1000) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndex[0] = player.getCurrentWindowIndex(); + currentPosition[0] = player.getCurrentPosition(); + bufferedPosition[0] = player.getBufferedPosition(); + totalBufferedDuration[0] = player.getTotalBufferedDuration(); + player.release(); + currentWindowIndex[1] = player.getCurrentWindowIndex(); + currentPosition[1] = player.getCurrentPosition(); + bufferedPosition[1] = player.getBufferedPosition(); + totalBufferedDuration[1] = player.getTotalBufferedDuration(); + } + }) + .waitForPlaybackState(Player.STATE_IDLE) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndex[2] = player.getCurrentWindowIndex(); + currentPosition[2] = player.getCurrentPosition(); + bufferedPosition[2] = player.getBufferedPosition(); + totalBufferedDuration[2] = player.getTotalBufferedDuration(); + } + }) + .build(); + + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource, mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + + assertThat(currentWindowIndex[0]).isEqualTo(1); + assertThat(currentPosition[0]).isGreaterThan(0); + assertThat(bufferedPosition[0]).isEqualTo(10000); + assertThat(totalBufferedDuration[0]).isEqualTo(10000 - currentPosition[0]); + + assertThat(currentWindowIndex[1]).isEqualTo(1); + assertThat(currentPosition[1]).isEqualTo(currentPosition[0]); + assertThat(bufferedPosition[1]).isEqualTo(1000); + assertThat(totalBufferedDuration[1]).isEqualTo(0); + + assertThat(currentWindowIndex[2]).isEqualTo(1); + assertThat(currentPosition[2]).isEqualTo(currentPosition[0]); + assertThat(bufferedPosition[2]).isEqualTo(1000); + assertThat(totalBufferedDuration[2]).isEqualTo(0); } @Test @@ -3533,26 +3641,34 @@ public final class ExoPlayerTest { FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); final int[] windowIndex = {C.INDEX_UNSET}; - final long[] positionMs = {C.TIME_UNSET}; + final long[] positionMs = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) .pause() .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) - .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) + .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 3000) .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { + positionMs[0] = player.getCurrentPosition(); + bufferedPositions[0] = player.getBufferedPosition(); + //noinspection deprecation player.prepare(mediaSource); - player.seekTo(/* positionMs= */ 5000); + player.seekTo(/* positionMs= */ 7000); + positionMs[1] = player.getCurrentPosition(); + bufferedPositions[1] = player.getBufferedPosition(); } }) + .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { windowIndex[0] = player.getCurrentWindowIndex(); - positionMs[0] = player.getCurrentPosition(); + positionMs[2] = player.getCurrentPosition(); + bufferedPositions[2] = player.getBufferedPosition(); } }) .build(); @@ -3564,7 +3680,13 @@ public final class ExoPlayerTest { .blockUntilActionScheduleFinished(TIMEOUT_MS); assertThat(windowIndex[0]).isEqualTo(0); - assertThat(positionMs[0]).isAtLeast(5000L); + assertThat(positionMs[0]).isAtLeast(3000L); + assertThat(positionMs[1]).isEqualTo(7000L); + assertThat(positionMs[2]).isEqualTo(7000L); + assertThat(bufferedPositions[0]).isAtLeast(3000L); + assertThat(bufferedPositions[1]).isEqualTo(7000L); + assertThat(bufferedPositions[2]) + .isEqualTo(fakeTimeline.getWindow(0, new Window()).getDurationMs()); } @Test @@ -3573,26 +3695,34 @@ public final class ExoPlayerTest { FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); final int[] windowIndex = {C.INDEX_UNSET}; - final long[] positionMs = {C.TIME_UNSET}; + final long[] positionMs = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) .pause() .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) - .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) + .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 3000) + .pause() .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { - player.setMediaSource(mediaSource, /* startPositionMs= */ 5000); + positionMs[0] = player.getCurrentPosition(); + bufferedPositions[0] = player.getBufferedPosition(); + player.setMediaSource(mediaSource, /* startPositionMs= */ 7000); player.prepare(); + positionMs[1] = player.getCurrentPosition(); + bufferedPositions[1] = player.getBufferedPosition(); } }) + .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { windowIndex[0] = player.getCurrentWindowIndex(); - positionMs[0] = player.getCurrentPosition(); + positionMs[2] = player.getCurrentPosition(); + bufferedPositions[2] = player.getBufferedPosition(); } }) .build(); @@ -3604,7 +3734,819 @@ public final class ExoPlayerTest { .blockUntilActionScheduleFinished(TIMEOUT_MS); assertThat(windowIndex[0]).isEqualTo(0); - assertThat(positionMs[0]).isAtLeast(5000L); + assertThat(positionMs[0]).isAtLeast(3000); + assertThat(positionMs[1]).isEqualTo(7000); + assertThat(positionMs[2]).isEqualTo(7000); + assertThat(bufferedPositions[0]).isAtLeast(3000); + assertThat(bufferedPositions[1]).isEqualTo(7000); + assertThat(bufferedPositions[2]) + .isEqualTo(fakeTimeline.getWindow(0, new Window()).getDurationMs()); + } + + @Test + public void seekTo_singlePeriod_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(9000); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 9200)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isEqualTo(9000); + assertThat(bufferedPositions[0]).isEqualTo(9200); + assertThat(totalBufferedDuration[0]).isEqualTo(200); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(9200); + assertThat(totalBufferedDuration[1]).isEqualTo(200); + } + + @Test + public void seekTo_singlePeriod_beyondBufferedData_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(9200); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 9200)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isEqualTo(9200); + assertThat(bufferedPositions[0]).isEqualTo(9200); + assertThat(totalBufferedDuration[0]).isEqualTo(0); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(9200); + assertThat(totalBufferedDuration[1]).isEqualTo(0); + } + + @Test + public void seekTo_backwardsSinglePeriod_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(1000); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 9200)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isEqualTo(1000); + assertThat(bufferedPositions[0]).isEqualTo(1000); + assertThat(totalBufferedDuration[0]).isEqualTo(0); + } + + @Test + public void seekTo_backwardsMultiplePeriods_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(0, 1000); + } + }, + /* pauseWindowIndex= */ 1, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 9200)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isEqualTo(1000); + assertThat(bufferedPositions[0]).isEqualTo(1000); + assertThat(totalBufferedDuration[0]).isEqualTo(0); + } + + @Test + public void seekTo_toUnbufferedPeriod_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(2, 1000); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 0)); + + assertThat(windowIndex[0]).isEqualTo(2); + assertThat(positionMs[0]).isEqualTo(1000); + assertThat(bufferedPositions[0]).isEqualTo(1000); + assertThat(totalBufferedDuration[0]).isEqualTo(0); + + assertThat(windowIndex[1]).isEqualTo(2); + assertThat(positionMs[1]).isEqualTo(1000); + assertThat(bufferedPositions[1]).isEqualTo(1000); + assertThat(totalBufferedDuration[1]).isEqualTo(0); + } + + @Test + public void seekTo_toLoadingPeriod_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(1, 1000); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); + + assertThat(windowIndex[0]).isEqualTo(1); + assertThat(positionMs[0]).isEqualTo(1000); + // TODO(b/160450903): Verify masking of buffering properties when behaviour in EPII is fully + // covered. + // assertThat(bufferedPositions[0]).isEqualTo(10_000); + // assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(10_000); + assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]); + } + + @Test + public void seekTo_toLoadingPeriod_withinPartiallyBufferedData_correctMaskingPosition() + throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(1, 1000); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); + + assertThat(windowIndex[0]).isEqualTo(1); + assertThat(positionMs[0]).isEqualTo(1000); + // TODO(b/160450903): Verify masking of buffering properties when behaviour in EPII is fully + // covered. + // assertThat(bufferedPositions[0]).isEqualTo(1000); + // assertThat(totalBufferedDuration[0]).isEqualTo(0); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(4000); + assertThat(totalBufferedDuration[1]).isEqualTo(3000); + } + + @Test + public void seekTo_toLoadingPeriod_beyondBufferedData_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(1, 5000); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); + + assertThat(windowIndex[0]).isEqualTo(1); + assertThat(positionMs[0]).isEqualTo(5000); + assertThat(bufferedPositions[0]).isEqualTo(5000); + assertThat(totalBufferedDuration[0]).isEqualTo(0); + + assertThat(windowIndex[1]).isEqualTo(1); + assertThat(positionMs[1]).isEqualTo(5000); + assertThat(bufferedPositions[1]).isEqualTo(5000); + assertThat(totalBufferedDuration[1]).isEqualTo(0); + } + + @Test + public void seekTo_toInnerFullyBufferedPeriod_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(1, 5000); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); + + assertThat(windowIndex[0]).isEqualTo(1); + assertThat(positionMs[0]).isEqualTo(5000); + // TODO(b/160450903): Verify masking of buffering properties when behaviour in EPII is fully + // covered. + // assertThat(bufferedPositions[0]).isEqualTo(10_000); + // assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(10_000); + assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]); + } + + @Test + public void addMediaSource_withinBufferedPeriods_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addMediaSource( + /* index= */ 1, createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 0)); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isAtLeast(8000); + assertThat(bufferedPositions[0]).isEqualTo(10_000); + assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(10_000); + assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]); + } + + @Test + public void moveMediaItem_behindLoadingPeriod_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 2); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isAtLeast(8000); + assertThat(bufferedPositions[0]).isEqualTo(10_000); + assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(10_000); + assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]); + } + + @Test + public void moveMediaItem_undloadedBehindPlaying_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.moveMediaItem(/* currentIndex= */ 3, /* newIndex= */ 1); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 0)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isAtLeast(8000); + assertThat(bufferedPositions[0]).isEqualTo(10_000); + assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(10000); + assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]); + } + + @Test + public void removeMediaItem_removePlayingWindow_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.removeMediaItem(/* index= */ 0); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isEqualTo(0); + // TODO(b/160450903): Verify masking of buffering properties when behaviour in EPII is fully + // covered. + // assertThat(bufferedPositions[0]).isEqualTo(4000); + // assertThat(totalBufferedDuration[0]).isEqualTo(4000); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(4000); + assertThat(totalBufferedDuration[1]).isEqualTo(4000); + } + + @Test + public void removeMediaItem_removeLoadingWindow_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.removeMediaItem(/* index= */ 2); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isAtLeast(8000); + assertThat(bufferedPositions[0]).isEqualTo(10_000); + assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(10_000); + assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]); + } + + @Test + public void removeMediaItem_removeInnerFullyBufferedWindow_correctMaskingPosition() + throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.removeMediaItem(/* index= */ 1); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isGreaterThan(8000); + assertThat(bufferedPositions[0]).isEqualTo(10_000); + assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]); + + assertThat(windowIndex[1]).isEqualTo(0); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(10_000); + assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[0]); + } + + @Test + public void clearMediaItems_correctMaskingPosition() throws Exception { + final int[] windowIndex = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET}; + + runPositionMaskingCapturingActionSchedule( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.clearMediaItems(); + } + }, + /* pauseWindowIndex= */ 0, + windowIndex, + positionMs, + bufferedPositions, + totalBufferedDuration, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isEqualTo(0); + assertThat(bufferedPositions[0]).isEqualTo(0); + assertThat(totalBufferedDuration[0]).isEqualTo(0); + + assertThat(windowIndex[1]).isEqualTo(windowIndex[0]); + assertThat(positionMs[1]).isEqualTo(positionMs[0]); + assertThat(bufferedPositions[1]).isEqualTo(bufferedPositions[0]); + assertThat(totalBufferedDuration[1]).isEqualTo(totalBufferedDuration[0]); + } + + private void runPositionMaskingCapturingActionSchedule( + PlayerRunnable actionRunnable, + int pauseWindowIndex, + int[] windowIndex, + long[] positionMs, + long[] bufferedPosition, + long[] totalBufferedDuration, + MediaSource... mediaSources) + throws Exception { + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .playUntilPosition(pauseWindowIndex, /* positionMs= */ 8000) + .executeRunnable(actionRunnable) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndex[0] = player.getCurrentWindowIndex(); + positionMs[0] = player.getCurrentPosition(); + bufferedPosition[0] = player.getBufferedPosition(); + totalBufferedDuration[0] = player.getTotalBufferedDuration(); + } + }) + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndex[1] = player.getCurrentWindowIndex(); + positionMs[1] = player.getCurrentPosition(); + bufferedPosition[1] = player.getBufferedPosition(); + totalBufferedDuration[1] = player.getTotalBufferedDuration(); + } + }) + .stop() + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSources) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + } + + private static FakeMediaSource createPartiallyBufferedMediaSource(long maxBufferedPositionMs) { + int windowOffsetInFirstPeriodUs = 1_000_000; + FakeTimeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ 10_000_000L, + /* defaultPositionUs= */ 0, + windowOffsetInFirstPeriodUs, + AdPlaybackState.NONE)); + return new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher, + @Nullable TransferListener transferListener) { + FakeMediaPeriod fakeMediaPeriod = + new FakeMediaPeriod( + trackGroupArray, + FakeMediaPeriod.TrackDataFactory.singleSampleWithTimeUs(/* sampleTimeUs= */ 0), + mediaSourceEventDispatcher, + drmSessionManager, + drmEventDispatcher, + /* deferOnPrepared= */ false); + fakeMediaPeriod.setBufferedPositionUs( + windowOffsetInFirstPeriodUs + C.msToUs(maxBufferedPositionMs)); + return fakeMediaPeriod; + } + }; + } + + @Test + public void addMediaSource_whilePlayingAd_correctMasking() throws Exception { + long contentDurationMs = 10_000; + long adDurationMs = 100_000; + AdPlaybackState adPlaybackState = new AdPlaybackState(/* adGroupTimesUs...= */ 0); + adPlaybackState = adPlaybackState.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1); + adPlaybackState = + adPlaybackState.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.EMPTY); + long[][] durationsUs = new long[1][]; + durationsUs[0] = new long[] {C.msToUs(adDurationMs)}; + adPlaybackState = adPlaybackState.withAdDurationsUs(durationsUs); + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(contentDurationMs), + adPlaybackState)); + FakeMediaSource adsMediaSource = new FakeMediaSource(adTimeline); + int[] windowIndex = new int[] {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + long[] positionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.INDEX_UNSET}; + long[] bufferedPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.INDEX_UNSET}; + long[] totalBufferedDurationMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.INDEX_UNSET}; + boolean[] isPlayingAd = new boolean[3]; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .pause() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addMediaSource( + /* index= */ 1, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); + windowIndex[0] = player.getCurrentWindowIndex(); + isPlayingAd[0] = player.isPlayingAd(); + positionMs[0] = player.getCurrentPosition(); + bufferedPositionMs[0] = player.getBufferedPosition(); + totalBufferedDurationMs[0] = player.getTotalBufferedDuration(); + } + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndex[1] = player.getCurrentWindowIndex(); + isPlayingAd[1] = player.isPlayingAd(); + positionMs[1] = player.getCurrentPosition(); + bufferedPositionMs[1] = player.getBufferedPosition(); + totalBufferedDurationMs[1] = player.getTotalBufferedDuration(); + } + }) + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 8000) + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); + windowIndex[2] = player.getCurrentWindowIndex(); + isPlayingAd[2] = player.isPlayingAd(); + positionMs[2] = player.getCurrentPosition(); + bufferedPositionMs[2] = player.getBufferedPosition(); + totalBufferedDurationMs[2] = player.getTotalBufferedDuration(); + } + }) + .play() + .build(); + + new ExoPlayerTestRunner.Builder(context) + .setMediaSources( + adsMediaSource, new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(isPlayingAd[0]).isTrue(); + assertThat(positionMs[0]).isAtMost(adDurationMs); + assertThat(bufferedPositionMs[0]).isEqualTo(adDurationMs); + assertThat(totalBufferedDurationMs[0]).isEqualTo(adDurationMs - positionMs[0]); + + assertThat(windowIndex[1]).isEqualTo(0); + assertThat(isPlayingAd[1]).isTrue(); + assertThat(positionMs[1]).isAtMost(adDurationMs); + assertThat(bufferedPositionMs[1]).isEqualTo(adDurationMs); + assertThat(totalBufferedDurationMs[1]).isEqualTo(adDurationMs - positionMs[1]); + + assertThat(windowIndex[2]).isEqualTo(0); + assertThat(isPlayingAd[2]).isFalse(); + assertThat(positionMs[2]).isGreaterThan(8000); + assertThat(bufferedPositionMs[2]).isEqualTo(contentDurationMs); + assertThat(totalBufferedDurationMs[2]).isEqualTo(contentDurationMs - positionMs[2]); + } + + @Test + public void seekTo_whilePlayingAd_correctMasking() throws Exception { + long contentDurationMs = 10_000; + long adDurationMs = 4_000; + AdPlaybackState adPlaybackState = new AdPlaybackState(/* adGroupTimesUs...= */ 0); + adPlaybackState = adPlaybackState.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1); + adPlaybackState = + adPlaybackState.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.EMPTY); + long[][] durationsUs = new long[1][]; + durationsUs[0] = new long[] {C.msToUs(adDurationMs)}; + adPlaybackState = adPlaybackState.withAdDurationsUs(durationsUs); + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(contentDurationMs), + adPlaybackState)); + FakeMediaSource adsMediaSource = new FakeMediaSource(adTimeline); + int[] windowIndex = new int[] {C.INDEX_UNSET, C.INDEX_UNSET}; + long[] positionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET}; + long[] bufferedPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET}; + long[] totalBufferedDurationMs = new long[] {C.TIME_UNSET, C.TIME_UNSET}; + boolean[] isPlayingAd = new boolean[2]; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .waitForPlaybackState(Player.STATE_READY) + .waitForIsLoading(true) + .waitForIsLoading(false) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 8000); + windowIndex[0] = player.getCurrentWindowIndex(); + isPlayingAd[0] = player.isPlayingAd(); + positionMs[0] = player.getCurrentPosition(); + bufferedPositionMs[0] = player.getBufferedPosition(); + totalBufferedDurationMs[0] = player.getTotalBufferedDuration(); + } + }) + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndex[1] = player.getCurrentWindowIndex(); + isPlayingAd[1] = player.isPlayingAd(); + positionMs[1] = player.getCurrentPosition(); + bufferedPositionMs[1] = player.getBufferedPosition(); + totalBufferedDurationMs[1] = player.getTotalBufferedDuration(); + } + }) + .stop() + .build(); + + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(adsMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(isPlayingAd[0]).isTrue(); + assertThat(positionMs[0]).isEqualTo(0); + assertThat(bufferedPositionMs[0]).isEqualTo(adDurationMs); + assertThat(totalBufferedDurationMs[0]).isEqualTo(adDurationMs); + + assertThat(windowIndex[1]).isEqualTo(0); + assertThat(isPlayingAd[1]).isTrue(); + assertThat(positionMs[1]).isEqualTo(0); + assertThat(bufferedPositionMs[1]).isEqualTo(adDurationMs); + assertThat(totalBufferedDurationMs[1]).isEqualTo(adDurationMs); } @Test @@ -4535,6 +5477,80 @@ public final class ExoPlayerTest { assertThat(positionAfterSetPlayWhenReady.get()).isAtLeast(5000); } + @Test + public void setPlayWhenReady_correctPositionMasking() throws Exception { + long[] currentPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + long[] bufferedPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .playUntilPosition(0, 5000) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentPositionMs[0] = player.getCurrentPosition(); + bufferedPositionMs[0] = player.getBufferedPosition(); + player.setPlayWhenReady(true); + currentPositionMs[1] = player.getCurrentPosition(); + bufferedPositionMs[1] = player.getBufferedPosition(); + player.setPlayWhenReady(false); + currentPositionMs[2] = player.getCurrentPosition(); + bufferedPositionMs[2] = player.getBufferedPosition(); + } + }) + .play() + .build(); + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(currentPositionMs[0]).isAtLeast(5000); + assertThat(currentPositionMs[1]).isEqualTo(currentPositionMs[0]); + assertThat(currentPositionMs[2]).isEqualTo(currentPositionMs[0]); + assertThat(bufferedPositionMs[0]).isGreaterThan(currentPositionMs[0]); + assertThat(bufferedPositionMs[1]).isEqualTo(bufferedPositionMs[0]); + assertThat(bufferedPositionMs[2]).isEqualTo(bufferedPositionMs[0]); + } + + @Test + public void setShuffleMode_correctPositionMasking() throws Exception { + long[] currentPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + long[] bufferedPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .playUntilPosition(0, 5000) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentPositionMs[0] = player.getCurrentPosition(); + bufferedPositionMs[0] = player.getBufferedPosition(); + player.setShuffleModeEnabled(true); + currentPositionMs[1] = player.getCurrentPosition(); + bufferedPositionMs[1] = player.getBufferedPosition(); + player.setShuffleModeEnabled(false); + currentPositionMs[2] = player.getCurrentPosition(); + bufferedPositionMs[2] = player.getBufferedPosition(); + } + }) + .play() + .build(); + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(currentPositionMs[0]).isAtLeast(5000); + assertThat(currentPositionMs[1]).isEqualTo(currentPositionMs[0]); + assertThat(currentPositionMs[2]).isEqualTo(currentPositionMs[0]); + assertThat(bufferedPositionMs[0]).isGreaterThan(currentPositionMs[0]); + assertThat(bufferedPositionMs[1]).isEqualTo(bufferedPositionMs[0]); + assertThat(bufferedPositionMs[2]).isEqualTo(bufferedPositionMs[0]); + } + @Test public void setShuffleOrder_keepsCurrentPosition() throws Exception { AtomicLong positionAfterSetShuffleOrder = new AtomicLong(C.TIME_UNSET); @@ -4866,13 +5882,14 @@ public final class ExoPlayerTest { } @Test - public void setMediaSources_whenEmpty_validInitialSeek_correctMaskingWindowIndex() - throws Exception { + public void setMediaSources_whenEmpty_validInitialSeek_correctMasking() throws Exception { Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2); MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) // Wait for initial seek to be fully handled by internal player. @@ -4883,9 +5900,13 @@ public final class ExoPlayerTest { @Override public void run(SimpleExoPlayer player) { currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPositions[0] = player.getCurrentPosition(); + bufferedPositions[0] = player.getBufferedPosition(); // Increase current window index. player.addMediaSource(/* index= */ 0, secondMediaSource); currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPositions[1] = player.getCurrentPosition(); + bufferedPositions[1] = player.getBufferedPosition(); } }) .prepare() @@ -4895,11 +5916,13 @@ public final class ExoPlayerTest { @Override public void run(SimpleExoPlayer player) { currentWindowIndices[2] = player.getCurrentWindowIndex(); + currentPositions[2] = player.getCurrentPosition(); + bufferedPositions[2] = player.getBufferedPosition(); } }) .build(); new ExoPlayerTestRunner.Builder(context) - .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .initialSeek(/* windowIndex= */ 1, 2000) .setMediaSources(firstMediaSource) .setActionSchedule(actionSchedule) .build() @@ -4907,16 +5930,19 @@ public final class ExoPlayerTest { .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); assertArrayEquals(new int[] {1, 2, 2}, currentWindowIndices); + assertArrayEquals(new long[] {2000, 2000, 2000}, currentPositions); + assertArrayEquals(new long[] {2000, 2000, 2000}, bufferedPositions); } @Test - public void setMediaSources_whenEmpty_invalidInitialSeek_correctMaskingWindowIndex() - throws Exception { + public void setMediaSources_whenEmpty_invalidInitialSeek_correctMasking() throws Exception { Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) // Wait for initial seek to be fully handled by internal player. @@ -4927,9 +5953,13 @@ public final class ExoPlayerTest { @Override public void run(SimpleExoPlayer player) { currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPositions[0] = player.getCurrentPosition(); + bufferedPositions[0] = player.getBufferedPosition(); // Increase current window index. player.addMediaSource(/* index= */ 0, secondMediaSource); currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPositions[1] = player.getCurrentPosition(); + bufferedPositions[1] = player.getBufferedPosition(); } }) .prepare() @@ -4939,12 +5969,14 @@ public final class ExoPlayerTest { @Override public void run(SimpleExoPlayer player) { currentWindowIndices[2] = player.getCurrentWindowIndex(); + currentPositions[2] = player.getCurrentPosition(); + bufferedPositions[2] = player.getBufferedPosition(); } }) .waitForPlaybackState(Player.STATE_ENDED) .build(); new ExoPlayerTestRunner.Builder(context) - .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .initialSeek(/* windowIndex= */ 1, 2000) .setMediaSources(firstMediaSource) .setActionSchedule(actionSchedule) .build() @@ -4952,6 +5984,8 @@ public final class ExoPlayerTest { .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); assertArrayEquals(new int[] {0, 1, 1}, currentWindowIndices); + assertArrayEquals(new long[] {0, 0, 0}, currentPositions); + assertArrayEquals(new long[] {0, 0, 0}, bufferedPositions); } @Test @@ -5542,10 +6576,47 @@ public final class ExoPlayerTest { } @Test - public void addMediaSources_skipSettingMediaItems_validInitialSeek_correctMaskingWindowIndex() + public void addMediaSources_whenEmptyInitialSeek_correctPeriodMasking() throws Exception { + final long[] positions = new long[2]; + Arrays.fill(positions, C.TIME_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Wait for initial seek to be fully handled by internal player. + .waitForPositionDiscontinuity() + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addMediaSource( + /* index= */ 0, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); + positions[0] = player.getCurrentPosition(); + positions[1] = player.getBufferedPosition(); + } + }) + .prepare() + .build(); + new ExoPlayerTestRunner.Builder(context) + .skipSettingMediaSources() + .initialSeek(/* windowIndex= */ 0, /* positionMs= */ 2000) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new long[] {2000, 2000}, positions); + } + + @Test + public void addMediaSources_skipSettingMediaItems_validInitialSeek_correctMasking() throws Exception { final int[] currentWindowIndices = new int[5]; Arrays.fill(currentWindowIndices, C.INDEX_UNSET); + final long[] currentPositions = new long[3]; + Arrays.fill(currentPositions, C.TIME_UNSET); + final long[] bufferedPositions = new long[3]; + Arrays.fill(bufferedPositions, C.TIME_UNSET); ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) // Wait for initial seek to be fully handled by internal player. @@ -5556,6 +6627,9 @@ public final class ExoPlayerTest { @Override public void run(SimpleExoPlayer player) { currentWindowIndices[0] = player.getCurrentWindowIndex(); + // If the timeline is empty masking variables are used. + currentPositions[0] = player.getCurrentPosition(); + bufferedPositions[0] = player.getBufferedPosition(); player.addMediaSource(/* index= */ 0, new ConcatenatingMediaSource()); currentWindowIndices[1] = player.getCurrentWindowIndex(); player.addMediaSource( @@ -5566,26 +6640,39 @@ public final class ExoPlayerTest { /* index= */ 0, new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); currentWindowIndices[3] = player.getCurrentWindowIndex(); + // With a non-empty timeline, we mask the periodId in the playback info. + currentPositions[1] = player.getCurrentPosition(); + bufferedPositions[1] = player.getBufferedPosition(); } }) .prepare() + .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { currentWindowIndices[4] = player.getCurrentWindowIndex(); + // Finally original playbackInfo coming from EPII is used. + currentPositions[2] = player.getCurrentPosition(); + bufferedPositions[2] = player.getBufferedPosition(); } }) .build(); new ExoPlayerTestRunner.Builder(context) .skipSettingMediaSources() - .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .initialSeek(/* windowIndex= */ 1, 2000) .setActionSchedule(actionSchedule) .build() .start(/* doPrepare= */ false) .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); assertArrayEquals(new int[] {1, 1, 1, 2, 2}, currentWindowIndices); + assertThat(currentPositions[0]).isEqualTo(2000); + assertThat(currentPositions[1]).isEqualTo(2000); + assertThat(currentPositions[2]).isAtLeast(2000); + assertThat(bufferedPositions[0]).isEqualTo(2000); + assertThat(bufferedPositions[1]).isEqualTo(2000); + assertThat(bufferedPositions[2]).isAtLeast(2000); } @Test @@ -5784,13 +6871,14 @@ public final class ExoPlayerTest { } @Test - public void removeMediaItems_currentItemRemoved_correctMaskingWindowIndex() throws Exception { + public void removeMediaItems_currentItemRemoved_correctMasking() throws Exception { Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET}; + final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET}; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_BUFFERING) @@ -5801,9 +6889,11 @@ public final class ExoPlayerTest { // Remove the current item. currentWindowIndices[0] = player.getCurrentWindowIndex(); currentPositions[0] = player.getCurrentPosition(); + bufferedPositions[0] = player.getBufferedPosition(); player.removeMediaItem(/* index= */ 1); currentWindowIndices[1] = player.getCurrentWindowIndex(); currentPositions[1] = player.getCurrentPosition(); + bufferedPositions[1] = player.getBufferedPosition(); } }) .build(); @@ -5817,7 +6907,9 @@ public final class ExoPlayerTest { .blockUntilEnded(TIMEOUT_MS); assertArrayEquals(new int[] {1, 1}, currentWindowIndices); assertThat(currentPositions[0]).isAtLeast(5000L); + assertThat(bufferedPositions[0]).isAtLeast(5000L); assertThat(currentPositions[1]).isEqualTo(0); + assertThat(bufferedPositions[1]).isAtLeast(0); } @Test @@ -5834,6 +6926,10 @@ public final class ExoPlayerTest { Arrays.fill(currentWindowIndices, C.INDEX_UNSET); final int[] maskingPlaybackStates = new int[4]; Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + final long[] currentPositions = new long[3]; + Arrays.fill(currentPositions, C.TIME_UNSET); + final long[] bufferedPositions = new long[3]; + Arrays.fill(bufferedPositions, C.TIME_UNSET); ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_READY) @@ -5843,12 +6939,16 @@ public final class ExoPlayerTest { public void run(SimpleExoPlayer player) { // Expect the current window index to be 2 after seek. currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPositions[0] = player.getCurrentPosition(); + bufferedPositions[0] = player.getBufferedPosition(); player.removeMediaItem(/* index= */ 2); // Expect the current window index to be 0 // (default position of timeline after not finding subsequent period). currentWindowIndices[1] = player.getCurrentWindowIndex(); // Transition to ENDED. maskingPlaybackStates[0] = player.getPlaybackState(); + currentPositions[1] = player.getCurrentPosition(); + bufferedPositions[1] = player.getBufferedPosition(); } }) .waitForPlaybackState(Player.STATE_ENDED) @@ -5864,6 +6964,8 @@ public final class ExoPlayerTest { currentWindowIndices[3] = player.getCurrentWindowIndex(); // Remains in ENDED. maskingPlaybackStates[1] = player.getPlaybackState(); + currentPositions[2] = player.getCurrentPosition(); + bufferedPositions[2] = player.getBufferedPosition(); } }) .waitForTimelineChanged() @@ -5932,6 +7034,12 @@ public final class ExoPlayerTest { }, // buffers after set items with seek maskingPlaybackStates); assertArrayEquals(new int[] {2, 0, 0, 1, 1, 0, 0, 0, 0}, currentWindowIndices); + assertThat(currentPositions[0]).isGreaterThan(0); + assertThat(currentPositions[1]).isEqualTo(0); + assertThat(currentPositions[2]).isEqualTo(0); + assertThat(bufferedPositions[0]).isGreaterThan(0); + assertThat(bufferedPositions[1]).isEqualTo(0); + assertThat(bufferedPositions[2]).isEqualTo(0); } @Test @@ -5969,16 +7077,24 @@ public final class ExoPlayerTest { MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; final int[] maskingPlaybackState = {C.INDEX_UNSET}; + final long[] currentPosition = {C.TIME_UNSET, C.TIME_UNSET}; + final long[] bufferedPosition = {C.TIME_UNSET, C.TIME_UNSET}; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) + .pause() .waitForPlaybackState(Player.STATE_BUFFERING) + .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 150) .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPosition[0] = player.getCurrentPosition(); + bufferedPosition[0] = player.getBufferedPosition(); player.clearMediaItems(); currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPosition[1] = player.getCurrentPosition(); + bufferedPosition[1] = player.getBufferedPosition(); maskingPlaybackState[0] = player.getPlaybackState(); } }) @@ -5992,6 +7108,11 @@ public final class ExoPlayerTest { .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); assertArrayEquals(new int[] {1, 0}, currentWindowIndices); + assertThat(currentPosition[0]).isAtLeast(150); + assertThat(currentPosition[1]).isEqualTo(0); + assertThat(bufferedPosition[0]).isAtLeast(150); + assertThat(bufferedPosition[1]).isEqualTo(0); + assertArrayEquals(new int[] {1, 0}, currentWindowIndices); assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackState); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index a33d16e4a5..b462d8617b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -421,7 +421,7 @@ public final class AnalyticsCollectorTest { assertThat(loadingEvents).hasSize(4); assertThat(loadingEvents).containsAtLeast(period0, period0).inOrder(); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) - .containsExactly(period0, period1) + .containsExactly(period0, period1, period1) .inOrder(); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( @@ -887,7 +887,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) - .containsExactly(period0Seq0, period0Seq1) + .containsExactly(period0Seq0, period0Seq1, period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly(WINDOW_0 /* manifest */, period0Seq0 /* media */, period1Seq1 /* media */) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index cc2ce99683..e5fc1d894b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -72,6 +72,7 @@ public class FakeMediaPeriod implements MediaPeriod { private boolean prepared; private long seekOffsetUs; private long discontinuityPositionUs; + private long bufferedPositionUs; /** * Constructs a FakeMediaPeriod with a single sample for each track in {@code trackGroupArray}. @@ -149,6 +150,7 @@ public class FakeMediaPeriod implements MediaPeriod { this.drmEventDispatcher = drmEventDispatcher; this.deferOnPrepared = deferOnPrepared; this.trackDataFactory = trackDataFactory; + this.bufferedPositionUs = C.TIME_END_OF_SOURCE; discontinuityPositionUs = C.TIME_UNSET; sampleStreams = new ArrayList<>(); fakePreparationLoadTaskId = LoadEventInfo.getNewId(); @@ -283,7 +285,11 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long getBufferedPositionUs() { assertThat(prepared).isTrue(); - return C.TIME_END_OF_SOURCE; + return bufferedPositionUs; + } + + public void setBufferedPositionUs(long bufferedPositionUs) { + this.bufferedPositionUs = bufferedPositionUs; } @Override @@ -293,6 +299,9 @@ public class FakeMediaPeriod implements MediaPeriod { for (SampleStream sampleStream : sampleStreams) { seekSampleStream(sampleStream, seekPositionUs); } + if (bufferedPositionUs != C.TIME_END_OF_SOURCE && seekPositionUs > bufferedPositionUs) { + bufferedPositionUs = seekPositionUs; + } return seekPositionUs; }