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
This commit is contained in:
tianyifeng 2024-08-21 05:14:20 -07:00 committed by Copybara-Service
parent 059ad62377
commit 410b26fba1
4 changed files with 209 additions and 14 deletions

View File

@ -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;

View File

@ -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) {

View File

@ -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<FakeSampleStream.FakeSampleStreamItem> 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);
}
}

View File

@ -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<PreloadException> preloadExceptionReference = new AtomicReference<>();
AtomicReference<PreloadMediaSource> 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<PreloadException> preloadExceptionReference = new AtomicReference<>();
AtomicReference<PreloadMediaSource> 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<PreloadException> 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<FakeSampleStream.FakeSampleStreamItem> 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() {