Skip to content

Commit

Permalink
Allow ProgressiveMediaSource to optionally suppress prepare errors
Browse files Browse the repository at this point in the history
Use this for sideloaded subtitles, so preparation can still complete
despite an error from e.g. `DataSource.open`. In this case, no subtitle
tracks will be emitted.

Issue: #1722
PiperOrigin-RevId: 686888588
  • Loading branch information
icbaker authored and copybara-github committed Oct 17, 2024
1 parent b78395b commit b3290ef
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,8 @@ public MediaSource createMediaSource(MediaItem mediaItem) {
: new UnknownSubtitlesExtractor(format)
};
ProgressiveMediaSource.Factory progressiveMediaSourceFactory =
new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory);
new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
.setSuppressPrepareError(true);
if (loadErrorHandlingPolicy != null) {
progressiveMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ interface Listener {
private final Allocator allocator;
@Nullable private final String customCacheKey;
private final long continueLoadingCheckIntervalBytes;
private final boolean suppressPrepareError;
private final long singleSampleDurationUs;
private final Loader loader;
private final ProgressiveMediaExtractor progressiveMediaExtractor;
Expand Down Expand Up @@ -172,6 +173,9 @@ interface Listener {
* indexing. May be null.
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
* @param suppressPrepareError True if an error that would be thrown from {@link
* #maybeThrowPrepareError()} should instead be suppressed and allow preparation to
* {@linkplain Callback#onPrepared complete}.
* @param singleSampleDurationUs The duration of media with a single sample in microseconds.
* @param downloadExecutor An optional externally provided {@link ReleasableExecutor} for loading
* and extracting media.
Expand All @@ -190,6 +194,7 @@ public ProgressiveMediaPeriod(
Allocator allocator,
@Nullable String customCacheKey,
int continueLoadingCheckIntervalBytes,
boolean suppressPrepareError,
long singleSampleDurationUs,
@Nullable ReleasableExecutor downloadExecutor) {
this.uri = uri;
Expand All @@ -202,6 +207,7 @@ public ProgressiveMediaPeriod(
this.allocator = allocator;
this.customCacheKey = customCacheKey;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
this.suppressPrepareError = suppressPrepareError;
loader =
downloadExecutor != null
? new Loader(downloadExecutor)
Expand Down Expand Up @@ -254,7 +260,17 @@ public void prepare(Callback callback, long positionUs) {

@Override
public void maybeThrowPrepareError() throws IOException {
maybeThrowError();
try {
maybeThrowError();
} catch (IOException e) {
if (suppressPrepareError) {
Log.e(TAG, "Suppressing preparation error because suppressPrepareError=true", e);
sampleQueuesBuilt = true;
setSeekMap(new Unseekable(C.TIME_UNSET));
} else {
throw e;
}
}
if (loadingFinished && !prepared) {
throw ParserException.createForMalformedContainer(
"Loading finished before preparation is complete.", /* cause= */ null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public static final class Factory implements MediaSourceFactory {
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private int continueLoadingCheckIntervalBytes;
@Nullable private Supplier<ReleasableExecutor> downloadExecutorSupplier;
private boolean suppressPrepareError;

/**
* Creates a new factory for {@link ProgressiveMediaSource}s.
Expand Down Expand Up @@ -189,6 +190,21 @@ public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckInte
return this;
}

/**
* Allow {@link MediaPeriod} preparation to {@linkplain
* MediaPeriod.Callback#onPrepared(MediaPeriod) complete} despite an error that would have
* otherwise blocked it.
*
* <p>If set to true, an error that would normally be thrown from {@link
* MediaPeriod#maybeThrowPrepareError()} (e.g. a {@link DataSource#open} error like HTTP 404) is
* instead suppressed and preparation is completed with no tracks.
*/
@CanIgnoreReturnValue
public Factory setSuppressPrepareError(boolean suppressPrepareError) {
this.suppressPrepareError = suppressPrepareError;
return this;
}

@CanIgnoreReturnValue
@Override
public Factory setDrmSessionManagerProvider(
Expand Down Expand Up @@ -236,6 +252,7 @@ public ProgressiveMediaSource createMediaSource(MediaItem mediaItem) {
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
continueLoadingCheckIntervalBytes,
suppressPrepareError,
downloadExecutorSupplier);
}

Expand All @@ -256,7 +273,7 @@ public ProgressiveMediaSource createMediaSource(MediaItem mediaItem) {
private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
private final int continueLoadingCheckIntervalBytes;

private final boolean suppressPrepareError;
@Nullable private final Supplier<ReleasableExecutor> downloadExecutorSupplier;

private boolean timelineIsPlaceholder;
Expand All @@ -275,13 +292,15 @@ private ProgressiveMediaSource(
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
int continueLoadingCheckIntervalBytes,
boolean suppressPrepareError,
@Nullable Supplier<ReleasableExecutor> downloadExecutorSupplier) {
this.mediaItem = mediaItem;
this.dataSourceFactory = dataSourceFactory;
this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
this.drmSessionManager = drmSessionManager;
this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
this.suppressPrepareError = suppressPrepareError;
this.timelineIsPlaceholder = true;
this.timelineDurationUs = C.TIME_UNSET;
this.downloadExecutorSupplier = downloadExecutorSupplier;
Expand Down Expand Up @@ -340,6 +359,7 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star
allocator,
localConfiguration.customCacheKey,
continueLoadingCheckIntervalBytes,
suppressPrepareError,
Util.msToUs(localConfiguration.imageDurationMs),
downloadExecutorSupplier != null ? downloadExecutorSupplier.get() : null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
/* customCacheKey= */ null,
ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES,
/* suppressPrepareError= */ false,
imageDurationUs,
executor != null ? ReleasableExecutor.from(executor, executorReleased) : null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,24 @@
*/
package androidx.media3.exoplayer.source;

import static androidx.media3.test.utils.robolectric.RobolectricUtil.DEFAULT_TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.test.utils.MediaSourceTestRunner;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -109,6 +117,60 @@ public void updateMediaItem_createsTimelineWithUpdatedItem() throws Exception {
.isEqualTo(updatedMediaItem);
}

@Test
public void maybeThrowPrepareError_withSuppressPrepareError_doesNotThrow() throws Exception {
ProgressiveMediaSource mediaSource =
new ProgressiveMediaSource.Factory(
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()))
// Disable retries, so the first error is marked fatal.
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 0))
.setSuppressPrepareError(true)
.createMediaSource(MediaItem.fromUri("file:///not/found"));
MediaSourceTestRunner mediaSourceTestRunner = new MediaSourceTestRunner(mediaSource);

Timeline timeline = mediaSourceTestRunner.prepareSource();
CountDownLatch loadErrorReported = new CountDownLatch(1);
mediaSourceTestRunner.runOnPlaybackThread(
() ->
mediaSource.addEventListener(
Util.createHandlerForCurrentLooper(),
new MediaSourceEventListener() {
@Override
public void onLoadError(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
loadErrorReported.countDown();
}
}));
MediaPeriod mediaPeriod =
mediaSourceTestRunner.createPeriod(
new MediaSource.MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
CountDownLatch preparedLatch =
mediaSourceTestRunner.preparePeriod(mediaPeriod, /* positionUs= */ 0);
assertThat(loadErrorReported.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();
AtomicReference<Throwable> prepareError = new AtomicReference<>();
mediaSourceTestRunner.runOnPlaybackThread(
() -> {
try {
mediaPeriod.maybeThrowPrepareError();
} catch (Throwable e) {
prepareError.set(e);
}
});
assertThat(prepareError.get()).isNull();
assertThat(preparedLatch.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();

mediaSourceTestRunner.releasePeriod(mediaPeriod);
mediaSourceTestRunner.releaseSource();
mediaSourceTestRunner.release();
}

private static MediaSource buildMediaSource(MediaItem mediaItem) {
return new ProgressiveMediaSource.Factory(
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()))
Expand Down

0 comments on commit b3290ef

Please sign in to comment.