diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2ed1e79db3..08d9fb9e7f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -252,6 +252,9 @@ skipped but only after the preload timeout rather than instantly ([#8428](https://github.com/google/ExoPlayer/issues/8428)), ([#7832](https://github.com/google/ExoPlayer/issues/7832)). + * Fix a regression that caused a short ad followed by another ad to be + skipped due to playback being stuck buffering waiting for the second ad + to load ([#8492](https://github.com/google/ExoPlayer/issues/8492)). * 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 d925e99055..5ce390404c 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 @@ -1729,8 +1729,13 @@ import java.util.concurrent.atomic.AtomicBoolean; ? livePlaybackSpeedControl.getTargetLiveOffsetUs() : C.TIME_UNSET; MediaPeriodHolder loadingHolder = queue.getLoadingPeriod(); - boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal; - return bufferedToEnd + boolean isBufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal; + // Ad loader implementations may only load ad media once playback has nearly reached the ad, but + // it is possible for playback to be stuck buffering waiting for this. Therefore, we start + // playback regardless of buffered duration if we are waiting for an ad media period to prepare. + boolean isAdPendingPreparation = loadingHolder.info.id.isAd() && !loadingHolder.prepared; + return isBufferedToEnd + || isAdPendingPreparation || loadControl.shouldStartPlayback( getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, 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 55c3bd70ed..92eb6ffd03 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 @@ -4816,6 +4816,53 @@ public final class ExoPlayerTest { .blockUntilEnded(TIMEOUT_MS); } + @Test + public void shortAdFollowedByUnpreparedAd_playbackDoesNotGetStuck() throws Exception { + AdPlaybackState adPlaybackState = + FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 2, /* adGroupTimesUs...= */ 0); + long shortAdDurationMs = 1_000; + adPlaybackState = + adPlaybackState.withAdDurationsUs(new long[][] {{shortAdDurationMs, shortAdDurationMs}}); + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000), + adPlaybackState)); + // Simulate the second ad not being prepared. + FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) { + @Override + protected MediaPeriod createMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher, + @Nullable TransferListener transferListener) { + return new FakeMediaPeriod( + trackGroupArray, + allocator, + FakeMediaPeriod.TrackDataFactory.singleSampleWithTimeUs(0), + mediaSourceEventDispatcher, + drmSessionManager, + drmEventDispatcher, + /* deferOnPrepared= */ id.adIndexInAdGroup == 1); + } + }; + SimpleExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.setMediaSource(mediaSource); + player.prepare(); + player.play(); + + // The player is not stuck in the buffering state. + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY); + } + @Test public void moveMediaItem() throws Exception { TimelineWindowDefinition firstWindowDefinition =