diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4880c262d9..2c3e1c78b0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,7 +6,9 @@ extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)). * DASH: Fix playback getting stuck when playing representations that have both sidx atoms and non-zero presentationTimeOffset values. -* HLS: Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags. +* HLS: + * Allow injection of custom playlist trackers. + * Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags. * Mitigate memory leaks when `MediaSource` loads are slow to cancel ([#4249](https://github.com/google/ExoPlayer/issues/4249)). * Fix inconsistent `Player.EventListener` invocations for recursive player state diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 01bb36f6ce..e0c805e1af 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat 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.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; @@ -58,6 +59,7 @@ public final class HlsMediaSource extends BaseMediaSource private HlsExtractorFactory extractorFactory; private @Nullable ParsingLoadable.Parser playlistParser; + private @Nullable HlsPlaylistTracker playlistTracker; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int minLoadableRetryCount; private boolean allowChunklessPreparation; @@ -136,16 +138,37 @@ public final class HlsMediaSource extends BaseMediaSource * Sets the parser to parse HLS playlists. The default is an instance of {@link * HlsPlaylistParser}. * + *

Must not be called after calling {@link #setPlaylistTracker} on the same builder. + * * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setPlaylistParser(ParsingLoadable.Parser playlistParser) { Assertions.checkState(!isCreateCalled); + Assertions.checkState(playlistTracker == null, "A playlist tracker has already been set."); this.playlistParser = Assertions.checkNotNull(playlistParser); return this; } + /** + * Sets the HLS playlist tracker. The default is an instance of {@link + * DefaultHlsPlaylistTracker}. Playlist trackers must not be shared by {@link HlsMediaSource} + * instances. + * + *

Must not be called after calling {@link #setPlaylistParser} on the same builder. + * + * @param playlistTracker A tracker for HLS playlists. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setPlaylistTracker(HlsPlaylistTracker playlistTracker) { + Assertions.checkState(!isCreateCalled); + Assertions.checkState(playlistParser == null, "A playlist parser has already been set."); + this.playlistTracker = Assertions.checkNotNull(playlistTracker); + return this; + } + /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source * loads data from multiple streams (video, audio etc...). The default is an instance of {@link @@ -187,8 +210,12 @@ public final class HlsMediaSource extends BaseMediaSource @Override public HlsMediaSource createMediaSource(Uri playlistUri) { isCreateCalled = true; - if (playlistParser == null) { - playlistParser = new HlsPlaylistParser(); + if (playlistTracker == null) { + playlistTracker = + new DefaultHlsPlaylistTracker( + hlsDataSourceFactory, + minLoadableRetryCount, + playlistParser != null ? playlistParser : new HlsPlaylistParser()); } return new HlsMediaSource( playlistUri, @@ -196,7 +223,7 @@ public final class HlsMediaSource extends BaseMediaSource extractorFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, - playlistParser, + playlistTracker, allowChunklessPreparation, tag); } @@ -233,12 +260,10 @@ public final class HlsMediaSource extends BaseMediaSource private final HlsDataSourceFactory dataSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; - private final ParsingLoadable.Parser playlistParser; private final boolean allowChunklessPreparation; + private final HlsPlaylistTracker playlistTracker; private final @Nullable Object tag; - private HlsPlaylistTracker playlistTracker; - /** * @param manifestUri The {@link Uri} of the HLS manifest. * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, @@ -276,8 +301,13 @@ public final class HlsMediaSource extends BaseMediaSource int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), - HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, + this( + manifestUri, + new DefaultHlsDataSourceFactory(dataSourceFactory), + HlsExtractorFactory.DEFAULT, + minLoadableRetryCount, + eventHandler, + eventListener, new HlsPlaylistParser()); } @@ -309,7 +339,8 @@ public final class HlsMediaSource extends BaseMediaSource extractorFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, - playlistParser, + new DefaultHlsPlaylistTracker( + dataSourceFactory, minLoadableRetryCount, new HlsPlaylistParser()), /* allowChunklessPreparation= */ false, /* tag= */ null); if (eventHandler != null && eventListener != null) { @@ -323,7 +354,7 @@ public final class HlsMediaSource extends BaseMediaSource HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, int minLoadableRetryCount, - ParsingLoadable.Parser playlistParser, + HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, @Nullable Object tag) { this.manifestUri = manifestUri; @@ -331,7 +362,7 @@ public final class HlsMediaSource extends BaseMediaSource this.extractorFactory = extractorFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.minLoadableRetryCount = minLoadableRetryCount; - this.playlistParser = playlistParser; + this.playlistTracker = playlistTracker; this.allowChunklessPreparation = allowChunklessPreparation; this.tag = tag; } @@ -339,9 +370,7 @@ public final class HlsMediaSource extends BaseMediaSource @Override public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); - playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, - minLoadableRetryCount, this, playlistParser); - playlistTracker.start(); + playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this); } @Override @@ -373,7 +402,6 @@ public final class HlsMediaSource extends BaseMediaSource public void releaseSourceInternal() { if (playlistTracker != null) { playlistTracker.release(); - playlistTracker = null; } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java new file mode 100644 index 0000000000..629c1eb59c --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -0,0 +1,566 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; +import android.os.Handler; +import android.os.SystemClock; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; +import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.UriUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; + +/** Default implementation for {@link HlsPlaylistTracker}. */ +public final class DefaultHlsPlaylistTracker + implements HlsPlaylistTracker, Loader.Callback> { + + /** + * Coefficient applied on the target duration of a playlist to determine the amount of time after + * which an unchanging playlist is considered stuck. + */ + private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5; + + private final HlsDataSourceFactory dataSourceFactory; + private final ParsingLoadable.Parser playlistParser; + private final int minRetryCount; + private final IdentityHashMap playlistBundles; + private final List listeners; + private final Loader initialPlaylistLoader; + + private Handler playlistRefreshHandler; + private EventDispatcher eventDispatcher; + private PrimaryPlaylistListener primaryPlaylistListener; + private HlsMasterPlaylist masterPlaylist; + private HlsUrl primaryHlsUrl; + private HlsMediaPlaylist primaryUrlSnapshot; + private boolean isLive; + private long initialStartTimeUs; + + /** + * @param dataSourceFactory A factory for {@link DataSource} instances. + * @param minRetryCount The minimum number of times loads must be retried before {@link + * #maybeThrowPlaylistRefreshError(HlsUrl)} and {@link + * #maybeThrowPrimaryPlaylistRefreshError()} propagate any loading errors. + * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + */ + public DefaultHlsPlaylistTracker( + HlsDataSourceFactory dataSourceFactory, + int minRetryCount, + ParsingLoadable.Parser playlistParser) { + this.dataSourceFactory = dataSourceFactory; + this.minRetryCount = minRetryCount; + this.playlistParser = playlistParser; + listeners = new ArrayList<>(); + initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist"); + playlistBundles = new IdentityHashMap<>(); + initialStartTimeUs = C.TIME_UNSET; + } + + // HlsPlaylistTracker implementation. + + @Override + public void start( + Uri initialPlaylistUri, + EventDispatcher eventDispatcher, + PrimaryPlaylistListener primaryPlaylistListener) { + this.playlistRefreshHandler = new Handler(); + this.eventDispatcher = eventDispatcher; + this.primaryPlaylistListener = primaryPlaylistListener; + ParsingLoadable masterPlaylistLoadable = + new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), + initialPlaylistUri, + C.DATA_TYPE_MANIFEST, + playlistParser); + initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); + } + + @Override + public void release() { + initialPlaylistLoader.release(); + for (MediaPlaylistBundle bundle : playlistBundles.values()) { + bundle.release(); + } + playlistRefreshHandler.removeCallbacksAndMessages(null); + playlistBundles.clear(); + } + + @Override + public void addListener(PlaylistEventListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(PlaylistEventListener listener) { + listeners.remove(listener); + } + + @Override + public HlsMasterPlaylist getMasterPlaylist() { + return masterPlaylist; + } + + @Override + public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { + HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); + if (snapshot != null) { + maybeSetPrimaryUrl(url); + } + return snapshot; + } + + @Override + public long getInitialStartTimeUs() { + return initialStartTimeUs; + } + + @Override + public boolean isSnapshotValid(HlsUrl url) { + return playlistBundles.get(url).isSnapshotValid(); + } + + @Override + public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { + initialPlaylistLoader.maybeThrowError(); + if (primaryHlsUrl != null) { + maybeThrowPlaylistRefreshError(primaryHlsUrl); + } + } + + @Override + public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException { + playlistBundles.get(url).maybeThrowPlaylistRefreshError(); + } + + @Override + public void refreshPlaylist(HlsUrl url) { + playlistBundles.get(url).loadPlaylist(); + } + + @Override + public boolean isLive() { + return isLive; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + HlsMasterPlaylist masterPlaylist; + boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; + if (isMediaPlaylist) { + masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); + } else /* result instanceof HlsMasterPlaylist */ { + masterPlaylist = (HlsMasterPlaylist) result; + } + this.masterPlaylist = masterPlaylist; + primaryHlsUrl = masterPlaylist.variants.get(0); + ArrayList urls = new ArrayList<>(); + urls.addAll(masterPlaylist.variants); + urls.addAll(masterPlaylist.audios); + urls.addAll(masterPlaylist.subtitles); + createBundles(urls); + MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); + if (isMediaPlaylist) { + // We don't need to load the playlist again. We can use the same result. + primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result); + } else { + primaryBundle.loadPlaylist(); + } + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public @Loader.RetryAction int onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + isFatal); + return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + // Internal methods. + + private boolean maybeSelectNewPrimaryUrl() { + List 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 (url == primaryHlsUrl + || !masterPlaylist.variants.contains(url) + || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { + // Ignore if the primary url is unchanged, if the url is not a variant url, or if the last + // primary snapshot contains an end tag. + return; + } + primaryHlsUrl = url; + playlistBundles.get(primaryHlsUrl).loadPlaylist(); + } + + private void createBundles(List urls) { + int listSize = urls.size(); + for (int i = 0; i < listSize; i++) { + HlsUrl url = urls.get(i); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); + playlistBundles.put(url, bundle); + } + } + + /** + * Called by the bundles when a snapshot changes. + * + * @param url The url of the playlist. + * @param newSnapshot The new snapshot. + */ + private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) { + if (url == primaryHlsUrl) { + if (primaryUrlSnapshot == null) { + // This is the first primary url snapshot. + isLive = !newSnapshot.hasEndTag; + initialStartTimeUs = newSnapshot.startTimeUs; + } + primaryUrlSnapshot = newSnapshot; + primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); + } + int listenersSize = listeners.size(); + for (int i = 0; i < listenersSize; i++) { + listeners.get(i).onPlaylistChanged(); + } + } + + private boolean notifyPlaylistError(HlsUrl playlistUrl, boolean shouldBlacklist) { + int listenersSize = listeners.size(); + boolean anyBlacklistingFailed = false; + for (int i = 0; i < listenersSize; i++) { + anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, shouldBlacklist); + } + return anyBlacklistingFailed; + } + + private HlsMediaPlaylist getLatestPlaylistSnapshot( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (!loadedPlaylist.isNewerThan(oldPlaylist)) { + if (loadedPlaylist.hasEndTag) { + // If the loaded playlist has an end tag but is not newer than the old playlist then we have + // an inconsistent state. This is typically caused by the server incorrectly resetting the + // media sequence when appending the end tag. We resolve this case as best we can by + // returning the old playlist with the end tag appended. + return oldPlaylist.copyWithEndTag(); + } else { + return oldPlaylist; + } + } + long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); + int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); + return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); + } + + private long getLoadedPlaylistStartTimeUs( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasProgramDateTime) { + return loadedPlaylist.startTimeUs; + } + long primarySnapshotStartTimeUs = + primaryUrlSnapshot != null ? primaryUrlSnapshot.startTimeUs : 0; + if (oldPlaylist == null) { + return primarySnapshotStartTimeUs; + } + int oldPlaylistSize = oldPlaylist.segments.size(); + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; + } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { + return oldPlaylist.getEndTimeUs(); + } else { + // No segments overlap, we assume the new playlist start coincides with the primary playlist. + return primarySnapshotStartTimeUs; + } + } + + private int getLoadedPlaylistDiscontinuitySequence( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasDiscontinuitySequence) { + return loadedPlaylist.discontinuitySequence; + } + // TODO: Improve cross-playlist discontinuity adjustment. + int primaryUrlDiscontinuitySequence = + primaryUrlSnapshot != null ? primaryUrlSnapshot.discontinuitySequence : 0; + if (oldPlaylist == null) { + return primaryUrlDiscontinuitySequence; + } + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.discontinuitySequence + + firstOldOverlappingSegment.relativeDiscontinuitySequence + - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; + } + return primaryUrlDiscontinuitySequence; + } + + private static Segment getFirstOldOverlappingSegment( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence); + List oldSegments = oldPlaylist.segments; + return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; + } + + /** Holds all information related to a specific Media Playlist. */ + private final class MediaPlaylistBundle + implements Loader.Callback>, Runnable { + + private final HlsUrl playlistUrl; + private final Loader mediaPlaylistLoader; + private final ParsingLoadable mediaPlaylistLoadable; + + private HlsMediaPlaylist playlistSnapshot; + private long lastSnapshotLoadMs; + private long lastSnapshotChangeMs; + private long earliestNextLoadTimeMs; + private long blacklistUntilMs; + private boolean loadPending; + private IOException playlistError; + + public MediaPlaylistBundle(HlsUrl playlistUrl) { + this.playlistUrl = playlistUrl; + mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist"); + mediaPlaylistLoadable = + new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), + UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), + C.DATA_TYPE_MANIFEST, + playlistParser); + } + + public HlsMediaPlaylist getPlaylistSnapshot() { + return playlistSnapshot; + } + + public boolean isSnapshotValid() { + if (playlistSnapshot == null) { + return false; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); + return playlistSnapshot.hasEndTag + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD + || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; + } + + public void release() { + mediaPlaylistLoader.release(); + } + + public void loadPlaylist() { + blacklistUntilMs = 0; + if (loadPending || mediaPlaylistLoader.isLoading()) { + // Load already pending or in progress. Do nothing. + return; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + if (currentTimeMs < earliestNextLoadTimeMs) { + loadPending = true; + playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs); + } else { + loadPlaylistImmediately(); + } + } + + public void maybeThrowPlaylistRefreshError() throws IOException { + mediaPlaylistLoader.maybeThrowError(); + if (playlistError != null) { + throw playlistError; + } + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + if (result instanceof HlsMediaPlaylist) { + processLoadedPlaylist((HlsMediaPlaylist) result); + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } else { + playlistError = new ParserException("Loaded playlist has unexpected type."); + } + } + + @Override + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public @Loader.RetryAction int onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + isFatal); + boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error); + boolean shouldRetryIfNotFatal = + notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist; + if (isFatal) { + return Loader.DONT_RETRY_FATAL; + } + if (shouldBlacklist) { + shouldRetryIfNotFatal |= blacklistPlaylist(); + } + return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY; + } + + // Runnable implementation. + + @Override + public void run() { + loadPending = false; + loadPlaylistImmediately(); + } + + // Internal methods. + + private void loadPlaylistImmediately() { + mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); + } + + private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) { + HlsMediaPlaylist oldPlaylist = playlistSnapshot; + long currentTimeMs = SystemClock.elapsedRealtime(); + lastSnapshotLoadMs = currentTimeMs; + playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); + if (playlistSnapshot != oldPlaylist) { + playlistError = null; + lastSnapshotChangeMs = currentTimeMs; + onPlaylistUpdated(playlistUrl, playlistSnapshot); + } else if (!playlistSnapshot.hasEndTag) { + if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() + < playlistSnapshot.mediaSequence) { + // The media sequence jumped backwards. The server has probably reset. + playlistError = new PlaylistResetException(playlistUrl.url); + notifyPlaylistError(playlistUrl, false); + } else if (currentTimeMs - lastSnapshotChangeMs + > C.usToMs(playlistSnapshot.targetDurationUs) + * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { + // The playlist seems to be stuck. Blacklist it. + playlistError = new PlaylistStuckException(playlistUrl.url); + notifyPlaylistError(playlistUrl, true); + blacklistPlaylist(); + } + } + // Do not allow the playlist to load again within the target duration if we obtained a new + // snapshot, or half the target duration otherwise. + earliestNextLoadTimeMs = + currentTimeMs + + C.usToMs( + playlistSnapshot != oldPlaylist + ? playlistSnapshot.targetDurationUs + : (playlistSnapshot.targetDurationUs / 2)); + // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the + // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes + // the primary. + if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) { + loadPlaylist(); + } + } + + /** + * Blacklists the playlist. + * + * @return Whether the playlist is the primary, despite being blacklisted. + */ + private boolean blacklistPlaylist() { + blacklistUntilMs = + SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; + return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); + } + } +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 9986f5b65b..febd1c217d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,66 +16,28 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; -import android.os.Handler; -import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; -import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.Loader; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.UriUtil; import java.io.IOException; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.List; /** - * Tracks playlists linked to a provided playlist url. The provided url might reference an HLS - * master playlist or a media playlist. + * Tracks playlists associated to an HLS stream and provides snapshots. + * + *

The playlist tracker is responsible for exposing the seeking window, which is defined by the + * segments that one of the playlists exposes. This playlist is called primary and needs to be + * periodically refreshed in the case of live streams. Note that the primary playlist is one of the + * media playlists while the master playlist is an optional kind of playlist defined by the HLS + * specification (RFC 8216). + * + *

Playlist loads might encounter errors. The tracker may choose to blacklist them to ensure a + * primary playlist is always available. */ -public final class HlsPlaylistTracker implements Loader.Callback> { +public interface HlsPlaylistTracker { - /** - * Thrown when a playlist is considered to be stuck due to a server side error. - */ - public static final class PlaylistStuckException extends IOException { - - /** - * The url of the stuck playlist. - */ - public final String url; - - private PlaylistStuckException(String url) { - this.url = url; - } - - } - - /** - * Thrown when the media sequence of a new snapshot indicates the server has reset. - */ - public static final class PlaylistResetException extends IOException { - - /** - * The url of the reset playlist. - */ - public final String url; - - private PlaylistResetException(String url) { - this.url = url; - } - - } - - /** - * Listener for primary playlist changes. - */ - public interface PrimaryPlaylistListener { + /** Listener for primary playlist changes. */ + interface PrimaryPlaylistListener { /** * Called when the primary playlist changes. @@ -85,10 +47,8 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser; - private final int minRetryCount; - private final IdentityHashMap playlistBundles; - private final Handler playlistRefreshHandler; - private final PrimaryPlaylistListener primaryPlaylistListener; - private final List listeners; - private final Loader initialPlaylistLoader; - private final EventDispatcher eventDispatcher; + /** The url of the stuck playlist. */ + public final String url; - private HlsMasterPlaylist masterPlaylist; - private HlsUrl primaryHlsUrl; - private HlsMediaPlaylist primaryUrlSnapshot; - private boolean isLive; - private long initialStartTimeUs; - - /** - * @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media - * playlist or a master playlist. - * @param dataSourceFactory A factory for {@link DataSource} instances. - * @param eventDispatcher A dispatcher to notify of events. - * @param minRetryCount The minimum number of times loads must be retried before - * {@link #maybeThrowPlaylistRefreshError(HlsUrl)} and - * {@link #maybeThrowPrimaryPlaylistRefreshError()} propagate any loading errors. - * @param primaryPlaylistListener A callback for the primary playlist change events. - */ - public HlsPlaylistTracker(Uri initialPlaylistUri, HlsDataSourceFactory dataSourceFactory, - EventDispatcher eventDispatcher, int minRetryCount, - PrimaryPlaylistListener primaryPlaylistListener, - ParsingLoadable.Parser playlistParser) { - this.initialPlaylistUri = initialPlaylistUri; - this.dataSourceFactory = dataSourceFactory; - this.eventDispatcher = eventDispatcher; - this.minRetryCount = minRetryCount; - this.primaryPlaylistListener = primaryPlaylistListener; - this.playlistParser = playlistParser; - listeners = new ArrayList<>(); - initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); - playlistBundles = new IdentityHashMap<>(); - playlistRefreshHandler = new Handler(); - initialStartTimeUs = C.TIME_UNSET; + /** + * Creates an instance. + * + * @param url See {@link #url}. + */ + public PlaylistStuckException(String url) { + this.url = url; + } } + /** Thrown when the media sequence of a new snapshot indicates the server has reset. */ + final class PlaylistResetException extends IOException { + + /** The url of the reset playlist. */ + public final String url; + + /** + * Creates an instance. + * + * @param url See {@link #url}. + */ + public PlaylistResetException(String url) { + this.url = url; + } + } + + /** + * Starts the playlist tracker. + * + *

Must be called from the playback thread. A tracker may be restarted after a {@link + * #release()} call. + * + * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master + * playlist. + * @param eventDispatcher A dispatcher to notify of events. + * @param listener A callback for the primary playlist change events. + */ + void start( + Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener); + + /** Releases all acquired resources. Must be called once per {@link #start} call. */ + void release(); + /** * Registers a listener to receive events from the playlist tracker. * * @param listener The listener. */ - public void addListener(PlaylistEventListener listener) { - listeners.add(listener); - } + void addListener(PlaylistEventListener 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. - */ - public void start() { - ParsingLoadable masterPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri, - C.DATA_TYPE_MANIFEST, playlistParser); - initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); - } + void removeListener(PlaylistEventListener listener); /** * Returns the master playlist. * + *

If the uri passed to {@link #start} points to a media playlist, an {@link HlsMasterPlaylist} + * with a single variant for said media playlist is returned. + * * @return The master playlist. Null if the initial playlist has yet to be loaded. */ - public HlsMasterPlaylist getMasterPlaylist() { - return masterPlaylist; - } + @Nullable + HlsMasterPlaylist getMasterPlaylist(); /** - * Returns the most recent snapshot available of the playlist referenced by the provided - * {@link HlsUrl}. + * Returns the most recent snapshot available of the playlist referenced by the provided {@link + * HlsUrl}. * * @param url The {@link HlsUrl} corresponding to the requested media playlist. * @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May * be null if no snapshot has been loaded yet. */ - public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { - HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); - if (snapshot != null) { - maybeSetPrimaryUrl(url); - } - return snapshot; - } + @Nullable + HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url); /** * Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no * media playlist has been loaded. */ - public long getInitialStartTimeUs() { - return initialStartTimeUs; - } + long getInitialStartTimeUs(); /** * Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is * valid, meaning all the segments referenced by the playlist are expected to be available. If the * playlist is not valid then some of the segments may no longer be available. - + * * @param url The {@link HlsUrl}. * @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is * valid. */ - public boolean isSnapshotValid(HlsUrl url) { - return playlistBundles.get(url).isSnapshotValid(); - } - - /** - * Releases the playlist tracker. - */ - public void release() { - initialPlaylistLoader.release(); - for (MediaPlaylistBundle bundle : playlistBundles.values()) { - bundle.release(); - } - playlistRefreshHandler.removeCallbacksAndMessages(null); - playlistBundles.clear(); - } + boolean isSnapshotValid(HlsUrl url); /** * If the tracker is having trouble refreshing the master playlist or the primary playlist, this @@ -247,401 +173,31 @@ public final class HlsPlaylistTracker implements Loader.CallbackThe playlist tracker may choose the delay the playlist refresh. The request is discarded if + * a refresh was already pending. * * @param url The {@link HlsUrl} of the playlist to be refreshed. */ - public void refreshPlaylist(HlsUrl url) { - playlistBundles.get(url).loadPlaylist(); - } + void refreshPlaylist(HlsUrl url); /** - * Returns whether this is live content. + * Returns whether the tracked playlists describe a live stream. * * @return True if the content is live. False otherwise. */ - public boolean isLive() { - return isLive; - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - HlsPlaylist result = loadable.getResult(); - HlsMasterPlaylist masterPlaylist; - boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; - if (isMediaPlaylist) { - masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); - } else /* result instanceof HlsMasterPlaylist */ { - masterPlaylist = (HlsMasterPlaylist) result; - } - this.masterPlaylist = masterPlaylist; - primaryHlsUrl = masterPlaylist.variants.get(0); - ArrayList urls = new ArrayList<>(); - urls.addAll(masterPlaylist.variants); - urls.addAll(masterPlaylist.audios); - urls.addAll(masterPlaylist.subtitles); - createBundles(urls); - MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); - if (isMediaPlaylist) { - // We don't need to load the playlist again. We can use the same result. - primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result); - } else { - primaryBundle.loadPlaylist(); - } - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public @Loader.RetryAction int onLoadError( - ParsingLoadable loadable, - long elapsedRealtimeMs, - long loadDurationMs, - IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded(), error, isFatal); - return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; - } - - // Internal methods. - - private boolean maybeSelectNewPrimaryUrl() { - List 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 (url == primaryHlsUrl - || !masterPlaylist.variants.contains(url) - || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { - // Ignore if the primary url is unchanged, if the url is not a variant url, or if the last - // primary snapshot contains an end tag. - return; - } - primaryHlsUrl = url; - playlistBundles.get(primaryHlsUrl).loadPlaylist(); - } - - private void createBundles(List urls) { - int listSize = urls.size(); - for (int i = 0; i < listSize; i++) { - HlsUrl url = urls.get(i); - MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); - playlistBundles.put(url, bundle); - } - } - - /** - * Called by the bundles when a snapshot changes. - * - * @param url The url of the playlist. - * @param newSnapshot The new snapshot. - */ - private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) { - if (url == primaryHlsUrl) { - if (primaryUrlSnapshot == null) { - // This is the first primary url snapshot. - isLive = !newSnapshot.hasEndTag; - initialStartTimeUs = newSnapshot.startTimeUs; - } - primaryUrlSnapshot = newSnapshot; - primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); - } - int listenersSize = listeners.size(); - for (int i = 0; i < listenersSize; i++) { - listeners.get(i).onPlaylistChanged(); - } - } - - private boolean notifyPlaylistError(HlsUrl playlistUrl, boolean shouldBlacklist) { - int listenersSize = listeners.size(); - boolean anyBlacklistingFailed = false; - for (int i = 0; i < listenersSize; i++) { - anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, shouldBlacklist); - } - return anyBlacklistingFailed; - } - - private HlsMediaPlaylist getLatestPlaylistSnapshot(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (!loadedPlaylist.isNewerThan(oldPlaylist)) { - if (loadedPlaylist.hasEndTag) { - // If the loaded playlist has an end tag but is not newer than the old playlist then we have - // an inconsistent state. This is typically caused by the server incorrectly resetting the - // media sequence when appending the end tag. We resolve this case as best we can by - // returning the old playlist with the end tag appended. - return oldPlaylist.copyWithEndTag(); - } else { - return oldPlaylist; - } - } - long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); - int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); - return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); - } - - private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (loadedPlaylist.hasProgramDateTime) { - return loadedPlaylist.startTimeUs; - } - long primarySnapshotStartTimeUs = primaryUrlSnapshot != null - ? primaryUrlSnapshot.startTimeUs : 0; - if (oldPlaylist == null) { - return primarySnapshotStartTimeUs; - } - int oldPlaylistSize = oldPlaylist.segments.size(); - Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); - if (firstOldOverlappingSegment != null) { - return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; - } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { - return oldPlaylist.getEndTimeUs(); - } else { - // No segments overlap, we assume the new playlist start coincides with the primary playlist. - return primarySnapshotStartTimeUs; - } - } - - private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (loadedPlaylist.hasDiscontinuitySequence) { - return loadedPlaylist.discontinuitySequence; - } - // TODO: Improve cross-playlist discontinuity adjustment. - int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null - ? primaryUrlSnapshot.discontinuitySequence : 0; - if (oldPlaylist == null) { - return primaryUrlDiscontinuitySequence; - } - Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); - if (firstOldOverlappingSegment != null) { - return oldPlaylist.discontinuitySequence - + firstOldOverlappingSegment.relativeDiscontinuitySequence - - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; - } - return primaryUrlDiscontinuitySequence; - } - - private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence); - List oldSegments = oldPlaylist.segments; - return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; - } - - /** - * Holds all information related to a specific Media Playlist. - */ - private final class MediaPlaylistBundle implements Loader.Callback>, - Runnable { - - private final HlsUrl playlistUrl; - private final Loader mediaPlaylistLoader; - private final ParsingLoadable mediaPlaylistLoadable; - - private HlsMediaPlaylist playlistSnapshot; - private long lastSnapshotLoadMs; - private long lastSnapshotChangeMs; - private long earliestNextLoadTimeMs; - private long blacklistUntilMs; - private boolean loadPending; - private IOException playlistError; - - public MediaPlaylistBundle(HlsUrl playlistUrl) { - this.playlistUrl = playlistUrl; - mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); - mediaPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), - UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, - playlistParser); - } - - public HlsMediaPlaylist getPlaylistSnapshot() { - return playlistSnapshot; - } - - public boolean isSnapshotValid() { - if (playlistSnapshot == null) { - return false; - } - long currentTimeMs = SystemClock.elapsedRealtime(); - long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); - return playlistSnapshot.hasEndTag - || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT - || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD - || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; - } - - public void release() { - mediaPlaylistLoader.release(); - } - - public void loadPlaylist() { - blacklistUntilMs = 0; - if (loadPending || mediaPlaylistLoader.isLoading()) { - // Load already pending or in progress. Do nothing. - return; - } - long currentTimeMs = SystemClock.elapsedRealtime(); - if (currentTimeMs < earliestNextLoadTimeMs) { - loadPending = true; - playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs); - } else { - loadPlaylistImmediately(); - } - } - - public void maybeThrowPlaylistRefreshError() throws IOException { - mediaPlaylistLoader.maybeThrowError(); - if (playlistError != null) { - throw playlistError; - } - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - HlsPlaylist result = loadable.getResult(); - if (result instanceof HlsMediaPlaylist) { - processLoadedPlaylist((HlsMediaPlaylist) result); - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } else { - playlistError = new ParserException("Loaded playlist has unexpected type."); - } - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public @Loader.RetryAction int onLoadError( - ParsingLoadable loadable, - long elapsedRealtimeMs, - long loadDurationMs, - IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded(), error, isFatal); - boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error); - boolean shouldRetryIfNotFatal = - notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist; - if (isFatal) { - return Loader.DONT_RETRY_FATAL; - } - if (shouldBlacklist) { - shouldRetryIfNotFatal |= blacklistPlaylist(); - } - return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY; - } - - // Runnable implementation. - - @Override - public void run() { - loadPending = false; - loadPlaylistImmediately(); - } - - // Internal methods. - - private void loadPlaylistImmediately() { - mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); - } - - private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) { - HlsMediaPlaylist oldPlaylist = playlistSnapshot; - long currentTimeMs = SystemClock.elapsedRealtime(); - lastSnapshotLoadMs = currentTimeMs; - playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); - if (playlistSnapshot != oldPlaylist) { - playlistError = null; - lastSnapshotChangeMs = currentTimeMs; - onPlaylistUpdated(playlistUrl, playlistSnapshot); - } else if (!playlistSnapshot.hasEndTag) { - if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() - < playlistSnapshot.mediaSequence) { - // The media sequence jumped backwards. The server has probably reset. - playlistError = new PlaylistResetException(playlistUrl.url); - notifyPlaylistError(playlistUrl, false); - } else if (currentTimeMs - lastSnapshotChangeMs - > C.usToMs(playlistSnapshot.targetDurationUs) - * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { - // The playlist seems to be stuck. Blacklist it. - playlistError = new PlaylistStuckException(playlistUrl.url); - notifyPlaylistError(playlistUrl, true); - blacklistPlaylist(); - } - } - // Do not allow the playlist to load again within the target duration if we obtained a new - // snapshot, or half the target duration otherwise. - earliestNextLoadTimeMs = currentTimeMs + C.usToMs(playlistSnapshot != oldPlaylist - ? playlistSnapshot.targetDurationUs : (playlistSnapshot.targetDurationUs / 2)); - // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the - // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes - // the primary. - if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) { - loadPlaylist(); - } - } - - /** - * Blacklists the playlist. - * - * @return Whether the playlist is the primary, despite being blacklisted. - */ - private boolean blacklistPlaylist() { - blacklistUntilMs = SystemClock.elapsedRealtime() - + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; - return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); - } - - } - + boolean isLive(); }