Skip to content

Commit

Permalink
Parameterize load error handling in HLS
Browse files Browse the repository at this point in the history
Issue:#2844
Issue:#2981

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=204718939
  • Loading branch information
AquilesCanta authored and ojw28 committed Jul 17, 2018
1 parent e2059ea commit e247a08
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 52 deletions.
2 changes: 2 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
* HLS:
* Pass HTTP response headers to `HlsExtractorFactory.createExtractor`.
* Add support for EXT-X-INDEPENDENT-SEGMENTS in the master playlist.
* Support load error handling customization
([#2981](https://github.com/google/ExoPlayer/issues/2981)).
* DRM:
* Allow DrmInitData to carry a license server URL
([#3393](https://github.com/google/ExoPlayer/issues/3393)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ private LoadErrorAction(@RetryActionType int type, long retryDelayMillis) {
this.type = type;
this.retryDelayMillis = retryDelayMillis;
}

/** Returns whether this is a retry action. */
public boolean isRetry() {
return type == ACTION_TYPE_RETRY || type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT;
}
}

private final ExecutorService downloadExecutorService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,17 +397,17 @@ public void onChunkLoadCompleted(Chunk chunk) {
}

/**
* Called when the {@link HlsSampleStreamWrapper} encounters an error loading a chunk obtained
* from this source.
* Attempts to blacklist the track associated with the given chunk. Blacklisting will fail if the
* track is the only non-blacklisted track in the selection.
*
* @param chunk The chunk whose load encountered the error.
* @param cancelable Whether the load can be canceled.
* @param error The error.
* @return Whether the load should be canceled.
* @param chunk The chunk whose load caused the blacklisting attempt.
* @param blacklistDurationMs The number of milliseconds for which the track selection should be
* blacklisted.
* @return Whether the blacklisting succeeded.
*/
public boolean onChunkLoadError(Chunk chunk, boolean cancelable, IOException error) {
return cancelable && ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection,
trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), error);
public boolean maybeBlacklistTrack(Chunk chunk, long blacklistDurationMs) {
return trackSelection.blacklist(
trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), blacklistDurationMs);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
Expand All @@ -53,6 +56,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private final HlsPlaylistTracker playlistTracker;
private final HlsDataSourceFactory dataSourceFactory;
private final @Nullable TransferListener<? super DataSource> mediaTransferListener;
private final LoadErrorHandlingPolicy<Chunk> chunkLoadErrorHandlingPolicy;
private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher;
private final Allocator allocator;
Expand All @@ -69,11 +73,29 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private SequenceableLoader compositeSequenceableLoader;
private boolean notifiedReadingStarted;

/**
* Creates an HLS media period.
*
* @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the segments.
* @param playlistTracker A tracker for HLS playlists.
* @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for segments
* and keys.
* @param mediaTransferListener The transfer listener to inform of any media data transfers. May
* be null if no listener is available.
* @param chunkLoadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy} for chunk loads.
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
* @param eventDispatcher A dispatcher to notify of events.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param compositeSequenceableLoaderFactory A factory to create composite {@link
* SequenceableLoader}s for when this media source loads data from multiple streams.
* @param allowChunklessPreparation Whether chunkless preparation is allowed.
*/
public HlsMediaPeriod(
HlsExtractorFactory extractorFactory,
HlsPlaylistTracker playlistTracker,
HlsDataSourceFactory dataSourceFactory,
@Nullable TransferListener<? super DataSource> mediaTransferListener,
LoadErrorHandlingPolicy<Chunk> chunkLoadErrorHandlingPolicy,
int minLoadableRetryCount,
EventDispatcher eventDispatcher,
Allocator allocator,
Expand All @@ -83,6 +105,7 @@ public HlsMediaPeriod(
this.playlistTracker = playlistTracker;
this.dataSourceFactory = dataSourceFactory;
this.mediaTransferListener = mediaTransferListener;
this.chunkLoadErrorHandlingPolicy = chunkLoadErrorHandlingPolicy;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventDispatcher = eventDispatcher;
this.allocator = allocator;
Expand Down Expand Up @@ -506,8 +529,16 @@ private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[]
mediaTransferListener,
timestampAdjusterProvider,
muxedCaptionFormats);
return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, positionUs,
muxedAudioFormat, minLoadableRetryCount, eventDispatcher);
return new HlsSampleStreamWrapper(
trackType,
/* callback= */ this,
defaultChunkSource,
allocator,
positionUs,
muxedAudioFormat,
chunkLoadErrorHandlingPolicy,
minLoadableRetryCount,
eventDispatcher);
}

