diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 43841a708c..5410c617cf 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -215,6 +215,9 @@ ad view group ([#7344](https://github.com/google/ExoPlayer/issues/7344)), ([#8339](https://github.com/google/ExoPlayer/issues/8339)). + * Fix a bug that could cause the next content position played after a + seek to snap back to the cue point of the preceding ad, rather than + the requested content position. * FFmpeg extension: * Link the FFmpeg library statically, saving 350KB in binary size on average. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f1fbad4c61..dab8daa3c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1086,7 +1086,7 @@ import java.util.concurrent.atomic.AtomicBoolean; MediaPeriodId periodId; long periodPositionUs; - long requestedContentPosition; + long requestedContentPositionUs; boolean seekPositionAdjusted; @Nullable Pair resolvedSeekPosition = @@ -1105,17 +1105,17 @@ import java.util.concurrent.atomic.AtomicBoolean; getPlaceholderFirstMediaPeriodPosition(playbackInfo.timeline); periodId = firstPeriodAndPosition.first; periodPositionUs = firstPeriodAndPosition.second; - requestedContentPosition = C.TIME_UNSET; + requestedContentPositionUs = C.TIME_UNSET; seekPositionAdjusted = !playbackInfo.timeline.isEmpty(); } else { // Update the resolved seek position to take ads into account. Object periodUid = resolvedSeekPosition.first; - long resolvedContentPosition = resolvedSeekPosition.second; - requestedContentPosition = - seekPosition.windowPositionUs == C.TIME_UNSET ? C.TIME_UNSET : resolvedContentPosition; + long resolvedContentPositionUs = resolvedSeekPosition.second; + requestedContentPositionUs = + seekPosition.windowPositionUs == C.TIME_UNSET ? C.TIME_UNSET : resolvedContentPositionUs; periodId = queue.resolveMediaPeriodIdForAds( - playbackInfo.timeline, periodUid, resolvedContentPosition); + playbackInfo.timeline, periodUid, resolvedContentPositionUs); if (periodId.isAd()) { playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); periodPositionUs = @@ -1124,7 +1124,7 @@ import java.util.concurrent.atomic.AtomicBoolean; : 0; seekPositionAdjusted = true; } else { - periodPositionUs = resolvedContentPosition; + periodPositionUs = resolvedContentPositionUs; seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; } } @@ -1175,11 +1175,11 @@ import java.util.concurrent.atomic.AtomicBoolean; /* newPeriodId= */ periodId, /* oldTimeline= */ playbackInfo.timeline, /* oldPeriodId= */ playbackInfo.periodId, - /* positionForTargetOffsetOverrideUs= */ requestedContentPosition); + /* positionForTargetOffsetOverrideUs= */ requestedContentPositionUs); } } finally { playbackInfo = - handlePositionDiscontinuity(periodId, periodPositionUs, requestedContentPosition); + handlePositionDiscontinuity(periodId, periodPositionUs, requestedContentPositionUs); if (seekPositionAdjusted) { playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); } @@ -2242,6 +2242,12 @@ import java.util.concurrent.atomic.AtomicBoolean; ? emptyTrackSelectorResult : playingPeriodHolder.getTrackSelectorResult(); staticMetadata = extractMetadataFromTrackSelectionArray(trackSelectorResult.selections); + // Ensure the media period queue requested content position matches the new playback info. + if (playingPeriodHolder != null + && playingPeriodHolder.info.requestedContentPositionUs != contentPositionUs) { + playingPeriodHolder.info = + playingPeriodHolder.info.copyWithRequestedContentPositionUs(contentPositionUs); + } } else if (!mediaPeriodId.equals(playbackInfo.periodId)) { // Reset previously kept track info if unprepared and the period changes. trackGroupArray = TrackGroupArray.EMPTY; 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 d4e66abf4e..55c3bd70ed 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 @@ -2682,6 +2682,64 @@ public final class ExoPlayerTest { assertThat(mediaSource.getCreatedMediaPeriods().get(3).adGroupIndex).isEqualTo(C.INDEX_UNSET); } + @Test + public void seekPastBufferingMidroll_playsAdAndThenContentFromSeekPosition() throws Exception { + long adGroupWindowTimeMs = 1_000; + long seekPositionMs = 95_000; + long contentDurationMs = 100_000; + AdPlaybackState adPlaybackState = + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, + /* adGroupTimesUs...= */ TimelineWindowDefinition + .DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + + C.msToUs(adGroupWindowTimeMs)); + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(contentDurationMs), + adPlaybackState)); + AtomicBoolean hasCreatedAdMediaPeriod = new AtomicBoolean(); + FakeMediaSource mediaSource = + new FakeMediaSource(timeline) { + @Override + public MediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + if (id.adGroupIndex == 0) { + hasCreatedAdMediaPeriod.set(true); + } + return super.createPeriod(id, allocator, startPositionUs); + } + }; + SimpleExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.setMediaSource(mediaSource); + // Throw on the playback thread if the player position reaches a value that is just less than + // seek position. This ensures that playback stops and the assertion on the player position + // below fails, even if a long time passes between detecting the discontinuity and asserting. + player + .createMessage( + (messageType, payload) -> { + throw new IllegalStateException(); + }) + .setPosition(seekPositionMs - 1) + .send(); + player.pause(); + player.prepare(); + + // Block until the midroll has started buffering, then seek after the midroll before playing. + runMainLooperUntil(hasCreatedAdMediaPeriod::get); + player.seekTo(seekPositionMs); + player.play(); + + // When the ad finishes, the player position should be at or after the requested seek position. + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AD_INSERTION); + assertThat(player.getCurrentPosition()).isAtLeast(seekPositionMs); + } + @Test public void repeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNumber() throws Exception {