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/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java
index 37aa181970..7fe03f6cb3 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java
@@ -73,6 +73,7 @@ public final class HlsDownloadHelper extends DownloadHelper {
public TrackGroupArray getTrackGroups(int periodIndex) {
Assertions.checkNotNull(playlist);
if (playlist instanceof HlsMediaPlaylist) {
+ renditionTypes = new int[0];
return TrackGroupArray.EMPTY;
}
// TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction.
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..014a302de7
--- /dev/null
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java
@@ -0,0 +1,582 @@
+/*
+ * 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.Assertions;
+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 EventDispatcher eventDispatcher;
+ private Loader initialPlaylistLoader;
+ private Handler playlistRefreshHandler;
+ 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<>();
+ 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);
+ Assertions.checkState(initialPlaylistLoader == null);
+ initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist");
+ long elapsedRealtime =
+ initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount);
+ eventDispatcher.loadStarted(
+ masterPlaylistLoadable.dataSpec, masterPlaylistLoadable.type, elapsedRealtime);
+ }
+
+ @Override
+ public void release() {
+ primaryHlsUrl = null;
+ primaryUrlSnapshot = null;
+ masterPlaylist = null;
+ initialStartTimeUs = C.TIME_UNSET;
+ initialPlaylistLoader.release();
+ initialPlaylistLoader = null;
+ for (MediaPlaylistBundle bundle : playlistBundles.values()) {
+ bundle.release();
+ }
+ playlistRefreshHandler.removeCallbacksAndMessages(null);
+ playlistRefreshHandler = 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 {
+ if (initialPlaylistLoader != null) {
+ 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() {
+ long elapsedRealtime =
+ mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount);
+ eventDispatcher.loadStarted(
+ mediaPlaylistLoadable.dataSpec, mediaPlaylistLoadable.type, elapsedRealtime);
+ }
+
+ 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();
}
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
index 9a0d57ff31..8e7c3e38c9 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.smoothstreaming;
+import android.support.annotation.Nullable;
import android.util.Base64;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SeekParameters;
@@ -52,7 +53,7 @@ import java.util.ArrayList;
private final TrackEncryptionBox[] trackEncryptionBoxes;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
- private Callback callback;
+ private @Nullable Callback callback;
private SsManifest manifest;
private ChunkSampleStream[] sampleStreams;
private SequenceableLoader compositeSequenceableLoader;
@@ -98,6 +99,7 @@ import java.util.ArrayList;
for (ChunkSampleStream sampleStream : sampleStreams) {
sampleStream.release();
}
+ callback = null;
eventDispatcher.mediaPeriodReleased();
}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
index 4dbd4d5fec..bb9c38d886 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
@@ -251,7 +251,7 @@ public final class SubtitleView extends View implements TextOutput {
// Calculate the bounds after padding is taken into account.
int left = getLeft() + getPaddingLeft();
int top = rawTop + getPaddingTop();
- int right = getRight() + getPaddingRight();
+ int right = getRight() - getPaddingRight();
int bottom = rawBottom - getPaddingBottom();
if (bottom <= top || right <= left) {
// No space to draw subtitles.
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java
index be0babf5a8..fe5d5cbbc5 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java
@@ -203,7 +203,9 @@ public class TrackSelectionView extends LinearLayout {
removeViewAt(i);
}
- if (trackSelector == null) {
+ MappingTrackSelector.MappedTrackInfo trackInfo =
+ trackSelector == null ? null : trackSelector.getCurrentMappedTrackInfo();
+ if (trackSelector == null || trackInfo == null) {
// The view is not initialized.
disableView.setEnabled(false);
defaultView.setEnabled(false);
@@ -212,7 +214,6 @@ public class TrackSelectionView extends LinearLayout {
disableView.setEnabled(true);
defaultView.setEnabled(true);
- MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo();
trackGroups = trackInfo.getTrackGroups(rendererIndex);
DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();