diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c39f102309..512a9ad6cf 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,6 +39,8 @@ handling is enabled. This ensures the blocking call isn't done if audio focus handling is not enabled ([#1616](https://github.com/androidx/media/pull/1616)). + * Allow playback regardless of buffered duration when loading fails + ([#1571](https://github.com/androidx/media/issues/1571)). * Transformer: * Add `SurfaceAssetLoader`, which supports queueing video data to Transformer via a `Surface`. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index bc4cf79603..c3ee83c5ef 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -2633,6 +2633,9 @@ import java.util.concurrent.atomic.AtomicBoolean; if (loadingPeriodHolder == null) { return false; } + if (loadingPeriodHolder.hasLoadingError()) { + return false; + } long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { return false; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java index 1c8c50a625..f3b0b8f520 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java @@ -35,6 +35,7 @@ import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import androidx.media3.exoplayer.upstream.Allocator; +import java.io.IOException; /** Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ /* package */ final class MediaPeriodHolder { @@ -394,6 +395,27 @@ import androidx.media3.exoplayer.upstream.Allocator; } } + /** + * Returns whether the media period has encountered an error that prevents it from being prepared + * or reading data. + */ + public boolean hasLoadingError() { + try { + if (!prepared) { + mediaPeriod.maybeThrowPrepareError(); + } else { + for (SampleStream sampleStream : sampleStreams) { + if (sampleStream != null) { + sampleStream.maybeThrowError(); + } + } + } + } catch (IOException e) { + return true; + } + return false; + } + private void enableTrackSelectionsInResult() { if (!isLoadingMediaPeriod()) { return; diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index c51e45e178..2984802526 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -10794,6 +10794,62 @@ public class ExoPlayerTest { player.release(); } + @Test + public void + mediaPeriodMaybeThrowPrepareError_bufferedDurationUnderMinimumBufferForPlayback_keepPlayingUntilBufferedDataExhausts() + throws Exception { + ExoPlayer player = parameterizeTestExoPlayerBuilder(new TestExoPlayerBuilder(context)).build(); + // Define a timeline that has a short duration of 1 second for the first item, which is smaller + // than the default buffer duration for playback in DefaultLoadControl (2.5 seconds). + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 1 * C.MICROS_PER_SECOND)); + player.addMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); + player.addMediaSource( + 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, + /* singleSampleTimeUs= */ 0, + mediaSourceEventDispatcher, + DrmSessionManager.DRM_UNSUPPORTED, + drmEventDispatcher, + /* deferOnPrepared= */ true) { + @Override + public void maybeThrowPrepareError() throws IOException { + throw new IOException(); + } + }; + } + }); + + player.prepare(); + player.play(); + ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player); + + Object period1Uid = + player + .getCurrentTimeline() + .getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true) + .uid; + assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + + player.release(); + } + @Test public void sampleStreamMaybeThrowError_isNotThrownUntilPlaybackReachedFailingItem() throws Exception { @@ -10859,6 +10915,79 @@ public class ExoPlayerTest { player.release(); } + @Test + public void + sampleStreamMaybeThrowError_bufferedDurationUnderMinimumBufferForPlayback_keepPlayingUntilBufferedDataExhausts() + throws Exception { + ExoPlayer player = parameterizeTestExoPlayerBuilder(new TestExoPlayerBuilder(context)).build(); + // Define a timeline that has a short duration of 1 second for the first item, which is smaller + // than the default buffer duration for playback in DefaultLoadControl (2.5 seconds). + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 1 * C.MICROS_PER_SECOND)); + player.addMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); + player.addMediaSource( + 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, + /* trackDataFactory= */ (format, mediaPeriodId) -> ImmutableList.of(), + mediaSourceEventDispatcher, + drmSessionManager, + drmEventDispatcher, + /* deferOnPrepared= */ false) { + @Override + protected FakeSampleStream createSampleStream( + Allocator allocator, + @Nullable MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher, + Format initialFormat, + List fakeSampleStreamItems) { + return new FakeSampleStream( + allocator, + mediaSourceEventDispatcher, + drmSessionManager, + drmEventDispatcher, + initialFormat, + fakeSampleStreamItems) { + @Override + public void maybeThrowError() throws IOException { + throw new IOException(); + } + }; + } + }; + } + }); + + player.prepare(); + player.play(); + ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player); + + Object period1Uid = + player + .getCurrentTimeline() + .getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true) + .uid; + assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + + player.release(); + } + @Test public void rendererError_isReportedWithReadingMediaPeriodId() throws Exception { FakeMediaSource source0 =