From d62688cfc00ad6507ee4d9c65c56c16e9eb1b7c9 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 Jul 2020 16:11:20 +0100 Subject: [PATCH] Mask periodId and loadingPeriodId This change masks playbackInfo.periodId and playbackInfo.loadingPeriodId for operations which change these periods (set/add/remove sources and seeks). Because this masking is reflected in the playbackInfo object, player attributes can be retrieved without the maskingXyz variables in EPI. This has the advantage that the playbackInfo object always reflects the public state of the player even when operations are still pending. The maskingXyz variables in EPI are only required for the deprecated use case of an initial seek in an empty timeline. PiperOrigin-RevId: 321160092 --- .../android/exoplayer2/ExoPlayerImpl.java | 337 +++-- .../android/exoplayer2/ExoPlayerTest.java | 1309 +++++++++++++++-- .../analytics/AnalyticsCollectorTest.java | 4 +- .../exoplayer2/testutil/FakeMediaPeriod.java | 11 +- 4 files changed, 1433 insertions(+), 228 deletions(-) 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; }