From 88b640136a8bbe6ceb046c3718a034bdd4e92fd7 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Wed, 21 Aug 2024 03:35:29 -0700 Subject: [PATCH] Allow playback regardless buffered duration when loading fails It is possible for playback to be stuck when there is failure in loading further data, while the player is required to load more due to the buffered duration being under `DefaultLoadControl.bufferForPlayback`. Therefore, we check if there is any loading error in `isLoadingPossible`, so that the player will allow the playback of the existing data rather than waiting forever for the data that can never be loaded. Issue: androidx/media#1571 PiperOrigin-RevId: 665801674 (cherry picked from commit 351593a2504c9185638637a69f8dd0caf4aa20db) --- RELEASENOTES.md | 2 + .../exoplayer/ExoPlayerImplInternal.java | 3 + .../media3/exoplayer/MediaPeriodHolder.java | 22 +++ .../media3/exoplayer/ExoPlayerTest.java | 129 ++++++++++++++++++ 4 files changed, 156 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d5b14bed721..26ac0d27dbe 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ * ExoPlayer: * Handle preload callbacks asynchronously in `PreloadMediaSource` ([#1568](https://github.com/androidx/media/issues/1568)). + * Allow playback regardless of buffered duration when loading fails + ([#1571](https://github.com/androidx/media/issues/1571)). * Transformer: * Track Selection: * Extractors: 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 bc4cf79603a..c3ee83c5efb 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 @@ private boolean isLoadingPossible() { 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 1c8c50a625e..f3b0b8f5201 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.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 @@ public void updateClipping() { } } + /** + * 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 ed2ea706c27..8e4ad953d14 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 void maybeThrowPrepareError() throws IOException { 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 void maybeThrowError() throws IOException { 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 =