mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Configure ProgressiveMediaSource
with a Format
for subtitles
This means we can complete preparation (and trigger track selection)
before opening a `DataSource`, which then means we only end up loading
the data for a selected subtitle track (instead of all tracks as
currently happens).
By making preparation trivial in this case (with no reasonable cause
of error), we can also remove the `suppressPrepareError` option added in
b3290eff10
.
This change also fixes the implementation of
`ProgressiveMediaPeriod.maybeStartDeferredRetry` to only short-circuit
return `false` if the chosen track is not audio or video **and** there
is at least one audio or video track in this period.
Issue: androidx/media#1721
PiperOrigin-RevId: 702275968
This commit is contained in:
parent
0037388660
commit
18d156fe3c
@ -40,6 +40,10 @@
|
|||||||
when provided while processing `onOutputFormatChanged`
|
when provided while processing `onOutputFormatChanged`
|
||||||
([#1371](https://github.com/androidx/media/pull/1371)).
|
([#1371](https://github.com/androidx/media/pull/1371)).
|
||||||
* Text:
|
* Text:
|
||||||
|
* Stop eagerly loading all subtitle files configured with
|
||||||
|
`MediaItem.Builder.setSubtitleConfigurations`, and instead only load one
|
||||||
|
if it is selected by track selection
|
||||||
|
([#1721](https://github.com/androidx/media/issues/1721)).
|
||||||
* Metadata:
|
* Metadata:
|
||||||
* Image:
|
* Image:
|
||||||
* DRM:
|
* DRM:
|
||||||
|
@ -525,12 +525,23 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||||||
() ->
|
() ->
|
||||||
new Extractor[] {
|
new Extractor[] {
|
||||||
subtitleParserFactory.supportsFormat(format)
|
subtitleParserFactory.supportsFormat(format)
|
||||||
? new SubtitleExtractor(subtitleParserFactory.create(format), format)
|
? new SubtitleExtractor(
|
||||||
|
subtitleParserFactory.create(format), /* format= */ null)
|
||||||
: new UnknownSubtitlesExtractor(format)
|
: new UnknownSubtitlesExtractor(format)
|
||||||
};
|
};
|
||||||
ProgressiveMediaSource.Factory progressiveMediaSourceFactory =
|
ProgressiveMediaSource.Factory progressiveMediaSourceFactory =
|
||||||
new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||||
.setSuppressPrepareError(true);
|
.enableLazyLoadingWithSingleTrack(
|
||||||
|
SubtitleExtractor.TRACK_ID,
|
||||||
|
subtitleParserFactory.supportsFormat(format)
|
||||||
|
? format
|
||||||
|
.buildUpon()
|
||||||
|
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
|
||||||
|
.setCodecs(format.sampleMimeType)
|
||||||
|
.setCueReplacementBehavior(
|
||||||
|
subtitleParserFactory.getCueReplacementBehavior(format))
|
||||||
|
.build()
|
||||||
|
: format);
|
||||||
if (loadErrorHandlingPolicy != null) {
|
if (loadErrorHandlingPolicy != null) {
|
||||||
progressiveMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
|
progressiveMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
|
||||||
}
|
}
|
||||||
@ -792,7 +803,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput output) {
|
||||||
TrackOutput trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_TEXT);
|
TrackOutput trackOutput = output.track(SubtitleExtractor.TRACK_ID, C.TRACK_TYPE_TEXT);
|
||||||
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
||||||
output.endTracks();
|
output.endTracks();
|
||||||
trackOutput.format(
|
trackOutput.format(
|
||||||
|
@ -59,6 +59,7 @@ import androidx.media3.extractor.DiscardingTrackOutput;
|
|||||||
import androidx.media3.extractor.Extractor;
|
import androidx.media3.extractor.Extractor;
|
||||||
import androidx.media3.extractor.ExtractorOutput;
|
import androidx.media3.extractor.ExtractorOutput;
|
||||||
import androidx.media3.extractor.ForwardingSeekMap;
|
import androidx.media3.extractor.ForwardingSeekMap;
|
||||||
|
import androidx.media3.extractor.IndexSeekMap;
|
||||||
import androidx.media3.extractor.PositionHolder;
|
import androidx.media3.extractor.PositionHolder;
|
||||||
import androidx.media3.extractor.SeekMap;
|
import androidx.media3.extractor.SeekMap;
|
||||||
import androidx.media3.extractor.SeekMap.SeekPoints;
|
import androidx.media3.extractor.SeekMap.SeekPoints;
|
||||||
@ -119,7 +120,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
private final Allocator allocator;
|
private final Allocator allocator;
|
||||||
@Nullable private final String customCacheKey;
|
@Nullable private final String customCacheKey;
|
||||||
private final long continueLoadingCheckIntervalBytes;
|
private final long continueLoadingCheckIntervalBytes;
|
||||||
private final boolean suppressPrepareError;
|
private final int singleTrackId;
|
||||||
|
@Nullable private final Format singleTrackFormat;
|
||||||
private final long singleSampleDurationUs;
|
private final long singleSampleDurationUs;
|
||||||
private final Loader loader;
|
private final Loader loader;
|
||||||
private final ProgressiveMediaExtractor progressiveMediaExtractor;
|
private final ProgressiveMediaExtractor progressiveMediaExtractor;
|
||||||
@ -173,9 +175,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* indexing. May be null.
|
* indexing. May be null.
|
||||||
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
|
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
|
||||||
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
||||||
* @param suppressPrepareError True if an error that would be thrown from {@link
|
* @param singleTrackId The ID of the track configured by {@code singleTrackFormat}. Ignored if
|
||||||
* #maybeThrowPrepareError()} should instead be suppressed and allow preparation to
|
* {@code singleTrackFormat} is null.
|
||||||
* {@linkplain Callback#onPrepared complete}.
|
* @param singleTrackFormat The format of the single track this period is known to emit, allowing
|
||||||
|
* preparation to complete without reading any data. Otherwise null.
|
||||||
* @param singleSampleDurationUs The duration of media with a single sample in microseconds.
|
* @param singleSampleDurationUs The duration of media with a single sample in microseconds.
|
||||||
* @param downloadExecutor An optional externally provided {@link ReleasableExecutor} for loading
|
* @param downloadExecutor An optional externally provided {@link ReleasableExecutor} for loading
|
||||||
* and extracting media.
|
* and extracting media.
|
||||||
@ -194,7 +197,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
Allocator allocator,
|
Allocator allocator,
|
||||||
@Nullable String customCacheKey,
|
@Nullable String customCacheKey,
|
||||||
int continueLoadingCheckIntervalBytes,
|
int continueLoadingCheckIntervalBytes,
|
||||||
boolean suppressPrepareError,
|
int singleTrackId,
|
||||||
|
@Nullable Format singleTrackFormat,
|
||||||
long singleSampleDurationUs,
|
long singleSampleDurationUs,
|
||||||
@Nullable ReleasableExecutor downloadExecutor) {
|
@Nullable ReleasableExecutor downloadExecutor) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
@ -207,7 +211,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
this.allocator = allocator;
|
this.allocator = allocator;
|
||||||
this.customCacheKey = customCacheKey;
|
this.customCacheKey = customCacheKey;
|
||||||
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||||
this.suppressPrepareError = suppressPrepareError;
|
this.singleTrackId = singleTrackId;
|
||||||
|
this.singleTrackFormat = singleTrackFormat;
|
||||||
loader =
|
loader =
|
||||||
downloadExecutor != null
|
downloadExecutor != null
|
||||||
? new Loader(downloadExecutor)
|
? new Loader(downloadExecutor)
|
||||||
@ -254,23 +259,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
@Override
|
@Override
|
||||||
public void prepare(Callback callback, long positionUs) {
|
public void prepare(Callback callback, long positionUs) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
loadCondition.open();
|
if (singleTrackFormat != null) {
|
||||||
startLoading();
|
// track() and endTracks() are meant to be called on the loading thread, which doesn't exist
|
||||||
|
// yet (we're on the playback thread here). Starting the loading thread will provide a memory
|
||||||
|
// barrier to ensure any changes done here are visible on the loading thread after it starts.
|
||||||
|
TrackOutput track = track(singleTrackId, C.TRACK_TYPE_TEXT);
|
||||||
|
track.format(singleTrackFormat);
|
||||||
|
setSeekMap(
|
||||||
|
new IndexSeekMap(
|
||||||
|
/* positions= */ new long[] {0},
|
||||||
|
/* timesUs= */ new long[] {0},
|
||||||
|
/* durationUs= */ C.TIME_UNSET));
|
||||||
|
endTracks();
|
||||||
|
pendingResetPositionUs = positionUs;
|
||||||
|
} else {
|
||||||
|
loadCondition.open();
|
||||||
|
startLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void maybeThrowPrepareError() throws IOException {
|
public void maybeThrowPrepareError() throws IOException {
|
||||||
try {
|
maybeThrowError();
|
||||||
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) {
|
if (loadingFinished && !prepared) {
|
||||||
throw ParserException.createForMalformedContainer(
|
throw ParserException.createForMalformedContainer(
|
||||||
"Loading finished before preparation is complete.", /* cause= */ null);
|
"Loading finished before preparation is complete.", /* cause= */ null);
|
||||||
@ -390,7 +400,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
if (loadingFinished
|
if (loadingFinished
|
||||||
|| loader.hasFatalError()
|
|| loader.hasFatalError()
|
||||||
|| pendingDeferredRetry
|
|| pendingDeferredRetry
|
||||||
|| (prepared && enabledTrackCount == 0)) {
|
|| ((prepared || singleTrackFormat != null) && enabledTrackCount == 0)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
boolean continuedLoading = loadCondition.open();
|
boolean continuedLoading = loadCondition.open();
|
||||||
@ -574,9 +584,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
|
|
||||||
private void maybeStartDeferredRetry(int track) {
|
private void maybeStartDeferredRetry(int track) {
|
||||||
assertPrepared();
|
assertPrepared();
|
||||||
boolean[] trackIsAudioVideoFlags = trackState.trackIsAudioVideoFlags;
|
|
||||||
if (!pendingDeferredRetry
|
if (!pendingDeferredRetry
|
||||||
|| !trackIsAudioVideoFlags[track]
|
|| (haveAudioVideoTracks && !trackState.trackIsAudioVideoFlags[track])
|
||||||
|| sampleQueues[track].isReady(/* loadingFinished= */ false)) {
|
|| sampleQueues[track].isReady(/* loadingFinished= */ false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import android.os.Looper;
|
|||||||
import androidx.annotation.GuardedBy;
|
import androidx.annotation.GuardedBy;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.Timeline;
|
import androidx.media3.common.Timeline;
|
||||||
import androidx.media3.common.util.Consumer;
|
import androidx.media3.common.util.Consumer;
|
||||||
@ -32,13 +33,16 @@ import androidx.media3.datasource.TransferListener;
|
|||||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
|
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
|
||||||
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
||||||
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
|
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
|
||||||
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
import androidx.media3.exoplayer.upstream.Allocator;
|
import androidx.media3.exoplayer.upstream.Allocator;
|
||||||
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
|
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
|
||||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
||||||
import androidx.media3.exoplayer.util.ReleasableExecutor;
|
import androidx.media3.exoplayer.util.ReleasableExecutor;
|
||||||
import androidx.media3.extractor.DefaultExtractorsFactory;
|
import androidx.media3.extractor.DefaultExtractorsFactory;
|
||||||
import androidx.media3.extractor.Extractor;
|
import androidx.media3.extractor.Extractor;
|
||||||
|
import androidx.media3.extractor.ExtractorOutput;
|
||||||
import androidx.media3.extractor.ExtractorsFactory;
|
import androidx.media3.extractor.ExtractorsFactory;
|
||||||
|
import androidx.media3.extractor.TrackOutput;
|
||||||
import com.google.common.base.Supplier;
|
import com.google.common.base.Supplier;
|
||||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@ -69,7 +73,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
private int continueLoadingCheckIntervalBytes;
|
private int continueLoadingCheckIntervalBytes;
|
||||||
@Nullable private Supplier<ReleasableExecutor> downloadExecutorSupplier;
|
@Nullable private Supplier<ReleasableExecutor> downloadExecutorSupplier;
|
||||||
private boolean suppressPrepareError;
|
private int singleTrackId;
|
||||||
|
@Nullable private Format singleTrackFormat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new factory for {@link ProgressiveMediaSource}s.
|
* Creates a new factory for {@link ProgressiveMediaSource}s.
|
||||||
@ -191,17 +196,21 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow {@link MediaPeriod} preparation to {@linkplain
|
* Allows the {@link ProgressiveMediaSource} to complete preparation without reading any data.
|
||||||
* 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
|
* <p>This must only be set if the source is guaranteed to emit a single track with the provided
|
||||||
* MediaPeriod#maybeThrowPrepareError()} (e.g. a {@link DataSource#open} error like HTTP 404) is
|
* ID and format.
|
||||||
* instead suppressed and preparation is completed with no tracks.
|
*
|
||||||
|
* <p>Data will only be loaded if the track is selected with {@link
|
||||||
|
* MediaPeriod#selectTracks(ExoTrackSelection[], boolean[], SampleStream[], boolean[], long)}
|
||||||
|
*
|
||||||
|
* @param trackId The ID of the track to pass to {@link ExtractorOutput#track}
|
||||||
|
* @param format The format of the track to pass to {@link TrackOutput#format}.
|
||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
/* package */ Factory setSuppressPrepareError(boolean suppressPrepareError) {
|
/* package */ Factory enableLazyLoadingWithSingleTrack(int trackId, Format format) {
|
||||||
this.suppressPrepareError = suppressPrepareError;
|
this.singleTrackId = trackId;
|
||||||
|
this.singleTrackFormat = checkNotNull(format);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +261,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||||||
drmSessionManagerProvider.get(mediaItem),
|
drmSessionManagerProvider.get(mediaItem),
|
||||||
loadErrorHandlingPolicy,
|
loadErrorHandlingPolicy,
|
||||||
continueLoadingCheckIntervalBytes,
|
continueLoadingCheckIntervalBytes,
|
||||||
suppressPrepareError,
|
singleTrackId,
|
||||||
|
singleTrackFormat,
|
||||||
downloadExecutorSupplier);
|
downloadExecutorSupplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +283,19 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||||||
private final DrmSessionManager drmSessionManager;
|
private final DrmSessionManager drmSessionManager;
|
||||||
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
|
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
|
||||||
private final int continueLoadingCheckIntervalBytes;
|
private final int continueLoadingCheckIntervalBytes;
|
||||||
private final boolean suppressPrepareError;
|
|
||||||
|
/**
|
||||||
|
* The ID passed to {@link Factory#enableLazyLoadingWithSingleTrack(int, Format)}. Only valid if
|
||||||
|
* {@link #singleTrackFormat} is non-null.
|
||||||
|
*/
|
||||||
|
private final int singleTrackId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link Format} passed to {@link Factory#enableLazyLoadingWithSingleTrack(int, Format)}, or
|
||||||
|
* {@code null} if not set.
|
||||||
|
*/
|
||||||
|
@Nullable private final Format singleTrackFormat;
|
||||||
|
|
||||||
@Nullable private final Supplier<ReleasableExecutor> downloadExecutorSupplier;
|
@Nullable private final Supplier<ReleasableExecutor> downloadExecutorSupplier;
|
||||||
|
|
||||||
private boolean timelineIsPlaceholder;
|
private boolean timelineIsPlaceholder;
|
||||||
@ -292,7 +314,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||||||
DrmSessionManager drmSessionManager,
|
DrmSessionManager drmSessionManager,
|
||||||
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
|
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
|
||||||
int continueLoadingCheckIntervalBytes,
|
int continueLoadingCheckIntervalBytes,
|
||||||
boolean suppressPrepareError,
|
int singleTrackId,
|
||||||
|
@Nullable Format singleTrackFormat,
|
||||||
@Nullable Supplier<ReleasableExecutor> downloadExecutorSupplier) {
|
@Nullable Supplier<ReleasableExecutor> downloadExecutorSupplier) {
|
||||||
this.mediaItem = mediaItem;
|
this.mediaItem = mediaItem;
|
||||||
this.dataSourceFactory = dataSourceFactory;
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
@ -300,7 +323,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;
|
this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;
|
||||||
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||||
this.suppressPrepareError = suppressPrepareError;
|
this.singleTrackFormat = singleTrackFormat;
|
||||||
|
this.singleTrackId = singleTrackId;
|
||||||
this.timelineIsPlaceholder = true;
|
this.timelineIsPlaceholder = true;
|
||||||
this.timelineDurationUs = C.TIME_UNSET;
|
this.timelineDurationUs = C.TIME_UNSET;
|
||||||
this.downloadExecutorSupplier = downloadExecutorSupplier;
|
this.downloadExecutorSupplier = downloadExecutorSupplier;
|
||||||
@ -359,7 +383,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||||||
allocator,
|
allocator,
|
||||||
localConfiguration.customCacheKey,
|
localConfiguration.customCacheKey,
|
||||||
continueLoadingCheckIntervalBytes,
|
continueLoadingCheckIntervalBytes,
|
||||||
suppressPrepareError,
|
singleTrackId,
|
||||||
|
singleTrackFormat,
|
||||||
Util.msToUs(localConfiguration.imageDurationMs),
|
Util.msToUs(localConfiguration.imageDurationMs),
|
||||||
downloadExecutorSupplier != null ? downloadExecutorSupplier.get() : null);
|
downloadExecutorSupplier != null ? downloadExecutorSupplier.get() : null);
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,8 @@ import androidx.test.core.app.ApplicationProvider;
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -55,6 +57,56 @@ public class SubtitlePlaybackTest {
|
|||||||
public ShadowMediaCodecConfig mediaCodecConfig =
|
public ShadowMediaCodecConfig mediaCodecConfig =
|
||||||
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
|
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
|
||||||
|
|
||||||
|
// https://github.com/androidx/media/issues/1721
|
||||||
|
@Test
|
||||||
|
public void multipleSideloadedSubtitles_noneSelected_noneLoaded() throws Exception {
|
||||||
|
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||||
|
List<Uri> loadStartedUris = new ArrayList<>();
|
||||||
|
AnalyticsListener analyticsListener =
|
||||||
|
new AnalyticsListener() {
|
||||||
|
@Override
|
||||||
|
public void onLoadStarted(
|
||||||
|
EventTime eventTime,
|
||||||
|
LoadEventInfo loadEventInfo,
|
||||||
|
MediaLoadData mediaLoadData,
|
||||||
|
int retryCount) {
|
||||||
|
loadStartedUris.add(loadEventInfo.uri);
|
||||||
|
loadStartedUris.add(loadEventInfo.dataSpec.uri);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ExoPlayer player =
|
||||||
|
new ExoPlayer.Builder(applicationContext)
|
||||||
|
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||||
|
.build();
|
||||||
|
player.addAnalyticsListener(analyticsListener);
|
||||||
|
Uri typicalVttUri = Uri.parse("asset:///media/webvtt/typical");
|
||||||
|
Uri simpleTtmlUri = Uri.parse("asset:///media/ttml/simple.xml");
|
||||||
|
MediaItem mediaItem =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri("asset:///media/mp4/sample.mp4")
|
||||||
|
.setSubtitleConfigurations(
|
||||||
|
ImmutableList.of(
|
||||||
|
new MediaItem.SubtitleConfiguration.Builder(typicalVttUri)
|
||||||
|
.setMimeType(MimeTypes.TEXT_VTT)
|
||||||
|
.setLanguage("en")
|
||||||
|
.build(),
|
||||||
|
new MediaItem.SubtitleConfiguration.Builder(simpleTtmlUri)
|
||||||
|
.setMimeType(MimeTypes.APPLICATION_TTML)
|
||||||
|
.setLanguage("en")
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
player.setMediaItem(mediaItem);
|
||||||
|
player.prepare();
|
||||||
|
run(player).untilState(Player.STATE_READY);
|
||||||
|
run(player).untilLoadingIs(false);
|
||||||
|
player.play();
|
||||||
|
run(player).untilState(Player.STATE_ENDED);
|
||||||
|
player.release();
|
||||||
|
|
||||||
|
assertThat(loadStartedUris).containsNoneOf(typicalVttUri, simpleTtmlUri);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sideloadedSubtitleLoadingError_playbackContinues_errorReportedToAnalyticsListener()
|
public void sideloadedSubtitleLoadingError_playbackContinues_errorReportedToAnalyticsListener()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
@ -105,9 +157,8 @@ public class SubtitlePlaybackTest {
|
|||||||
surface.release();
|
surface.release();
|
||||||
|
|
||||||
assertThat(loadErrorEventInfo.get().uri).isEqualTo(notFoundSubtitleUri);
|
assertThat(loadErrorEventInfo.get().uri).isEqualTo(notFoundSubtitleUri);
|
||||||
// Assert the output is the same as playing the video without sideloaded subtitles.
|
|
||||||
DumpFileAsserts.assertOutput(
|
DumpFileAsserts.assertOutput(
|
||||||
applicationContext, playbackOutput, "playbackdumps/mp4/sample.mp4.dump");
|
applicationContext, playbackOutput, "playbackdumps/subtitles/sideloaded-error.mp4.dump");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -172,8 +223,6 @@ public class SubtitlePlaybackTest {
|
|||||||
.hasMessageThat()
|
.hasMessageThat()
|
||||||
.contains("test subtitle parsing error");
|
.contains("test subtitle parsing error");
|
||||||
DumpFileAsserts.assertOutput(
|
DumpFileAsserts.assertOutput(
|
||||||
applicationContext,
|
applicationContext, playbackOutput, "playbackdumps/subtitles/sideloaded-error.mp4.dump");
|
||||||
playbackOutput,
|
|
||||||
"playbackdumps/subtitles/sideloaded-parse-error.mp4.dump");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,8 @@ public final class ProgressiveMediaPeriodTest {
|
|||||||
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
|
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
|
||||||
/* customCacheKey= */ null,
|
/* customCacheKey= */ null,
|
||||||
ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES,
|
ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES,
|
||||||
/* suppressPrepareError= */ false,
|
/* singleTrackId= */ 0,
|
||||||
|
/* singleTrackFormat= */ null,
|
||||||
imageDurationUs,
|
imageDurationUs,
|
||||||
executor != null ? ReleasableExecutor.from(executor, executorReleased) : null);
|
executor != null ? ReleasableExecutor.from(executor, executorReleased) : null);
|
||||||
|
|
||||||
|
@ -15,24 +15,39 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.source;
|
package androidx.media3.exoplayer.source;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
import static androidx.media3.test.utils.robolectric.RobolectricUtil.DEFAULT_TIMEOUT_MS;
|
import static androidx.media3.test.utils.robolectric.RobolectricUtil.DEFAULT_TIMEOUT_MS;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
import android.os.SystemClock;
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.Timeline;
|
import androidx.media3.common.Timeline;
|
||||||
|
import androidx.media3.common.util.ConditionVariable;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.datasource.DataSource;
|
||||||
import androidx.media3.datasource.DefaultDataSource;
|
import androidx.media3.datasource.DefaultDataSource;
|
||||||
|
import androidx.media3.datasource.ResolvingDataSource;
|
||||||
|
import androidx.media3.exoplayer.LoadingInfo;
|
||||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||||
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
|
import androidx.media3.exoplayer.trackselection.FixedTrackSelection;
|
||||||
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
|
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
|
||||||
import androidx.media3.test.utils.MediaSourceTestRunner;
|
import androidx.media3.test.utils.MediaSourceTestRunner;
|
||||||
import androidx.media3.test.utils.TestUtil;
|
import androidx.media3.test.utils.TestUtil;
|
||||||
import androidx.media3.test.utils.robolectric.RobolectricUtil;
|
import androidx.media3.test.utils.robolectric.RobolectricUtil;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -119,14 +134,91 @@ public class ProgressiveMediaSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void maybeThrowPrepareError_withSuppressPrepareError_doesNotThrow() throws Exception {
|
public void lazyLoading_preparationCompletesWithoutLoadingData_loadsDataWhenTrackSelected()
|
||||||
|
throws Exception {
|
||||||
|
Set<Uri> openedUris = new HashSet<>();
|
||||||
|
DataSource.Factory dataSourceFactory =
|
||||||
|
new ResolvingDataSource.Factory(
|
||||||
|
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()),
|
||||||
|
dataSpec -> {
|
||||||
|
openedUris.add(dataSpec.uri);
|
||||||
|
return dataSpec;
|
||||||
|
});
|
||||||
|
Uri mediaUri = Uri.parse("asset:///media/mp4/sample_opus.mp4");
|
||||||
|
Format format =
|
||||||
|
new Format.Builder().setId("format ID").setSampleMimeType(MimeTypes.AUDIO_OPUS).build();
|
||||||
|
ProgressiveMediaSource mediaSource =
|
||||||
|
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||||
|
.enableLazyLoadingWithSingleTrack(/* trackId= */ 42, format)
|
||||||
|
.createMediaSource(MediaItem.fromUri(mediaUri));
|
||||||
|
MediaSourceTestRunner mediaSourceTestRunner = new MediaSourceTestRunner(mediaSource);
|
||||||
|
ConditionVariable loadCompleted = new ConditionVariable();
|
||||||
|
mediaSourceTestRunner.runOnPlaybackThread(
|
||||||
|
() ->
|
||||||
|
mediaSource.addEventListener(
|
||||||
|
new Handler(checkNotNull(Looper.myLooper())),
|
||||||
|
new MediaSourceEventListener() {
|
||||||
|
@Override
|
||||||
|
public void onLoadCompleted(
|
||||||
|
int windowIndex,
|
||||||
|
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||||
|
LoadEventInfo loadEventInfo,
|
||||||
|
MediaLoadData mediaLoadData) {
|
||||||
|
loadCompleted.open();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Timeline timeline = mediaSourceTestRunner.prepareSource();
|
||||||
|
MediaPeriod mediaPeriod =
|
||||||
|
mediaSourceTestRunner.createPeriod(
|
||||||
|
new MediaSource.MediaPeriodId(
|
||||||
|
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
|
||||||
|
CountDownLatch preparedLatch =
|
||||||
|
mediaSourceTestRunner.preparePeriod(mediaPeriod, /* positionUs= */ 0);
|
||||||
|
|
||||||
|
assertThat(preparedLatch.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
assertThat(openedUris).isEmpty();
|
||||||
|
|
||||||
|
ListenableFuture<Boolean> isLoading =
|
||||||
|
mediaSourceTestRunner.asyncRunOnPlaybackThread(
|
||||||
|
() -> {
|
||||||
|
mediaPeriod.continueLoading(
|
||||||
|
new LoadingInfo.Builder().setPlaybackPositionUs(0).build());
|
||||||
|
return mediaPeriod.isLoading();
|
||||||
|
});
|
||||||
|
assertThat(isLoading.get()).isFalse();
|
||||||
|
|
||||||
|
isLoading =
|
||||||
|
mediaSourceTestRunner.asyncRunOnPlaybackThread(
|
||||||
|
() -> {
|
||||||
|
selectOnlyTrack(mediaPeriod);
|
||||||
|
mediaPeriod.continueLoading(
|
||||||
|
new LoadingInfo.Builder().setPlaybackPositionUs(0).build());
|
||||||
|
return mediaPeriod.isLoading();
|
||||||
|
});
|
||||||
|
assertThat(isLoading.get()).isTrue();
|
||||||
|
|
||||||
|
loadCompleted.block();
|
||||||
|
|
||||||
|
assertThat(mediaSourceTestRunner.asyncRunOnPlaybackThread(mediaPeriod::isLoading).get())
|
||||||
|
.isFalse();
|
||||||
|
assertThat(openedUris).containsExactly(mediaUri);
|
||||||
|
|
||||||
|
mediaSourceTestRunner.releasePeriod(mediaPeriod);
|
||||||
|
mediaSourceTestRunner.releaseSource();
|
||||||
|
mediaSourceTestRunner.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lazyLoading_notFoundUri_loadErrorReportedWhenTrackSelected() throws Exception {
|
||||||
ProgressiveMediaSource mediaSource =
|
ProgressiveMediaSource mediaSource =
|
||||||
new ProgressiveMediaSource.Factory(
|
new ProgressiveMediaSource.Factory(
|
||||||
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()))
|
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()))
|
||||||
// Disable retries, so the first error is marked fatal.
|
// Disable retries, so the first error is marked fatal.
|
||||||
.setLoadErrorHandlingPolicy(
|
.setLoadErrorHandlingPolicy(
|
||||||
new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 0))
|
new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 0))
|
||||||
.setSuppressPrepareError(true)
|
.enableLazyLoadingWithSingleTrack(
|
||||||
|
/* trackId= */ 42, new Format.Builder().setId("format ID").build())
|
||||||
.createMediaSource(MediaItem.fromUri("file:///not/found"));
|
.createMediaSource(MediaItem.fromUri("file:///not/found"));
|
||||||
MediaSourceTestRunner mediaSourceTestRunner = new MediaSourceTestRunner(mediaSource);
|
MediaSourceTestRunner mediaSourceTestRunner = new MediaSourceTestRunner(mediaSource);
|
||||||
|
|
||||||
@ -154,27 +246,19 @@ public class ProgressiveMediaSourceTest {
|
|||||||
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
|
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
|
||||||
CountDownLatch preparedLatch =
|
CountDownLatch preparedLatch =
|
||||||
mediaSourceTestRunner.preparePeriod(mediaPeriod, /* positionUs= */ 0);
|
mediaSourceTestRunner.preparePeriod(mediaPeriod, /* positionUs= */ 0);
|
||||||
assertThat(loadErrorReported.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();
|
|
||||||
// Call maybeThrowPrepareError() in a loop until preparation completes (preparation is not
|
|
||||||
// unblocked until the error is caught and suppressed inside maybeThrowPrepareError()). This
|
|
||||||
// mimics the behaviour of ExoPlayerImplInternal which calls maybeThrowPrepareError() on
|
|
||||||
// un-prepared MediaPeriods on every doSomeWork() iteration.
|
|
||||||
long startTime = SystemClock.elapsedRealtime();
|
|
||||||
do {
|
|
||||||
AtomicReference<Throwable> prepareError = new AtomicReference<>();
|
|
||||||
mediaSourceTestRunner.runOnPlaybackThread(
|
|
||||||
() -> {
|
|
||||||
try {
|
|
||||||
mediaPeriod.maybeThrowPrepareError();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
prepareError.set(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
assertThat(prepareError.get()).isNull();
|
|
||||||
} while (preparedLatch.getCount() > 0
|
|
||||||
&& (SystemClock.elapsedRealtime() - startTime) < DEFAULT_TIMEOUT_MS);
|
|
||||||
assertThat(preparedLatch.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();
|
assertThat(preparedLatch.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
|
||||||
|
ListenableFuture<Boolean> isLoading =
|
||||||
|
mediaSourceTestRunner.asyncRunOnPlaybackThread(
|
||||||
|
() -> {
|
||||||
|
selectOnlyTrack(mediaPeriod);
|
||||||
|
mediaPeriod.continueLoading(
|
||||||
|
new LoadingInfo.Builder().setPlaybackPositionUs(0).build());
|
||||||
|
return mediaPeriod.isLoading();
|
||||||
|
});
|
||||||
|
assertThat(isLoading.get()).isTrue();
|
||||||
|
assertThat(loadErrorReported.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
|
||||||
mediaSourceTestRunner.releasePeriod(mediaPeriod);
|
mediaSourceTestRunner.releasePeriod(mediaPeriod);
|
||||||
mediaSourceTestRunner.releaseSource();
|
mediaSourceTestRunner.releaseSource();
|
||||||
mediaSourceTestRunner.release();
|
mediaSourceTestRunner.release();
|
||||||
@ -185,4 +269,14 @@ public class ProgressiveMediaSourceTest {
|
|||||||
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()))
|
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()))
|
||||||
.createMediaSource(mediaItem);
|
.createMediaSource(mediaItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void selectOnlyTrack(MediaPeriod mediaPeriod) {
|
||||||
|
checkState(mediaPeriod.getTrackGroups().length == 1);
|
||||||
|
mediaPeriod.selectTracks(
|
||||||
|
new ExoTrackSelection[] {new FixedTrackSelection(mediaPeriod.getTrackGroups().get(0), 0)},
|
||||||
|
/* mayRetainStreamFlags= */ new boolean[] {false},
|
||||||
|
new SampleStream[1],
|
||||||
|
/* streamResetFlags= */ new boolean[] {false},
|
||||||
|
/* positionUs= */ 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
|||||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||||
|
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
@ -48,6 +49,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
/** Generic extractor for extracting subtitles from various subtitle formats. */
|
/** Generic extractor for extracting subtitles from various subtitle formats. */
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class SubtitleExtractor implements Extractor {
|
public class SubtitleExtractor implements Extractor {
|
||||||
|
|
||||||
|
/** The ID of the single track emitted by this extractor. */
|
||||||
|
public static final int TRACK_ID = 0;
|
||||||
|
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@Target(TYPE_USE)
|
@Target(TYPE_USE)
|
||||||
@ -83,7 +88,7 @@ public class SubtitleExtractor implements Extractor {
|
|||||||
|
|
||||||
private final SubtitleParser subtitleParser;
|
private final SubtitleParser subtitleParser;
|
||||||
private final CueEncoder cueEncoder;
|
private final CueEncoder cueEncoder;
|
||||||
private final Format format;
|
@Nullable private final Format format;
|
||||||
private final List<Sample> samples;
|
private final List<Sample> samples;
|
||||||
private final ParsableByteArray scratchSampleArray;
|
private final ParsableByteArray scratchSampleArray;
|
||||||
|
|
||||||
@ -99,20 +104,26 @@ public class SubtitleExtractor implements Extractor {
|
|||||||
*
|
*
|
||||||
* @param subtitleParser The parser used for parsing the subtitle data. The extractor will reset
|
* @param subtitleParser The parser used for parsing the subtitle data. The extractor will reset
|
||||||
* the parser in {@link SubtitleExtractor#release()}.
|
* the parser in {@link SubtitleExtractor#release()}.
|
||||||
* @param format {@link Format} that describes subtitle data.
|
* @param format {@link Format} that describes subtitle data. Can be null if {@link
|
||||||
|
* TrackOutput#format}, {@link ExtractorOutput#seekMap} and {@link
|
||||||
|
* ExtractorOutput#endTracks()} will be called outside this extractor.
|
||||||
*/
|
*/
|
||||||
public SubtitleExtractor(SubtitleParser subtitleParser, Format format) {
|
public SubtitleExtractor(SubtitleParser subtitleParser, @Nullable Format format) {
|
||||||
this.subtitleParser = subtitleParser;
|
this.subtitleParser = subtitleParser;
|
||||||
cueEncoder = new CueEncoder();
|
cueEncoder = new CueEncoder();
|
||||||
subtitleData = Util.EMPTY_BYTE_ARRAY;
|
subtitleData = Util.EMPTY_BYTE_ARRAY;
|
||||||
scratchSampleArray = new ParsableByteArray();
|
scratchSampleArray = new ParsableByteArray();
|
||||||
|
// TODO: b/376693592 - Simplify this by taking the post-transformation Format as a parameter
|
||||||
|
// instead.
|
||||||
this.format =
|
this.format =
|
||||||
format
|
format != null
|
||||||
.buildUpon()
|
? format
|
||||||
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
|
.buildUpon()
|
||||||
.setCodecs(format.sampleMimeType)
|
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
|
||||||
.setCueReplacementBehavior(subtitleParser.getCueReplacementBehavior())
|
.setCodecs(format.sampleMimeType)
|
||||||
.build();
|
.setCueReplacementBehavior(subtitleParser.getCueReplacementBehavior())
|
||||||
|
.build()
|
||||||
|
: null;
|
||||||
samples = new ArrayList<>();
|
samples = new ArrayList<>();
|
||||||
state = STATE_CREATED;
|
state = STATE_CREATED;
|
||||||
timestamps = Util.EMPTY_LONG_ARRAY;
|
timestamps = Util.EMPTY_LONG_ARRAY;
|
||||||
@ -130,14 +141,16 @@ public class SubtitleExtractor implements Extractor {
|
|||||||
@Override
|
@Override
|
||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput output) {
|
||||||
checkState(state == STATE_CREATED);
|
checkState(state == STATE_CREATED);
|
||||||
trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_TEXT);
|
trackOutput = output.track(TRACK_ID, C.TRACK_TYPE_TEXT);
|
||||||
trackOutput.format(format);
|
if (format != null) {
|
||||||
output.endTracks();
|
trackOutput.format(format);
|
||||||
output.seekMap(
|
output.endTracks();
|
||||||
new IndexSeekMap(
|
output.seekMap(
|
||||||
/* positions= */ new long[] {0},
|
new IndexSeekMap(
|
||||||
/* timesUs= */ new long[] {0},
|
/* positions= */ new long[] {0},
|
||||||
/* durationUs= */ C.TIME_UNSET));
|
/* timesUs= */ new long[] {0},
|
||||||
|
/* durationUs= */ C.TIME_UNSET));
|
||||||
|
}
|
||||||
state = STATE_INITIALIZED;
|
state = STATE_INITIALIZED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,13 +42,17 @@ import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller;
|
|||||||
import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
||||||
import androidx.media3.exoplayer.upstream.Allocator;
|
import androidx.media3.exoplayer.upstream.Allocator;
|
||||||
import androidx.media3.exoplayer.upstream.DefaultAllocator;
|
import androidx.media3.exoplayer.upstream.DefaultAllocator;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.LinkedBlockingDeque;
|
import java.util.concurrent.LinkedBlockingDeque;
|
||||||
import org.checkerframework.dataflow.qual.SideEffectFree;
|
import org.checkerframework.dataflow.qual.SideEffectFree;
|
||||||
|
|
||||||
@ -93,26 +97,35 @@ public class MediaSourceTestRunner {
|
|||||||
* @param runnable The {@link Runnable} to run.
|
* @param runnable The {@link Runnable} to run.
|
||||||
*/
|
*/
|
||||||
public void runOnPlaybackThread(final Runnable runnable) {
|
public void runOnPlaybackThread(final Runnable runnable) {
|
||||||
Throwable[] throwable = new Throwable[1];
|
ListenableFuture<Void> result =
|
||||||
CountDownLatch finishedLatch = new CountDownLatch(1);
|
asyncRunOnPlaybackThread(
|
||||||
|
() -> {
|
||||||
|
runnable.run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
result.get();
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
Util.sneakyThrow(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the provided {@link Callable} on the playback thread and returns a future of the result.
|
||||||
|
*
|
||||||
|
* @param callable The {@link Callable} to run.
|
||||||
|
*/
|
||||||
|
public <T> ListenableFuture<T> asyncRunOnPlaybackThread(Callable<T> callable) {
|
||||||
|
SettableFuture<T> result = SettableFuture.create();
|
||||||
playbackHandler.post(
|
playbackHandler.post(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
runnable.run();
|
result.set(callable.call());
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
throwable[0] = e;
|
result.setException(e);
|
||||||
} finally {
|
|
||||||
finishedLatch.countDown();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
try {
|
return result;
|
||||||
assertThat(finishedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Util.sneakyThrow(e);
|
|
||||||
}
|
|
||||||
if (throwable[0] != null) {
|
|
||||||
Util.sneakyThrow(throwable[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user