From 1cbc0fc678d73c1807a89cc8a1a50ea6fe685a83 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 7 Dec 2016 03:58:59 -0800 Subject: [PATCH] Allow HlsPlaylistTracker to change the primaryHlsUrl When the primary url is blacklisted (due to a 404, for example) or the selected variant is different from primary url, allow the tracker to change the url. Issue:#87 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=141291435 --- .../chunk/ChunkedTrackBlacklistUtil.java | 49 ++-- .../exoplayer2/source/hls/HlsChunkSource.java | 25 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 42 ++-- .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 4 +- .../source/hls/playlist/HlsMediaPlaylist.java | 16 +- .../hls/playlist/HlsPlaylistParser.java | 9 +- .../hls/playlist/HlsPlaylistTracker.java | 213 ++++++++++++------ 8 files changed, 220 insertions(+), 140 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java index 6c085418bd..38e0c0d51f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java @@ -51,9 +51,9 @@ public final class ChunkedTrackBlacklistUtil { /** * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for - * {@code blacklistDurationMs} if {@code e} is an {@link InvalidResponseCodeException} with - * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. Else does nothing. Note - * that blacklisting will fail if the track is the only non-blacklisted track in the selection. + * {@code blacklistDurationMs} if calling {@link #shouldBlacklist(Exception)} for {@code e} + * returns true. Else does nothing. Note that blacklisting will fail if the track is the only + * non-blacklisted track in the selection. * * @param trackSelection The track selection. * @param trackSelectionIndex The index in the selection to consider blacklisting. @@ -63,24 +63,33 @@ public final class ChunkedTrackBlacklistUtil { */ public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex, Exception e, long blacklistDurationMs) { - if (trackSelection.length() == 1) { - // Blacklisting won't ever work if there's only one track in the selection. - return false; - } - if (e instanceof InvalidResponseCodeException) { - InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e; - int responseCode = responseCodeException.responseCode; - if (responseCode == 404 || responseCode == 410) { - boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs); - if (blacklisted) { - Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode=" - + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); - } else { - Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode=" - + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); - } - return blacklisted; + if (shouldBlacklist(e)) { + boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs); + int responseCode = ((InvalidResponseCodeException) e).responseCode; + if (blacklisted) { + Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode=" + + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); + } else { + Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode=" + + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); } + return blacklisted; + } + return false; + } + + /** + * Returns whether a loading error is an {@link InvalidResponseCodeException} with + * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. + * + * @param e The loading error. + * @return Wheter the loading error is an {@link InvalidResponseCodeException} with + * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. + */ + public static boolean shouldBlacklist(Exception e) { + if (e instanceof InvalidResponseCodeException) { + int responseCode = ((InvalidResponseCodeException) e).responseCode; + return responseCode == 404 || responseCode == 410; } return false; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index a44d272bd3..9deff145d8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -278,9 +278,10 @@ import java.util.Locale; DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex], - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, - startTimeUs + segment.durationUs, chunkMediaSequence, segment.discontinuitySequenceNumber, - isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), + startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, + segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous, + encryptionKey, encryptionIv); } /** @@ -317,19 +318,19 @@ import java.util.Locale; } /** - * Called when an error is encountered while loading a playlist. + * Called when a playlist is blacklisted. * - * @param url The url that references the playlist whose load encountered the error. - * @param error The error. + * @param url The url that references the blacklisted playlist. + * @param blacklistMs The amount of milliseconds for which the playlist was blacklisted. */ - public void onPlaylistLoadError(HlsUrl url, IOException error) { + public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { int trackGroupIndex = trackGroup.indexOf(url.format); - if (trackGroupIndex == C.INDEX_UNSET) { - // The url is not handled by this chunk source. - return; + if (trackGroupIndex != C.INDEX_UNSET) { + int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex); + if (trackSelectionIndex != C.INDEX_UNSET) { + trackSelection.blacklist(trackSelectionIndex, blacklistMs); + } } - ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, - trackSelection.indexOf(trackGroupIndex), error); } // Private methods. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index be07b3410e..6082372b05 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -31,7 +31,6 @@ 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.Loader; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; @@ -42,7 +41,7 @@ import java.util.List; * A {@link MediaPeriod} that loads an HLS stream. */ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback, - HlsPlaylistTracker.PlaylistRefreshCallback { + HlsPlaylistTracker.PlaylistEventListener { private final HlsPlaylistTracker playlistTracker; private final DataSource.Factory dataSourceFactory; @@ -52,7 +51,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; private final Handler continueLoadingHandler; - private final Loader manifestFetcher; private final long preparePositionUs; private Callback callback; @@ -74,13 +72,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); continueLoadingHandler = new Handler(); - manifestFetcher = new Loader("Loader:ManifestFetcher"); preparePositionUs = positionUs; } public void release() { + playlistTracker.removeListener(this); continueLoadingHandler.removeCallbacksAndMessages(null); - manifestFetcher.release(); if (sampleStreamWrappers != null) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.release(); @@ -90,15 +87,14 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void prepare(Callback callback) { + playlistTracker.addListener(this); this.callback = callback; buildAndPrepareSampleStreamWrappers(); } @Override public void maybeThrowPrepareError() throws IOException { - if (sampleStreamWrappers == null) { - manifestFetcher.maybeThrowError(); - } else { + if (sampleStreamWrappers != null) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.maybeThrowPrepareError(); } @@ -255,7 +251,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void onPlaylistRefreshRequired(HlsUrl url) { - playlistTracker.refreshPlaylist(url, this); + playlistTracker.refreshPlaylist(url); } @Override @@ -271,22 +267,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void onPlaylistChanged() { - if (trackGroups != null) { - callback.onContinueLoadingRequested(this); - } else { - // Some of the wrappers were waiting for their media playlist to prepare. - for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { - wrapper.continuePreparing(); - } - } + continuePreparingOrLoading(); } @Override - public void onPlaylistLoadError(HlsUrl url, IOException error) { - for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { - sampleStreamWrapper.onPlaylistLoadError(url, error); + public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { + for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) { + streamWrapper.onPlaylistBlacklisted(url, blacklistMs); } - callback.onContinueLoadingRequested(this); + continuePreparingOrLoading(); } // Internal methods. @@ -363,6 +352,17 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper eventDispatcher); } + private void continuePreparingOrLoading() { + if (trackGroups != null) { + callback.onContinueLoadingRequested(this); + } else { + // Some of the wrappers were waiting for their media playlist to prepare. + for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { + wrapper.continuePreparing(); + } + } + } + private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) { String codecs = variant.format.codecs; if (TextUtils.isEmpty(codecs)) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 4833e4a08d..2f46fc694c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -77,7 +77,7 @@ public final class HlsMediaSource implements MediaSource, @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - playlistTracker.maybeThrowPrimaryPlaylistRefreshError(); + playlistTracker.maybeThrowPlaylistRefreshError(); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index bc44c84e39..a9bbddb69c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -279,8 +279,8 @@ import java.util.LinkedList; chunkSource.setIsTimestampMaster(isTimestampMaster); } - public void onPlaylistLoadError(HlsUrl url, IOException error) { - chunkSource.onPlaylistLoadError(url, error); + public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { + chunkSource.onPlaylistBlacklisted(url, blacklistMs); } // SampleStream implementation. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 41ea2a03b9..fc70ec6de1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -68,19 +68,21 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final long startTimeUs; public final int mediaSequence; public final int version; - public final Segment initializationSegment; - public final List segments; + public final long targetDurationUs; public final boolean hasEndTag; public final boolean hasProgramDateTime; + public final Segment initializationSegment; + public final List segments; public final long durationUs; - public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence, int version, - boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment, - List segments) { + public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence, + int version, long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime, + Segment initializationSegment, List segments) { super(baseUri, HlsPlaylist.TYPE_MEDIA); this.startTimeUs = startTimeUs; this.mediaSequence = mediaSequence; this.version = version; + this.targetDurationUs = targetDurationUs; this.hasEndTag = hasEndTag; this.hasProgramDateTime = hasProgramDateTime; this.initializationSegment = initializationSegment; @@ -105,8 +107,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) { - return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, hasEndTag, - hasProgramDateTime, initializationSegment, segments); + return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, targetDurationUs, + hasEndTag, hasProgramDateTime, initializationSegment, segments); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 3829cbadbf..1932caccf7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -67,6 +67,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); @@ -239,6 +242,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser playlistBundles; private final Handler playlistRefreshHandler; private final PrimaryPlaylistListener primaryPlaylistListener; + private final List listeners; private final Loader initialPlaylistLoader; private final EventDispatcher eventDispatcher; private HlsMasterPlaylist masterPlaylist; private HlsUrl primaryHlsUrl; + private HlsMediaPlaylist primaryUrlSnapshot; private boolean isLive; /** @@ -113,12 +119,31 @@ public final class HlsPlaylistTracker implements Loader.Callback(); initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); playlistParser = new HlsPlaylistParser(); playlistBundles = new IdentityHashMap<>(); playlistRefreshHandler = new Handler(); } + /** + * Registers a listener to receive events from the playlist tracker. + * + * @param listener The listener. + */ + public void addListener(PlaylistEventListener listener) { + listeners.add(listener); + } + + /** + * Unregisters a listener. + * + * @param listener The listener to unregister. + */ + public void removeListener(PlaylistEventListener listener) { + listeners.remove(listener); + } + /** * Starts tracking all the playlists related to the provided Uri. */ @@ -147,7 +172,7 @@ public final class HlsPlaylistTracker implements Loader.Callback variants = masterPlaylist.variants; + int variantsSize = variants.size(); + long currentTimeMs = SystemClock.elapsedRealtime(); + for (int i = 0; i < variantsSize; i++) { + MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i)); + if (currentTimeMs > bundle.blacklistUntilMs) { + primaryHlsUrl = bundle.playlistUrl; + bundle.loadPlaylist(); + return true; + } + } + return false; + } + + private void maybeSetPrimaryUrl(HlsUrl url) { + if (!masterPlaylist.variants.contains(url)) { + // Only allow variant urls to be chosen as primary. + return; + } + MediaPlaylistBundle currentPrimaryBundle = playlistBundles.get(primaryHlsUrl); + long primarySnapshotAccessAgeMs = + currentPrimaryBundle.lastSnapshotAccessTimeMs - SystemClock.elapsedRealtime(); + if (primarySnapshotAccessAgeMs > PRIMARY_URL_KEEPALIVE_MS) { + primaryHlsUrl = url; + playlistBundles.get(primaryHlsUrl).loadPlaylist(); + } + } + private void createBundles(List urls) { int listSize = urls.size(); + long currentTimeMs = SystemClock.elapsedRealtime(); for (int i = 0; i < listSize; i++) { HlsUrl url = urls.get(i); - MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs); playlistBundles.put(urls.get(i), bundle); } } @@ -271,20 +323,30 @@ public final class HlsPlaylistTracker implements Loader.Callback oldSegments = oldPlaylist.segments; @@ -324,7 +387,7 @@ public final class HlsPlaylistTracker implements Loader.Callback mediaPlaylistLoadable; - private PlaylistRefreshCallback callback; - private HlsMediaPlaylist latestPlaylistSnapshot; + private HlsMediaPlaylist playlistSnapshot; + private long lastSnapshotAccessTimeMs; + private long blacklistUntilMs; - public MediaPlaylistBundle(HlsUrl playlistUrl) { - this(playlistUrl, null); - } - - public MediaPlaylistBundle(HlsUrl playlistUrl, HlsMediaPlaylist initialSnapshot) { + public MediaPlaylistBundle(HlsUrl playlistUrl, long initialLastSnapshotAccessTimeMs) { this.playlistUrl = playlistUrl; - latestPlaylistSnapshot = initialSnapshot; + lastSnapshotAccessTimeMs = initialLastSnapshotAccessTimeMs; mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); mediaPlaylistLoadable = new ParsingLoadable<>(dataSourceFactory.createDataSource(), UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, playlistParser); } + public HlsMediaPlaylist getPlaylistSnapshot() { + lastSnapshotAccessTimeMs = SystemClock.elapsedRealtime(); + return playlistSnapshot; + } + public void release() { mediaPlaylistLoader.release(); } public void loadPlaylist() { + blacklistUntilMs = 0; if (!mediaPlaylistLoader.isLoading()) { mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); } } - public void setCallback(PlaylistRefreshCallback callback) { - this.callback = callback; - } - - public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedStartTimeUs) { - int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence; - if (latestPlaylistSnapshot.hasProgramDateTime || indexOfChunk < 0) { + public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedChunkStartTimeUs) { + int indexOfChunk = chunkMediaSequence - playlistSnapshot.mediaSequence; + if (playlistSnapshot.hasProgramDateTime || indexOfChunk < 0) { return; } - Segment actualSegment = latestPlaylistSnapshot.segments.get(indexOfChunk); + Segment actualSegment = playlistSnapshot.segments.get(indexOfChunk); long segmentAbsoluteStartTimeUs = - actualSegment.relativeStartTimeUs + latestPlaylistSnapshot.startTimeUs; - long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - adjustedStartTimeUs); + actualSegment.relativeStartTimeUs + playlistSnapshot.startTimeUs; + long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - adjustedChunkStartTimeUs); if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) { return; } - latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithStartTimeUs( - adjustedStartTimeUs - actualSegment.relativeStartTimeUs); + playlistSnapshot = playlistSnapshot.copyWithStartTimeUs( + adjustedChunkStartTimeUs - actualSegment.relativeStartTimeUs); } // Loader.Callback implementation. @@ -403,18 +465,21 @@ public final class HlsPlaylistTracker implements Loader.Callback loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - // TODO: Change primary playlist if this is the primary playlist bundle. boolean isFatal = error instanceof ParserException; eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, isFatal); - if (callback != null) { - callback.onPlaylistLoadError(playlistUrl, error); - } if (isFatal) { return Loader.DONT_RETRY_FATAL; - } else { - return primaryHlsUrl == playlistUrl ? Loader.RETRY : Loader.DONT_RETRY; } + boolean shouldRetry = true; + if (ChunkedTrackBlacklistUtil.shouldBlacklist(error)) { + blacklistUntilMs = + SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; + notifyPlaylistBlacklisting(playlistUrl, + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS); + shouldRetry = primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); + } + return shouldRetry ? Loader.RETRY : Loader.DONT_RETRY; } // Runnable implementation. @@ -427,21 +492,19 @@ public final class HlsPlaylistTracker implements Loader.Callback