private static Format deriveVideoFormat(Format variantFormat) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
Expand All @@ -62,6 +64,7 @@ public static final class Factory implements AdsMediaSource.MediaSourceFactory {
private @Nullable ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private @Nullable HlsPlaylistTracker playlistTracker;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy<Chunk> chunkLoadErrorHandlingPolicy;
private int minLoadableRetryCount;
private boolean allowChunklessPreparation;
private boolean isCreateCalled;
Expand All @@ -87,6 +90,7 @@ public Factory(DataSource.Factory dataSourceFactory) {
public Factory(HlsDataSourceFactory hlsDataSourceFactory) {
this.hlsDataSourceFactory = Assertions.checkNotNull(hlsDataSourceFactory);
extractorFactory = HlsExtractorFactory.DEFAULT;
chunkLoadErrorHandlingPolicy = LoadErrorHandlingPolicy.getDefault();
minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT;
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
}
Expand Down Expand Up @@ -121,6 +125,21 @@ public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) {
return this;
}

/**
* Sets the {@link LoadErrorHandlingPolicy} for chunk loads. The default value is {@link
* LoadErrorHandlingPolicy#DEFAULT}.
*
* @param chunkLoadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy} for chunk loads.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setChunkLoadErrorHandlingPolicy(
LoadErrorHandlingPolicy<Chunk> chunkLoadErrorHandlingPolicy) {
Assertions.checkState(!isCreateCalled);
this.chunkLoadErrorHandlingPolicy = chunkLoadErrorHandlingPolicy;
return this;
}

/**
* Sets the minimum number of times to retry if a loading error occurs. The default value is
* {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}.
Expand Down Expand Up @@ -215,6 +234,7 @@ public HlsMediaSource createMediaSource(Uri playlistUri) {
playlistTracker =
new DefaultHlsPlaylistTracker(
hlsDataSourceFactory,
LoadErrorHandlingPolicy.getDefault(),
minLoadableRetryCount,
playlistParser != null ? playlistParser : new HlsPlaylistParser());
}
Expand All @@ -223,6 +243,7 @@ public HlsMediaSource createMediaSource(Uri playlistUri) {
hlsDataSourceFactory,
extractorFactory,
compositeSequenceableLoaderFactory,
chunkLoadErrorHandlingPolicy,
minLoadableRetryCount,
playlistTracker,
allowChunklessPreparation,
Expand Down Expand Up @@ -260,6 +281,7 @@ public int[] getSupportedTypes() {
private final Uri manifestUri;
private final HlsDataSourceFactory dataSourceFactory;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final LoadErrorHandlingPolicy<Chunk> chunkLoadErrorHandlingPolicy;
private final int minLoadableRetryCount;
private final boolean allowChunklessPreparation;
private final HlsPlaylistTracker playlistTracker;
Expand Down Expand Up @@ -341,8 +363,13 @@ public HlsMediaSource(
dataSourceFactory,
extractorFactory,
new DefaultCompositeSequenceableLoaderFactory(),
LoadErrorHandlingPolicy.getDefault(),
minLoadableRetryCount,
new DefaultHlsPlaylistTracker(dataSourceFactory, minLoadableRetryCount, playlistParser),
new DefaultHlsPlaylistTracker(
dataSourceFactory,
LoadErrorHandlingPolicy.getDefault(),
minLoadableRetryCount,
playlistParser),
/* allowChunklessPreparation= */ false,
/* tag= */ null);
if (eventHandler != null && eventListener != null) {
Expand All @@ -355,6 +382,7 @@ private HlsMediaSource(
HlsDataSourceFactory dataSourceFactory,
HlsExtractorFactory extractorFactory,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
LoadErrorHandlingPolicy<Chunk> chunkLoadErrorHandlingPolicy,
int minLoadableRetryCount,
HlsPlaylistTracker playlistTracker,
boolean allowChunklessPreparation,
Expand All @@ -363,6 +391,7 @@ private HlsMediaSource(
this.dataSourceFactory = dataSourceFactory;
this.extractorFactory = extractorFactory;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.chunkLoadErrorHandlingPolicy = chunkLoadErrorHandlingPolicy;
this.minLoadableRetryCount = minLoadableRetryCount;
this.playlistTracker = playlistTracker;
this.allowChunklessPreparation = allowChunklessPreparation;
Expand Down Expand Up @@ -393,6 +422,7 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
playlistTracker,
dataSourceFactory,
mediaTransferListener,
chunkLoadErrorHandlingPolicy,
minLoadableRetryCount,
eventDispatcher,
allocator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
Expand All @@ -39,6 +38,7 @@
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
import com.google.android.exoplayer2.util.Assertions;
Expand Down Expand Up @@ -97,6 +97,7 @@ public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWra
private final HlsChunkSource chunkSource;
private final Allocator allocator;
private final Format muxedAudioFormat;
private final LoadErrorHandlingPolicy<Chunk> chunkLoadErrorHandlingPolicy;
private final int minLoadableRetryCount;
private final Loader loader;
private final EventDispatcher eventDispatcher;
Expand Down Expand Up @@ -149,18 +150,27 @@ public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWra
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param positionUs The position from which to start loading media.
* @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist.
* @param chunkLoadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for chunk loads.
* @param minLoadableRetryCount The minimum number of times that the source should retry a load
* before propagating an error.
* @param eventDispatcher A dispatcher to notify of events.
*/
public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource,
Allocator allocator, long positionUs, Format muxedAudioFormat, int minLoadableRetryCount,
public HlsSampleStreamWrapper(
int trackType,
Callback callback,
HlsChunkSource chunkSource,
Allocator allocator,
long positionUs,
Format muxedAudioFormat,
LoadErrorHandlingPolicy<Chunk> chunkLoadErrorHandlingPolicy,
int minLoadableRetryCount,
EventDispatcher eventDispatcher) {
this.trackType = trackType;
this.callback = callback;
this.chunkSource = chunkSource;
this.allocator = allocator;
this.muxedAudioFormat = muxedAudioFormat;
this.chunkLoadErrorHandlingPolicy = chunkLoadErrorHandlingPolicy;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventDispatcher = eventDispatcher;
loader = new Loader("Loader:HlsSampleStreamWrapper");
Expand All @@ -174,20 +184,8 @@ public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource c
mediaChunks = new ArrayList<>();
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
hlsSampleStreams = new ArrayList<>();
maybeFinishPrepareRunnable =
new Runnable() {
@Override
public void run() {
maybeFinishPrepare();
}
};
onTracksEndedRunnable =
new Runnable() {
@Override
public void run() {
onTracksEnded();
}
};
maybeFinishPrepareRunnable = this::maybeFinishPrepare;
onTracksEndedRunnable = this::onTracksEnded;
handler = new Handler();
lastSeekPositionUs = positionUs;
pendingResetPositionUs = positionUs;
Expand Down Expand Up @@ -644,18 +642,37 @@ public LoadErrorAction onLoadError(
int errorCount) {
long bytesLoaded = loadable.bytesLoaded();
boolean isMediaChunk = isMediaChunk(loadable);
boolean cancelable = !isMediaChunk || bytesLoaded == 0;
boolean canceled = false;
if (chunkSource.onChunkLoadError(loadable, cancelable, error)) {
boolean blacklistSucceeded = false;
LoadErrorAction loadErrorAction;

if (!isMediaChunk || bytesLoaded == 0) {
long blacklistDurationMs =
chunkLoadErrorHandlingPolicy.getBlacklistDurationMsFor(
loadable, loadDurationMs, error, errorCount);
if (blacklistDurationMs != C.TIME_UNSET) {
blacklistSucceeded = chunkSource.maybeBlacklistTrack(loadable, blacklistDurationMs);
}
}

if (blacklistSucceeded) {
if (isMediaChunk) {
HlsMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1);
Assertions.checkState(removed == loadable);
if (mediaChunks.isEmpty()) {
pendingResetPositionUs = lastSeekPositionUs;
}
}
canceled = true;
loadErrorAction = Loader.DONT_RETRY;
} else /* did not blacklist */ {
long retryDelayMs =
chunkLoadErrorHandlingPolicy.getRetryDelayMsFor(
loadable, loadDurationMs, error, errorCount);
loadErrorAction =
retryDelayMs != C.TIME_UNSET
? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs)
: Loader.DONT_RETRY_FATAL;
}

eventDispatcher.loadError(
loadable.dataSpec,
loadable.getUri(),
Expand All @@ -670,17 +687,16 @@ public LoadErrorAction onLoadError(
loadDurationMs,
loadable.bytesLoaded(),
error,
canceled);
if (canceled) {
/* wasCanceled= */ !loadErrorAction.isRetry());

if (blacklistSucceeded) {
if (!prepared) {
continueLoading(lastSeekPositionUs);
} else {
callback.onContinueLoadingRequested(this);
}
return Loader.DONT_RETRY;
} else {
return error instanceof ParserException ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
}
return loadErrorAction;
}

// Called by the consuming thread, but only when there is no loading thread.
Expand Down
Loading

0 comments on commit e247a08

Please sign in to comment.