From 410b26fba13d55563c49fee14f13ee62f0eabdbd Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Wed, 21 Aug 2024 05:14:20 -0700 Subject: [PATCH] Detect SampleStream error after PreloadMediaPeriod has prepared Per the javadoc, the method `MediaPeriod.maybeThrowPrepareError` is only allowed to be called before the period has completed preparation. For later errors in loading the streams, `SampleStream.maybeThrowError` will be called instead. PiperOrigin-RevId: 665831430 --- .../source/preload/PreloadMediaPeriod.java | 17 ++- .../source/preload/PreloadMediaSource.java | 7 +- .../preload/PreloadMediaPeriodTest.java | 71 ++++++++++ .../preload/PreloadMediaSourceTest.java | 128 ++++++++++++++++-- 4 files changed, 209 insertions(+), 14 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriod.java index bd89e81d48..cafb51a261 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriod.java @@ -35,8 +35,8 @@ import java.util.Objects; public final MediaPeriod mediaPeriod; + public boolean prepared; private boolean prepareInternalCalled; - private boolean prepared; @Nullable private Callback callback; @Nullable private PreloadTrackSelectionHolder preloadTrackSelectionHolder; @@ -49,7 +49,7 @@ import java.util.Objects; this.mediaPeriod = mediaPeriod; } - /* package */ void preload(Callback callback, long positionUs) { + public void preload(Callback callback, long positionUs) { this.callback = callback; if (prepared) { callback.onPrepared(PreloadMediaPeriod.this); @@ -217,7 +217,7 @@ import java.util.Objects; return true; } - /* package */ long selectTracksForPreloading( + public long selectTracksForPreloading( @NullableType ExoTrackSelection[] selections, long positionUs) { @NullableType SampleStream[] preloadedSampleStreams = new SampleStream[selections.length]; boolean[] preloadedStreamResetFlags = new boolean[selections.length]; @@ -284,6 +284,17 @@ import java.util.Objects; mediaPeriod.reevaluateBuffer(positionUs); } + public void maybeThrowStreamError() throws IOException { + checkState(prepared); + if (preloadTrackSelectionHolder != null) { + for (SampleStream stream : preloadTrackSelectionHolder.streams) { + if (stream != null) { + stream.maybeThrowError(); + } + } + } + } + private static class PreloadTrackSelectionHolder { public final @NullableType ExoTrackSelection[] selections; public final boolean[] mayRetainStreamFlags; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java index 2bc72c003c..41e0cad8be 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java @@ -424,7 +424,12 @@ public final class PreloadMediaSource extends WrappingMediaSource { try { maybeThrowSourceInfoRefreshError(); if (preloadingMediaPeriodAndKey != null) { - preloadingMediaPeriodAndKey.first.maybeThrowPrepareError(); + PreloadMediaPeriod preloadingMediaPeriod = preloadingMediaPeriodAndKey.first; + if (!preloadingMediaPeriod.prepared) { + preloadingMediaPeriod.maybeThrowPrepareError(); + } else { + preloadingMediaPeriod.maybeThrowStreamError(); + } } preloadHandler.postDelayed(this::checkForPreloadError, CHECK_FOR_PRELOAD_ERROR_INTERVAL_MS); } catch (IOException e) { diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriodTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriodTest.java index 320b0b4d47..b5fe947e1e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriodTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriodTest.java @@ -15,7 +15,9 @@ */ package androidx.media3.exoplayer.source.preload; +import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -29,6 +31,7 @@ import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.os.Looper; +import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; @@ -43,11 +46,15 @@ import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.FixedTrackSelection; +import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.test.utils.FakeMediaPeriod; +import androidx.media3.test.utils.FakeSampleStream; import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.FakeTrackSelection; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -1594,4 +1601,68 @@ public final class PreloadMediaPeriodTest { .selectTracks(eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L)); assertThat(trackSelectionStartPositionUs).isEqualTo(0L); } + + @Test + public void maybeThrowStreamError_preloadedStreamHasError_errorThrows() throws Exception { + Format videoFormat = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setAverageBitrate(800_000) + .setWidth(1280) + .setHeight(720) + .build(); + MediaSource.MediaPeriodId mediaPeriodId = + new MediaSource.MediaPeriodId(/* periodUid= */ new Object()); + FakeMediaPeriod wrappedMediaPeriod = + new FakeMediaPeriod( + new TrackGroupArray(new TrackGroup(videoFormat)), + new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE), + FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + new MediaSourceEventListener.EventDispatcher() + .withParameters(/* windowIndex= */ 0, mediaPeriodId)) { + @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(); + } + }; + } + }; + PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod); + AtomicBoolean onPreparedOfPreloadCallbackCalled = new AtomicBoolean(); + MediaPeriod.Callback preloadCallback = + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + onPreparedOfPreloadCallbackCalled.set(true); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }; + preloadMediaPeriod.preload(preloadCallback, /* positionUs= */ 0L); + runMainLooperUntil(onPreparedOfPreloadCallbackCalled::get); + ExoTrackSelection[] preloadTrackSelections = + new ExoTrackSelection[] { + new FixedTrackSelection(new TrackGroup(videoFormat), /* track= */ 0) + }; + // PreloadMediaPeriod.selectTracksForPreloading keeps the preloaded stream. + preloadMediaPeriod.selectTracksForPreloading(preloadTrackSelections, /* positionUs= */ 0L); + + assertThrows(IOException.class, preloadMediaPeriod::maybeThrowStreamError); + } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java index 6c4018ba6a..415ec2ecda 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java @@ -31,7 +31,9 @@ import android.os.Looper; import android.util.Pair; import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.Format; import androidx.media3.common.MediaItem; +import androidx.media3.common.MimeTypes; import androidx.media3.common.Timeline; import androidx.media3.common.Tracks; import androidx.media3.common.util.SystemClock; @@ -68,12 +70,15 @@ import androidx.media3.test.utils.FakeAudioRenderer; import androidx.media3.test.utils.FakeMediaPeriod; import androidx.media3.test.utils.FakeMediaSource; import androidx.media3.test.utils.FakeMediaSourceFactory; +import androidx.media3.test.utils.FakeSampleStream; import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.FakeTrackSelector; import androidx.media3.test.utils.FakeVideoRenderer; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.util.List; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -334,15 +339,12 @@ public final class PreloadMediaSourceTest { @Test public void preload_sourceInfoRefreshErrorThrows_onPreloadErrorCalled() throws TimeoutException { AtomicReference preloadExceptionReference = new AtomicReference<>(); - AtomicReference preloadMediaSourceReference = new AtomicReference<>(); IOException causeException = new IOException("Failed to refresh source info"); TestPreloadControl preloadControl = new TestPreloadControl() { @Override public void onPreloadError(PreloadException error, PreloadMediaSource mediaSource) { - super.onPreloadError(error, mediaSource); preloadExceptionReference.set(error); - preloadMediaSourceReference.set(mediaSource); } }; MediaSource.Factory mediaSourceFactory = @@ -393,9 +395,8 @@ public final class PreloadMediaSourceTest { .build()); preloadMediaSource.preload(/* startPositionUs= */ 0L); - runMainLooperUntil(() -> preloadMediaSourceReference.get() != null); + runMainLooperUntil(() -> preloadExceptionReference.get() != null); - assertThat(preloadControl.onPreloadErrorCalled).isTrue(); assertThat(preloadExceptionReference.get()).hasCauseThat().isEqualTo(causeException); assertThat(preloadControl.onSourcePreparedCalledCount).isEqualTo(0); assertThat(preloadControl.onTrackSelectedCalled).isFalse(); @@ -406,15 +407,12 @@ public final class PreloadMediaSourceTest { @Test public void preload_periodPrepareErrorThrows_onPreloadErrorCalled() throws TimeoutException { AtomicReference preloadExceptionReference = new AtomicReference<>(); - AtomicReference preloadMediaSourceReference = new AtomicReference<>(); IOException causeException = new IOException("Failed to prepare the period"); TestPreloadControl preloadControl = new TestPreloadControl() { @Override public void onPreloadError(PreloadException error, PreloadMediaSource mediaSource) { - super.onPreloadError(error, mediaSource); preloadExceptionReference.set(error); - preloadMediaSourceReference.set(mediaSource); } }; MediaSource.Factory mediaSourceFactory = @@ -481,9 +479,8 @@ public final class PreloadMediaSourceTest { .build()); preloadMediaSource.preload(/* startPositionUs= */ 0L); - runMainLooperUntil(() -> preloadMediaSourceReference.get() != null); + runMainLooperUntil(() -> preloadExceptionReference.get() != null); - assertThat(preloadControl.onPreloadErrorCalled).isTrue(); assertThat(preloadExceptionReference.get()).hasCauseThat().isEqualTo(causeException); assertThat(preloadControl.onSourcePreparedCalledCount).isGreaterThan(0); assertThat(preloadControl.onTrackSelectedCalled).isFalse(); @@ -491,6 +488,117 @@ public final class PreloadMediaSourceTest { assertThat(preloadControl.onUsedByPlayerCalled).isFalse(); } + @Test + public void preload_sampleStreamErrorThrows_onPreloadErrorCalled() throws TimeoutException { + AtomicReference preloadExceptionReference = new AtomicReference<>(); + IOException causeException = new IOException("Failed to read the data"); + TestPreloadControl preloadControl = + new TestPreloadControl() { + @Override + public void onPreloadError(PreloadException error, PreloadMediaSource mediaSource) { + preloadExceptionReference.set(error); + } + }; + MediaSource.Factory mediaSourceFactory = + new MediaSource.Factory() { + @Override + public MediaSource.Factory setDrmSessionManagerProvider( + DrmSessionManagerProvider drmSessionManagerProvider) { + return this; + } + + @Override + public MediaSource.Factory setLoadErrorHandlingPolicy( + LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + return this; + } + + @Override + public @C.ContentType int[] getSupportedTypes() { + return new int[0]; + } + + @Override + public MediaSource createMediaSource(MediaItem mediaItem) { + Format videoFormat = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setAverageBitrate(800_000) + .setWidth(1280) + .setHeight(720) + .build(); + return new FakeMediaSource(new FakeTimeline(), videoFormat) { + @Override + public 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 causeException; + } + }; + } + }; + } + }; + } + }; + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + preloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + allocator, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = + preloadMediaSourceFactory.createMediaSource( + new MediaItem.Builder() + .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) + .build()); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + runMainLooperUntil(() -> preloadExceptionReference.get() != null); + + assertThat(preloadExceptionReference.get()).hasCauseThat().isEqualTo(causeException); + assertThat(preloadControl.onSourcePreparedCalledCount).isGreaterThan(0); + assertThat(preloadControl.onTrackSelectedCalled).isTrue(); + assertThat(preloadControl.onContinueLoadingRequestedCalled).isFalse(); + assertThat(preloadControl.onUsedByPlayerCalled).isFalse(); + } + @Test public void prepareSource_beforeSourceInfoRefreshedForPreloading_onlyInvokeExternalCallerOnSourceInfoRefreshed() {