diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fe272cfd56..81c23a87c7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -97,6 +97,9 @@ * Add support for [IMA Dynamic Ad Insertion (DAI)](https://support.google.com/admanager/answer/6147120) ([#8213](https://github.com/google/ExoPlayer/issues/8213)). + * Fix issue where an ad group that failed to load caused an immediate + playback reset + ([#9929](https://github.com/google/ExoPlayer/issues/9929)). * DASH: * Support the `forced-subtitle` track role ([#9727](https://github.com/google/ExoPlayer/issues/9727)). 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 ab16fac891..33ebd69f73 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 @@ -1927,8 +1927,6 @@ import java.util.concurrent.TimeoutException; long oldPositionUs; long oldContentPositionUs; if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) { - oldPositionUs = oldPeriod.positionInWindowUs + oldPeriod.durationUs; - oldContentPositionUs = oldPositionUs; if (oldPlaybackInfo.periodId.isAd()) { // The old position is the end of the previous ad. oldPositionUs = @@ -1936,12 +1934,15 @@ import java.util.concurrent.TimeoutException; oldPlaybackInfo.periodId.adGroupIndex, oldPlaybackInfo.periodId.adIndexInAdGroup); // The ad cue point is stored in the old requested content position. oldContentPositionUs = getRequestedContentPositionUs(oldPlaybackInfo); - } else if (oldPlaybackInfo.periodId.nextAdGroupIndex != C.INDEX_UNSET - && playbackInfo.periodId.isAd()) { - // If it's a transition from content to an ad in the same window, the old position is the - // ad cue point that is the same as current content position. + } else if (oldPlaybackInfo.periodId.nextAdGroupIndex != C.INDEX_UNSET) { + // The old position is the end of a clipped content before an ad group. Use the exact ad + // cue point as the transition position. oldPositionUs = getRequestedContentPositionUs(playbackInfo); oldContentPositionUs = oldPositionUs; + } else { + // The old position is the end of a Timeline period. Use the exact duration. + oldPositionUs = oldPeriod.positionInWindowUs + oldPeriod.durationUs; + oldContentPositionUs = oldPositionUs; } } else if (oldPlaybackInfo.periodId.isAd()) { oldPositionUs = oldPlaybackInfo.positionUs; 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 c7fc668676..ea6e7674c9 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 @@ -2664,7 +2664,7 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean earliestCuePointIsUnchangedOrLater = periodIdWithAds.nextAdGroupIndex == C.INDEX_UNSET || (oldPeriodId.nextAdGroupIndex != C.INDEX_UNSET - && periodIdWithAds.adGroupIndex >= oldPeriodId.nextAdGroupIndex); + && periodIdWithAds.nextAdGroupIndex >= oldPeriodId.nextAdGroupIndex); // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and // the only change is that MediaPeriodId.nextAdGroupIndex increased. This postpones a potential // discontinuity until we reach the former next ad group position. 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 f0ea43b622..faa43fde8b 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 @@ -720,7 +720,7 @@ public final class ExoPlayerTest { } @Test - public void adGroupWithLoadErrorIsSkipped() throws Exception { + public void adGroupWithLoadError_noFurtherAdGroup_isSkipped() throws Exception { AdPlaybackState initialAdPlaybackState = FakeTimeline.createAdPlaybackState( /* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ @@ -735,11 +735,12 @@ public final class ExoPlayerTest { /* isDynamic= */ false, /* isLive= */ false, /* isPlaceholder= */ false, - /* durationUs= */ C.MICROS_PER_SECOND, + /* durationUs= */ 10 * C.MICROS_PER_SECOND, /* defaultPositionUs= */ 0, TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, initialAdPlaybackState)); - AdPlaybackState errorAdPlaybackState = initialAdPlaybackState.withAdLoadError(0, 0); + AdPlaybackState errorAdPlaybackState = + initialAdPlaybackState.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); final Timeline adErrorTimeline = new FakeTimeline( new TimelineWindowDefinition( @@ -749,30 +750,166 @@ public final class ExoPlayerTest { /* isDynamic= */ false, /* isLive= */ false, /* isPlaceholder= */ false, - /* durationUs= */ C.MICROS_PER_SECOND, + /* durationUs= */ 10 * C.MICROS_PER_SECOND, /* defaultPositionUs= */ 0, TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, errorAdPlaybackState)); final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - .waitForPlaybackState(Player.STATE_READY) - .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline)) - .waitForTimelineChanged( - adErrorTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .play() - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context) - .setMediaSources(fakeMediaSource) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + Player.Listener mockListener = mock(Player.Listener.class); + player.addListener(mockListener); + + player.setMediaSource(fakeMediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + fakeMediaSource.setNewSourceInfo(adErrorTimeline); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); + Timeline.Window window = + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, new Timeline.Window()); + Timeline.Period period = + player + .getCurrentTimeline() + .getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true); + player.release(); + // There is still one discontinuity from content to content for the failed ad insertion. - testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + PositionInfo positionInfo = + new PositionInfo( + window.uid, + /* mediaItemIndex= */ 0, + window.mediaItem, + period.uid, + /* periodIndex= */ 0, + /* positionMs= */ 5_000, + /* contentPositionMs= */ 5_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + verify(mockListener) + .onPositionDiscontinuity( + positionInfo, positionInfo, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + } + + @Test + public void adGroupWithLoadError_withFurtherAdGroup_isSkipped() throws Exception { + AdPlaybackState initialAdPlaybackState = + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + + 5 * C.MICROS_PER_SECOND, + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + + 8 * C.MICROS_PER_SECOND); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ 10 * C.MICROS_PER_SECOND, + /* defaultPositionUs= */ 0, + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + initialAdPlaybackState)); + AdPlaybackState errorAdPlaybackState = + initialAdPlaybackState.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + final Timeline adErrorTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ 10 * C.MICROS_PER_SECOND, + /* defaultPositionUs= */ 0, + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + errorAdPlaybackState)); + final FakeMediaSource fakeMediaSource = + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + Player.Listener mockListener = mock(Player.Listener.class); + player.addListener(mockListener); + + player.setMediaSource(fakeMediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + fakeMediaSource.setNewSourceInfo(adErrorTimeline); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); + Timeline.Window window = + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, new Timeline.Window()); + Timeline.Period period = + player + .getCurrentTimeline() + .getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true); + player.release(); + + // There is still one discontinuity from content to content for the failed ad insertion and the + // normal ad transition for the successful ad insertion. + PositionInfo positionInfoFailedAd = + new PositionInfo( + window.uid, + /* mediaItemIndex= */ 0, + window.mediaItem, + period.uid, + /* periodIndex= */ 0, + /* positionMs= */ 5_000, + /* contentPositionMs= */ 5_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + verify(mockListener) + .onPositionDiscontinuity( + positionInfoFailedAd, + positionInfoFailedAd, + Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + PositionInfo positionInfoContentAtSuccessfulAd = + new PositionInfo( + window.uid, + /* mediaItemIndex= */ 0, + window.mediaItem, + period.uid, + /* periodIndex= */ 0, + /* positionMs= */ 8_000, + /* contentPositionMs= */ 8_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + PositionInfo positionInfoSuccessfulAdStart = + new PositionInfo( + window.uid, + /* mediaItemIndex= */ 0, + window.mediaItem, + period.uid, + /* periodIndex= */ 0, + /* positionMs= */ 0, + /* contentPositionMs= */ 8_000, + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0); + PositionInfo positionInfoSuccessfulAdEnd = + new PositionInfo( + window.uid, + /* mediaItemIndex= */ 0, + window.mediaItem, + period.uid, + /* periodIndex= */ 0, + /* positionMs= */ Util.usToMs( + period.getAdDurationUs(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0)), + /* contentPositionMs= */ 8_000, + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0); + verify(mockListener) + .onPositionDiscontinuity( + positionInfoContentAtSuccessfulAd, + positionInfoSuccessfulAdStart, + Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + verify(mockListener) + .onPositionDiscontinuity( + positionInfoSuccessfulAdEnd, + positionInfoContentAtSuccessfulAd, + Player.DISCONTINUITY_REASON_AUTO_TRANSITION); } @Test