summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java33
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java678
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java55
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java330
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java375
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java50
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java1007
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java38
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java226
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java19
10 files changed, 2811 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java
new file mode 100644
index 0000000000..394a97a56a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable;
+
+/** Default implementation for {@link HlsPlaylistParserFactory}. */
+public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
+
+ @Override
+ public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
+ return new HlsPlaylistParser();
+ }
+
+ @Override
+ public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
+ HlsMasterPlaylist masterPlaylist) {
+ return new HlsPlaylistParser(masterPlaylist);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java
new file mode 100644
index 0000000000..b7f6a06975
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java
@@ -0,0 +1,678 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/** Default implementation for {@link HlsPlaylistTracker}. */
+public final class DefaultHlsPlaylistTracker
+ implements HlsPlaylistTracker, Loader.Callback<ParsingLoadable<HlsPlaylist>> {
+
+ /** Factory for {@link DefaultHlsPlaylistTracker} instances. */
+ public static final Factory FACTORY = DefaultHlsPlaylistTracker::new;
+
+ /**
+ * Default coefficient applied on the target duration of a playlist to determine the amount of
+ * time after which an unchanging playlist is considered stuck.
+ */
+ public static final double DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5;
+
+ private final HlsDataSourceFactory dataSourceFactory;
+ private final HlsPlaylistParserFactory playlistParserFactory;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final HashMap<Uri, MediaPlaylistBundle> playlistBundles;
+ private final List<PlaylistEventListener> listeners;
+ private final double playlistStuckTargetDurationCoefficient;
+
+ @Nullable private ParsingLoadable.Parser<HlsPlaylist> mediaPlaylistParser;
+ @Nullable private EventDispatcher eventDispatcher;
+ @Nullable private Loader initialPlaylistLoader;
+ @Nullable private Handler playlistRefreshHandler;
+ @Nullable private PrimaryPlaylistListener primaryPlaylistListener;
+ @Nullable private HlsMasterPlaylist masterPlaylist;
+ @Nullable private Uri primaryMediaPlaylistUrl;
+ @Nullable private HlsMediaPlaylist primaryMediaPlaylistSnapshot;
+ private boolean isLive;
+ private long initialStartTimeUs;
+
+ /**
+ * Creates an instance.
+ *
+ * @param dataSourceFactory A factory for {@link DataSource} instances.
+ * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
+ * @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
+ */
+ public DefaultHlsPlaylistTracker(
+ HlsDataSourceFactory dataSourceFactory,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ HlsPlaylistParserFactory playlistParserFactory) {
+ this(
+ dataSourceFactory,
+ loadErrorHandlingPolicy,
+ playlistParserFactory,
+ DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param dataSourceFactory A factory for {@link DataSource} instances.
+ * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
+ * @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
+ * @param playlistStuckTargetDurationCoefficient A coefficient to apply to the target duration of
+ * media playlists in order to determine that a non-changing playlist is stuck. Once a
+ * playlist is deemed stuck, a {@link PlaylistStuckException} is thrown via {@link
+ * #maybeThrowPlaylistRefreshError(Uri)}.
+ */
+ public DefaultHlsPlaylistTracker(
+ HlsDataSourceFactory dataSourceFactory,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ HlsPlaylistParserFactory playlistParserFactory,
+ double playlistStuckTargetDurationCoefficient) {
+ this.dataSourceFactory = dataSourceFactory;
+ this.playlistParserFactory = playlistParserFactory;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ this.playlistStuckTargetDurationCoefficient = playlistStuckTargetDurationCoefficient;
+ listeners = new ArrayList<>();
+ playlistBundles = new HashMap<>();
+ 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<HlsPlaylist> masterPlaylistLoadable =
+ new ParsingLoadable<>(
+ dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
+ initialPlaylistUri,
+ C.DATA_TYPE_MANIFEST,
+ playlistParserFactory.createPlaylistParser());
+ Assertions.checkState(initialPlaylistLoader == null);
+ initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist");
+ long elapsedRealtime =
+ initialPlaylistLoader.startLoading(
+ masterPlaylistLoadable,
+ this,
+ loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type));
+ eventDispatcher.loadStarted(
+ masterPlaylistLoadable.dataSpec,
+ masterPlaylistLoadable.type,
+ elapsedRealtime);
+ }
+
+ @Override
+ public void stop() {
+ primaryMediaPlaylistUrl = null;
+ primaryMediaPlaylistSnapshot = 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
+ @Nullable
+ public HlsMasterPlaylist getMasterPlaylist() {
+ return masterPlaylist;
+ }
+
+ @Override
+ @Nullable
+ public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) {
+ HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
+ if (snapshot != null && isForPlayback) {
+ maybeSetPrimaryUrl(url);
+ }
+ return snapshot;
+ }
+
+ @Override
+ public long getInitialStartTimeUs() {
+ return initialStartTimeUs;
+ }
+
+ @Override
+ public boolean isSnapshotValid(Uri url) {
+ return playlistBundles.get(url).isSnapshotValid();
+ }
+
+ @Override
+ public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
+ if (initialPlaylistLoader != null) {
+ initialPlaylistLoader.maybeThrowError();
+ }
+ if (primaryMediaPlaylistUrl != null) {
+ maybeThrowPlaylistRefreshError(primaryMediaPlaylistUrl);
+ }
+ }
+
+ @Override
+ public void maybeThrowPlaylistRefreshError(Uri url) throws IOException {
+ playlistBundles.get(url).maybeThrowPlaylistRefreshError();
+ }
+
+ @Override
+ public void refreshPlaylist(Uri url) {
+ playlistBundles.get(url).loadPlaylist();
+ }
+
+ @Override
+ public boolean isLive() {
+ return isLive;
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(
+ ParsingLoadable<HlsPlaylist> 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;
+ mediaPlaylistParser = playlistParserFactory.createPlaylistParser(masterPlaylist);
+ primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url;
+ createBundles(masterPlaylist.mediaPlaylistUrls);
+ MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);
+ if (isMediaPlaylist) {
+ // We don't need to load the playlist again. We can use the same result.
+ primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs);
+ } else {
+ primaryBundle.loadPlaylist();
+ }
+ eventDispatcher.loadCompleted(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ C.DATA_TYPE_MANIFEST,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded());
+ }
+
+ @Override
+ public void onLoadCanceled(
+ ParsingLoadable<HlsPlaylist> loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ boolean released) {
+ eventDispatcher.loadCanceled(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ C.DATA_TYPE_MANIFEST,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded());
+ }
+
+ @Override
+ public LoadErrorAction onLoadError(
+ ParsingLoadable<HlsPlaylist> loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ IOException error,
+ int errorCount) {
+ long retryDelayMs =
+ loadErrorHandlingPolicy.getRetryDelayMsFor(
+ loadable.type, loadDurationMs, error, errorCount);
+ boolean isFatal = retryDelayMs == C.TIME_UNSET;
+ eventDispatcher.loadError(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ C.DATA_TYPE_MANIFEST,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded(),
+ error,
+ isFatal);
+ return isFatal
+ ? Loader.DONT_RETRY_FATAL
+ : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs);
+ }
+
+ // Internal methods.
+
+ private boolean maybeSelectNewPrimaryUrl() {
+ List<Variant> 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).url);
+ if (currentTimeMs > bundle.blacklistUntilMs) {
+ primaryMediaPlaylistUrl = bundle.playlistUrl;
+ bundle.loadPlaylist();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void maybeSetPrimaryUrl(Uri url) {
+ if (url.equals(primaryMediaPlaylistUrl)
+ || !isVariantUrl(url)
+ || (primaryMediaPlaylistSnapshot != null && primaryMediaPlaylistSnapshot.hasEndTag)) {
+ // Ignore if the primary media playlist URL is unchanged, if the media playlist is not
+ // referenced directly by a variant, or it the last primary snapshot contains an end tag.
+ return;
+ }
+ primaryMediaPlaylistUrl = url;
+ playlistBundles.get(primaryMediaPlaylistUrl).loadPlaylist();
+ }
+
+ /** Returns whether any of the variants in the master playlist have the specified playlist URL. */
+ private boolean isVariantUrl(Uri playlistUrl) {
+ List<Variant> variants = masterPlaylist.variants;
+ for (int i = 0; i < variants.size(); i++) {
+ if (playlistUrl.equals(variants.get(i).url)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void createBundles(List<Uri> urls) {
+ int listSize = urls.size();
+ for (int i = 0; i < listSize; i++) {
+ Uri 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(Uri url, HlsMediaPlaylist newSnapshot) {
+ if (url.equals(primaryMediaPlaylistUrl)) {
+ if (primaryMediaPlaylistSnapshot == null) {
+ // This is the first primary url snapshot.
+ isLive = !newSnapshot.hasEndTag;
+ initialStartTimeUs = newSnapshot.startTimeUs;
+ }
+ primaryMediaPlaylistSnapshot = newSnapshot;
+ primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
+ }
+ int listenersSize = listeners.size();
+ for (int i = 0; i < listenersSize; i++) {
+ listeners.get(i).onPlaylistChanged();
+ }
+ }
+
+ private boolean notifyPlaylistError(Uri playlistUrl, long blacklistDurationMs) {
+ int listenersSize = listeners.size();
+ boolean anyBlacklistingFailed = false;
+ for (int i = 0; i < listenersSize; i++) {
+ anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, blacklistDurationMs);
+ }
+ 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 =
+ primaryMediaPlaylistSnapshot != null ? primaryMediaPlaylistSnapshot.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 =
+ primaryMediaPlaylistSnapshot != null
+ ? primaryMediaPlaylistSnapshot.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<Segment> 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<ParsingLoadable<HlsPlaylist>>, Runnable {
+
+ private final Uri playlistUrl;
+ private final Loader mediaPlaylistLoader;
+ private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
+
+ @Nullable private HlsMediaPlaylist playlistSnapshot;
+ private long lastSnapshotLoadMs;
+ private long lastSnapshotChangeMs;
+ private long earliestNextLoadTimeMs;
+ private long blacklistUntilMs;
+ private boolean loadPending;
+ private IOException playlistError;
+
+ public MediaPlaylistBundle(Uri playlistUrl) {
+ this.playlistUrl = playlistUrl;
+ mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist");
+ mediaPlaylistLoadable =
+ new ParsingLoadable<>(
+ dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
+ playlistUrl,
+ C.DATA_TYPE_MANIFEST,
+ mediaPlaylistParser);
+ }
+
+ @Nullable
+ 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() || mediaPlaylistLoader.hasFatalError()) {
+ // Load already pending, in progress, or a fatal error has been encountered. 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<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {
+ HlsPlaylist result = loadable.getResult();
+ if (result instanceof HlsMediaPlaylist) {
+ processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs);
+ eventDispatcher.loadCompleted(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ C.DATA_TYPE_MANIFEST,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded());
+ } else {
+ playlistError = new ParserException("Loaded playlist has unexpected type.");
+ }
+ }
+
+ @Override
+ public void onLoadCanceled(
+ ParsingLoadable<HlsPlaylist> loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ boolean released) {
+ eventDispatcher.loadCanceled(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ C.DATA_TYPE_MANIFEST,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded());
+ }
+
+ @Override
+ public LoadErrorAction onLoadError(
+ ParsingLoadable<HlsPlaylist> loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ IOException error,
+ int errorCount) {
+ LoadErrorAction loadErrorAction;
+
+ long blacklistDurationMs =
+ loadErrorHandlingPolicy.getBlacklistDurationMsFor(
+ loadable.type, loadDurationMs, error, errorCount);
+ boolean shouldBlacklist = blacklistDurationMs != C.TIME_UNSET;
+
+ boolean blacklistingFailed =
+ notifyPlaylistError(playlistUrl, blacklistDurationMs) || !shouldBlacklist;
+ if (shouldBlacklist) {
+ blacklistingFailed |= blacklistPlaylist(blacklistDurationMs);
+ }
+
+ if (blacklistingFailed) {
+ long retryDelay =
+ loadErrorHandlingPolicy.getRetryDelayMsFor(
+ loadable.type, loadDurationMs, error, errorCount);
+ loadErrorAction =
+ retryDelay != C.TIME_UNSET
+ ? Loader.createRetryAction(false, retryDelay)
+ : Loader.DONT_RETRY_FATAL;
+ } else {
+ loadErrorAction = Loader.DONT_RETRY;
+ }
+
+ eventDispatcher.loadError(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ C.DATA_TYPE_MANIFEST,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded(),
+ error,
+ /* wasCanceled= */ !loadErrorAction.isRetry());
+
+ return loadErrorAction;
+ }
+
+ // Runnable implementation.
+
+ @Override
+ public void run() {
+ loadPending = false;
+ loadPlaylistImmediately();
+ }
+
+ // Internal methods.
+
+ private void loadPlaylistImmediately() {
+ long elapsedRealtime =
+ mediaPlaylistLoader.startLoading(
+ mediaPlaylistLoadable,
+ this,
+ loadErrorHandlingPolicy.getMinimumLoadableRetryCount(mediaPlaylistLoadable.type));
+ eventDispatcher.loadStarted(
+ mediaPlaylistLoadable.dataSpec,
+ mediaPlaylistLoadable.type,
+ elapsedRealtime);
+ }
+
+ private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist, long loadDurationMs) {
+ 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) {
+ // TODO: Allow customization of playlist resets handling.
+ // The media sequence jumped backwards. The server has probably reset. We do not try
+ // blacklisting in this case.
+ playlistError = new PlaylistResetException(playlistUrl);
+ notifyPlaylistError(playlistUrl, C.TIME_UNSET);
+ } else if (currentTimeMs - lastSnapshotChangeMs
+ > C.usToMs(playlistSnapshot.targetDurationUs)
+ * playlistStuckTargetDurationCoefficient) {
+ // TODO: Allow customization of stuck playlists handling.
+ playlistError = new PlaylistStuckException(playlistUrl);
+ long blacklistDurationMs =
+ loadErrorHandlingPolicy.getBlacklistDurationMsFor(
+ C.DATA_TYPE_MANIFEST, loadDurationMs, playlistError, /* errorCount= */ 1);
+ notifyPlaylistError(playlistUrl, blacklistDurationMs);
+ if (blacklistDurationMs != C.TIME_UNSET) {
+ blacklistPlaylist(blacklistDurationMs);
+ }
+ }
+ }
+ // 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.equals(primaryMediaPlaylistUrl) && !playlistSnapshot.hasEndTag) {
+ loadPlaylist();
+ }
+ }
+
+ /**
+ * Blacklists the playlist.
+ *
+ * @param blacklistDurationMs The number of milliseconds for which the playlist should be
+ * blacklisted.
+ * @return Whether the playlist is the primary, despite being blacklisted.
+ */
+ private boolean blacklistPlaylist(long blacklistDurationMs) {
+ blacklistUntilMs = SystemClock.elapsedRealtime() + blacklistDurationMs;
+ return playlistUrl.equals(primaryMediaPlaylistUrl) && !maybeSelectNewPrimaryUrl();
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java
new file mode 100644
index 0000000000..a8c9ea1756
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.FilteringManifestParser;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable;
+import java.util.List;
+
+/**
+ * A {@link HlsPlaylistParserFactory} that includes only the streams identified by the given stream
+ * keys.
+ */
+public final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
+
+ private final HlsPlaylistParserFactory hlsPlaylistParserFactory;
+ private final List<StreamKey> streamKeys;
+
+ /**
+ * @param hlsPlaylistParserFactory A factory for the parsers of the playlists which will be
+ * filtered.
+ * @param streamKeys The stream keys. If null or empty then filtering will not occur.
+ */
+ public FilteringHlsPlaylistParserFactory(
+ HlsPlaylistParserFactory hlsPlaylistParserFactory, List<StreamKey> streamKeys) {
+ this.hlsPlaylistParserFactory = hlsPlaylistParserFactory;
+ this.streamKeys = streamKeys;
+ }
+
+ @Override
+ public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
+ return new FilteringManifestParser<>(
+ hlsPlaylistParserFactory.createPlaylistParser(), streamKeys);
+ }
+
+ @Override
+ public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
+ HlsMasterPlaylist masterPlaylist) {
+ return new FilteringManifestParser<>(
+ hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist), streamKeys);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
new file mode 100644
index 0000000000..376f2b4301
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
@@ -0,0 +1,330 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/** Represents an HLS master playlist. */
+public final class HlsMasterPlaylist extends HlsPlaylist {
+
+ /** Represents an empty master playlist, from which no attributes can be inherited. */
+ public static final HlsMasterPlaylist EMPTY =
+ new HlsMasterPlaylist(
+ /* baseUri= */ "",
+ /* tags= */ Collections.emptyList(),
+ /* variants= */ Collections.emptyList(),
+ /* videos= */ Collections.emptyList(),
+ /* audios= */ Collections.emptyList(),
+ /* subtitles= */ Collections.emptyList(),
+ /* closedCaptions= */ Collections.emptyList(),
+ /* muxedAudioFormat= */ null,
+ /* muxedCaptionFormats= */ Collections.emptyList(),
+ /* hasIndependentSegments= */ false,
+ /* variableDefinitions= */ Collections.emptyMap(),
+ /* sessionKeyDrmInitData= */ Collections.emptyList());
+
+ // These constants must not be changed because they are persisted in offline stream keys.
+ public static final int GROUP_INDEX_VARIANT = 0;
+ public static final int GROUP_INDEX_AUDIO = 1;
+ public static final int GROUP_INDEX_SUBTITLE = 2;
+
+ /** A variant (i.e. an #EXT-X-STREAM-INF tag) in a master playlist. */
+ public static final class Variant {
+
+ /** The variant's url. */
+ public final Uri url;
+
+ /** Format information associated with this variant. */
+ public final Format format;
+
+ /** The video rendition group referenced by this variant, or {@code null}. */
+ @Nullable public final String videoGroupId;
+
+ /** The audio rendition group referenced by this variant, or {@code null}. */
+ @Nullable public final String audioGroupId;
+
+ /** The subtitle rendition group referenced by this variant, or {@code null}. */
+ @Nullable public final String subtitleGroupId;
+
+ /** The caption rendition group referenced by this variant, or {@code null}. */
+ @Nullable public final String captionGroupId;
+
+ /**
+ * @param url See {@link #url}.
+ * @param format See {@link #format}.
+ * @param videoGroupId See {@link #videoGroupId}.
+ * @param audioGroupId See {@link #audioGroupId}.
+ * @param subtitleGroupId See {@link #subtitleGroupId}.
+ * @param captionGroupId See {@link #captionGroupId}.
+ */
+ public Variant(
+ Uri url,
+ Format format,
+ @Nullable String videoGroupId,
+ @Nullable String audioGroupId,
+ @Nullable String subtitleGroupId,
+ @Nullable String captionGroupId) {
+ this.url = url;
+ this.format = format;
+ this.videoGroupId = videoGroupId;
+ this.audioGroupId = audioGroupId;
+ this.subtitleGroupId = subtitleGroupId;
+ this.captionGroupId = captionGroupId;
+ }
+
+ /**
+ * Creates a variant for a given media playlist url.
+ *
+ * @param url The media playlist url.
+ * @return The variant instance.
+ */
+ public static Variant createMediaPlaylistVariantUrl(Uri url) {
+ Format format =
+ Format.createContainerFormat(
+ "0",
+ /* label= */ null,
+ MimeTypes.APPLICATION_M3U8,
+ /* sampleMimeType= */ null,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* selectionFlags= */ 0,
+ /* roleFlags= */ 0,
+ /* language= */ null);
+ return new Variant(
+ url,
+ format,
+ /* videoGroupId= */ null,
+ /* audioGroupId= */ null,
+ /* subtitleGroupId= */ null,
+ /* captionGroupId= */ null);
+ }
+
+ /** Returns a copy of this instance with the given {@link Format}. */
+ public Variant copyWithFormat(Format format) {
+ return new Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId);
+ }
+ }
+
+ /** A rendition (i.e. an #EXT-X-MEDIA tag) in a master playlist. */
+ public static final class Rendition {
+
+ /** The rendition's url, or null if the tag does not have a URI attribute. */
+ @Nullable public final Uri url;
+
+ /** Format information associated with this rendition. */
+ public final Format format;
+
+ /** The group to which this rendition belongs. */
+ public final String groupId;
+
+ /** The name of the rendition. */
+ public final String name;
+
+ /**
+ * @param url See {@link #url}.
+ * @param format See {@link #format}.
+ * @param groupId See {@link #groupId}.
+ * @param name See {@link #name}.
+ */
+ public Rendition(@Nullable Uri url, Format format, String groupId, String name) {
+ this.url = url;
+ this.format = format;
+ this.groupId = groupId;
+ this.name = name;
+ }
+
+ }
+
+ /** All of the media playlist URLs referenced by the playlist. */
+ public final List<Uri> mediaPlaylistUrls;
+ /** The variants declared by the playlist. */
+ public final List<Variant> variants;
+ /** The video renditions declared by the playlist. */
+ public final List<Rendition> videos;
+ /** The audio renditions declared by the playlist. */
+ public final List<Rendition> audios;
+ /** The subtitle renditions declared by the playlist. */
+ public final List<Rendition> subtitles;
+ /** The closed caption renditions declared by the playlist. */
+ public final List<Rendition> closedCaptions;
+
+ /**
+ * The format of the audio muxed in the variants. May be null if the playlist does not declare any
+ * muxed audio.
+ */
+ @Nullable public final Format muxedAudioFormat;
+ /**
+ * The format of the closed captions declared by the playlist. May be empty if the playlist
+ * explicitly declares no captions are available, or null if the playlist does not declare any
+ * captions information.
+ */
+ @Nullable public final List<Format> muxedCaptionFormats;
+ /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */
+ public final Map<String, String> variableDefinitions;
+ /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */
+ public final List<DrmInitData> sessionKeyDrmInitData;
+
+ /**
+ * @param baseUri See {@link #baseUri}.
+ * @param tags See {@link #tags}.
+ * @param variants See {@link #variants}.
+ * @param videos See {@link #videos}.
+ * @param audios See {@link #audios}.
+ * @param subtitles See {@link #subtitles}.
+ * @param closedCaptions See {@link #closedCaptions}.
+ * @param muxedAudioFormat See {@link #muxedAudioFormat}.
+ * @param muxedCaptionFormats See {@link #muxedCaptionFormats}.
+ * @param hasIndependentSegments See {@link #hasIndependentSegments}.
+ * @param variableDefinitions See {@link #variableDefinitions}.
+ * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}.
+ */
+ public HlsMasterPlaylist(
+ String baseUri,
+ List<String> tags,
+ List<Variant> variants,
+ List<Rendition> videos,
+ List<Rendition> audios,
+ List<Rendition> subtitles,
+ List<Rendition> closedCaptions,
+ @Nullable Format muxedAudioFormat,
+ @Nullable List<Format> muxedCaptionFormats,
+ boolean hasIndependentSegments,
+ Map<String, String> variableDefinitions,
+ List<DrmInitData> sessionKeyDrmInitData) {
+ super(baseUri, tags, hasIndependentSegments);
+ this.mediaPlaylistUrls =
+ Collections.unmodifiableList(
+ getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions));
+ this.variants = Collections.unmodifiableList(variants);
+ this.videos = Collections.unmodifiableList(videos);
+ this.audios = Collections.unmodifiableList(audios);
+ this.subtitles = Collections.unmodifiableList(subtitles);
+ this.closedCaptions = Collections.unmodifiableList(closedCaptions);
+ this.muxedAudioFormat = muxedAudioFormat;
+ this.muxedCaptionFormats = muxedCaptionFormats != null
+ ? Collections.unmodifiableList(muxedCaptionFormats) : null;
+ this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions);
+ this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData);
+ }
+
+ @Override
+ public HlsMasterPlaylist copy(List<StreamKey> streamKeys) {
+ return new HlsMasterPlaylist(
+ baseUri,
+ tags,
+ copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys),
+ // TODO: Allow stream keys to specify video renditions to be retained.
+ /* videos= */ Collections.emptyList(),
+ copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys),
+ copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys),
+ // TODO: Update to retain all closed captions.
+ /* closedCaptions= */ Collections.emptyList(),
+ muxedAudioFormat,
+ muxedCaptionFormats,
+ hasIndependentSegments,
+ variableDefinitions,
+ sessionKeyDrmInitData);
+ }
+
+ /**
+ * Creates a playlist with a single variant.
+ *
+ * @param variantUrl The url of the single variant.
+ * @return A master playlist with a single variant for the provided url.
+ */
+ public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) {
+ List<Variant> variant =
+ Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl)));
+ return new HlsMasterPlaylist(
+ /* baseUri= */ "",
+ /* tags= */ Collections.emptyList(),
+ variant,
+ /* videos= */ Collections.emptyList(),
+ /* audios= */ Collections.emptyList(),
+ /* subtitles= */ Collections.emptyList(),
+ /* closedCaptions= */ Collections.emptyList(),
+ /* muxedAudioFormat= */ null,
+ /* muxedCaptionFormats= */ null,
+ /* hasIndependentSegments= */ false,
+ /* variableDefinitions= */ Collections.emptyMap(),
+ /* sessionKeyDrmInitData= */ Collections.emptyList());
+ }
+
+ private static List<Uri> getMediaPlaylistUrls(
+ List<Variant> variants,
+ List<Rendition> videos,
+ List<Rendition> audios,
+ List<Rendition> subtitles,
+ List<Rendition> closedCaptions) {
+ ArrayList<Uri> mediaPlaylistUrls = new ArrayList<>();
+ for (int i = 0; i < variants.size(); i++) {
+ Uri uri = variants.get(i).url;
+ if (!mediaPlaylistUrls.contains(uri)) {
+ mediaPlaylistUrls.add(uri);
+ }
+ }
+ addMediaPlaylistUrls(videos, mediaPlaylistUrls);
+ addMediaPlaylistUrls(audios, mediaPlaylistUrls);
+ addMediaPlaylistUrls(subtitles, mediaPlaylistUrls);
+ addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls);
+ return mediaPlaylistUrls;
+ }
+
+ private static void addMediaPlaylistUrls(List<Rendition> renditions, List<Uri> out) {
+ for (int i = 0; i < renditions.size(); i++) {
+ Uri uri = renditions.get(i).url;
+ if (uri != null && !out.contains(uri)) {
+ out.add(uri);
+ }
+ }
+ }
+
+ private static <T> List<T> copyStreams(
+ List<T> streams, int groupIndex, List<StreamKey> streamKeys) {
+ List<T> copiedStreams = new ArrayList<>(streamKeys.size());
+ // TODO:
+ // 1. When variants with the same URL are not de-duplicated, duplicates must not increment
+ // trackIndex so as to avoid breaking stream keys that have been persisted for offline. All
+ // duplicates should be copied if the first variant is copied, or discarded otherwise.
+ // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to
+ // avoid breaking stream keys that have been persisted for offline. All renitions with null
+ // URLs should be copied. They may become unreachable if all variants that reference them are
+ // removed, but this is OK.
+ // 3. Renditions with URLs matching copied variants should always themselves be copied, even if
+ // the corresponding stream key is omitted. Else we're throwing away information for no gain.
+ for (int i = 0; i < streams.size(); i++) {
+ T stream = streams.get(i);
+ for (int j = 0; j < streamKeys.size(); j++) {
+ StreamKey streamKey = streamKeys.get(j);
+ if (streamKey.groupIndex == groupIndex && streamKey.trackIndex == i) {
+ copiedStreams.add(stream);
+ break;
+ }
+ }
+ }
+ return copiedStreams;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
new file mode 100644
index 0000000000..c3250a5cc0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
@@ -0,0 +1,375 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/** Represents an HLS media playlist. */
+public final class HlsMediaPlaylist extends HlsPlaylist {
+
+ /** Media segment reference. */
+ @SuppressWarnings("ComparableType")
+ public static final class Segment implements Comparable<Long> {
+
+ /**
+ * The url of the segment.
+ */
+ public final String url;
+ /**
+ * The media initialization section for this segment, as defined by #EXT-X-MAP. May be null if
+ * the media playlist does not define a media section for this segment. The same instance is
+ * used for all segments that share an EXT-X-MAP tag.
+ */
+ @Nullable public final Segment initializationSegment;
+ /** The duration of the segment in microseconds, as defined by #EXTINF. */
+ public final long durationUs;
+ /** The human readable title of the segment. */
+ public final String title;
+ /**
+ * The number of #EXT-X-DISCONTINUITY tags in the playlist before the segment.
+ */
+ public final int relativeDiscontinuitySequence;
+ /**
+ * The start time of the segment in microseconds, relative to the start of the playlist.
+ */
+ public final long relativeStartTimeUs;
+ /**
+ * DRM initialization data for sample decryption, or null if the segment does not use CDM-DRM
+ * protection.
+ */
+ @Nullable public final DrmInitData drmInitData;
+ /**
+ * The encryption identity key uri as defined by #EXT-X-KEY, or null if the segment does not use
+ * full segment encryption with identity key.
+ */
+ @Nullable public final String fullSegmentEncryptionKeyUri;
+ /**
+ * The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not
+ * encrypted.
+ */
+ @Nullable public final String encryptionIV;
+ /**
+ * The segment's byte range offset, as defined by #EXT-X-BYTERANGE.
+ */
+ public final long byterangeOffset;
+ /**
+ * The segment's byte range length, as defined by #EXT-X-BYTERANGE, or {@link C#LENGTH_UNSET} if
+ * no byte range is specified.
+ */
+ public final long byterangeLength;
+
+ /** Whether the segment is tagged with #EXT-X-GAP. */
+ public final boolean hasGapTag;
+
+ /**
+ * @param uri See {@link #url}.
+ * @param byterangeOffset See {@link #byterangeOffset}.
+ * @param byterangeLength See {@link #byterangeLength}.
+ * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.
+ * @param encryptionIV See {@link #encryptionIV}.
+ */
+ public Segment(
+ String uri,
+ long byterangeOffset,
+ long byterangeLength,
+ @Nullable String fullSegmentEncryptionKeyUri,
+ @Nullable String encryptionIV) {
+ this(
+ uri,
+ /* initializationSegment= */ null,
+ /* title= */ "",
+ /* durationUs= */ 0,
+ /* relativeDiscontinuitySequence= */ -1,
+ /* relativeStartTimeUs= */ C.TIME_UNSET,
+ /* drmInitData= */ null,
+ fullSegmentEncryptionKeyUri,
+ encryptionIV,
+ byterangeOffset,
+ byterangeLength,
+ /* hasGapTag= */ false);
+ }
+
+ /**
+ * @param url See {@link #url}.
+ * @param initializationSegment See {@link #initializationSegment}.
+ * @param title See {@link #title}.
+ * @param durationUs See {@link #durationUs}.
+ * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}.
+ * @param relativeStartTimeUs See {@link #relativeStartTimeUs}.
+ * @param drmInitData See {@link #drmInitData}.
+ * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.
+ * @param encryptionIV See {@link #encryptionIV}.
+ * @param byterangeOffset See {@link #byterangeOffset}.
+ * @param byterangeLength See {@link #byterangeLength}.
+ * @param hasGapTag See {@link #hasGapTag}.
+ */
+ public Segment(
+ String url,
+ @Nullable Segment initializationSegment,
+ String title,
+ long durationUs,
+ int relativeDiscontinuitySequence,
+ long relativeStartTimeUs,
+ @Nullable DrmInitData drmInitData,
+ @Nullable String fullSegmentEncryptionKeyUri,
+ @Nullable String encryptionIV,
+ long byterangeOffset,
+ long byterangeLength,
+ boolean hasGapTag) {
+ this.url = url;
+ this.initializationSegment = initializationSegment;
+ this.title = title;
+ this.durationUs = durationUs;
+ this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
+ this.relativeStartTimeUs = relativeStartTimeUs;
+ this.drmInitData = drmInitData;
+ this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri;
+ this.encryptionIV = encryptionIV;
+ this.byterangeOffset = byterangeOffset;
+ this.byterangeLength = byterangeLength;
+ this.hasGapTag = hasGapTag;
+ }
+
+ @Override
+ public int compareTo(Long relativeStartTimeUs) {
+ return this.relativeStartTimeUs > relativeStartTimeUs
+ ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0);
+ }
+
+ }
+
+ /**
+ * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. One of {@link
+ * #PLAYLIST_TYPE_UNKNOWN}, {@link #PLAYLIST_TYPE_VOD} or {@link #PLAYLIST_TYPE_EVENT}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT})
+ public @interface PlaylistType {}
+
+ public static final int PLAYLIST_TYPE_UNKNOWN = 0;
+ public static final int PLAYLIST_TYPE_VOD = 1;
+ public static final int PLAYLIST_TYPE_EVENT = 2;
+
+ /**
+ * The type of the playlist. See {@link PlaylistType}.
+ */
+ @PlaylistType public final int playlistType;
+ /**
+ * The start offset in microseconds, as defined by #EXT-X-START.
+ */
+ public final long startOffsetUs;
+ /**
+ * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch.
+ * Otherwise, contains the aggregated duration of removed segments up to this snapshot of the
+ * playlist.
+ */
+ public final long startTimeUs;
+ /**
+ * Whether the playlist contains the #EXT-X-DISCONTINUITY-SEQUENCE tag.
+ */
+ public final boolean hasDiscontinuitySequence;
+ /**
+ * The discontinuity sequence number of the first media segment in the playlist, as defined by
+ * #EXT-X-DISCONTINUITY-SEQUENCE.
+ */
+ public final int discontinuitySequence;
+ /**
+ * The media sequence number of the first media segment in the playlist, as defined by
+ * #EXT-X-MEDIA-SEQUENCE.
+ */
+ public final long mediaSequence;
+ /**
+ * The compatibility version, as defined by #EXT-X-VERSION.
+ */
+ public final int version;
+ /**
+ * The target duration in microseconds, as defined by #EXT-X-TARGETDURATION.
+ */
+ public final long targetDurationUs;
+ /**
+ * Whether the playlist contains the #EXT-X-ENDLIST tag.
+ */
+ public final boolean hasEndTag;
+ /**
+ * Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag.
+ */
+ public final boolean hasProgramDateTime;
+ /**
+ * Contains the CDM protection schemes used by segments in this playlist. Does not contain any key
+ * acquisition data. Null if none of the segments in the playlist is CDM-encrypted.
+ */
+ @Nullable public final DrmInitData protectionSchemes;
+ /**
+ * The list of segments in the playlist.
+ */
+ public final List<Segment> segments;
+ /**
+ * The total duration of the playlist in microseconds.
+ */
+ public final long durationUs;
+
+ /**
+ * @param playlistType See {@link #playlistType}.
+ * @param baseUri See {@link #baseUri}.
+ * @param tags See {@link #tags}.
+ * @param startOffsetUs See {@link #startOffsetUs}.
+ * @param startTimeUs See {@link #startTimeUs}.
+ * @param hasDiscontinuitySequence See {@link #hasDiscontinuitySequence}.
+ * @param discontinuitySequence See {@link #discontinuitySequence}.
+ * @param mediaSequence See {@link #mediaSequence}.
+ * @param version See {@link #version}.
+ * @param targetDurationUs See {@link #targetDurationUs}.
+ * @param hasIndependentSegments See {@link #hasIndependentSegments}.
+ * @param hasEndTag See {@link #hasEndTag}.
+ * @param protectionSchemes See {@link #protectionSchemes}.
+ * @param hasProgramDateTime See {@link #hasProgramDateTime}.
+ * @param segments See {@link #segments}.
+ */
+ public HlsMediaPlaylist(
+ @PlaylistType int playlistType,
+ String baseUri,
+ List<String> tags,
+ long startOffsetUs,
+ long startTimeUs,
+ boolean hasDiscontinuitySequence,
+ int discontinuitySequence,
+ long mediaSequence,
+ int version,
+ long targetDurationUs,
+ boolean hasIndependentSegments,
+ boolean hasEndTag,
+ boolean hasProgramDateTime,
+ @Nullable DrmInitData protectionSchemes,
+ List<Segment> segments) {
+ super(baseUri, tags, hasIndependentSegments);
+ this.playlistType = playlistType;
+ this.startTimeUs = startTimeUs;
+ this.hasDiscontinuitySequence = hasDiscontinuitySequence;
+ this.discontinuitySequence = discontinuitySequence;
+ this.mediaSequence = mediaSequence;
+ this.version = version;
+ this.targetDurationUs = targetDurationUs;
+ this.hasEndTag = hasEndTag;
+ this.hasProgramDateTime = hasProgramDateTime;
+ this.protectionSchemes = protectionSchemes;
+ this.segments = Collections.unmodifiableList(segments);
+ if (!segments.isEmpty()) {
+ Segment last = segments.get(segments.size() - 1);
+ durationUs = last.relativeStartTimeUs + last.durationUs;
+ } else {
+ durationUs = 0;
+ }
+ this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET
+ : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs;
+ }
+
+ @Override
+ public HlsMediaPlaylist copy(List<StreamKey> streamKeys) {
+ return this;
+ }
+
+ /**
+ * Returns whether this playlist is newer than {@code other}.
+ *
+ * @param other The playlist to compare.
+ * @return Whether this playlist is newer than {@code other}.
+ */
+ public boolean isNewerThan(HlsMediaPlaylist other) {
+ if (other == null || mediaSequence > other.mediaSequence) {
+ return true;
+ }
+ if (mediaSequence < other.mediaSequence) {
+ return false;
+ }
+ // The media sequences are equal.
+ int segmentCount = segments.size();
+ int otherSegmentCount = other.segments.size();
+ return segmentCount > otherSegmentCount
+ || (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag);
+ }
+
+ /**
+ * Returns the result of adding the duration of the playlist to its start time.
+ */
+ public long getEndTimeUs() {
+ return startTimeUs + durationUs;
+ }
+
+ /**
+ * Returns a playlist identical to this one except for the start time, the discontinuity sequence
+ * and {@code hasDiscontinuitySequence} values. The first two are set to the specified values,
+ * {@code hasDiscontinuitySequence} is set to true.
+ *
+ * @param startTimeUs The start time for the returned playlist.
+ * @param discontinuitySequence The discontinuity sequence for the returned playlist.
+ * @return An identical playlist including the provided discontinuity and timing information.
+ */
+ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
+ return new HlsMediaPlaylist(
+ playlistType,
+ baseUri,
+ tags,
+ startOffsetUs,
+ startTimeUs,
+ /* hasDiscontinuitySequence= */ true,
+ discontinuitySequence,
+ mediaSequence,
+ version,
+ targetDurationUs,
+ hasIndependentSegments,
+ hasEndTag,
+ hasProgramDateTime,
+ protectionSchemes,
+ segments);
+ }
+
+ /**
+ * Returns a playlist identical to this one except that an end tag is added. If an end tag is
+ * already present then the playlist will return itself.
+ */
+ public HlsMediaPlaylist copyWithEndTag() {
+ if (this.hasEndTag) {
+ return this;
+ }
+ return new HlsMediaPlaylist(
+ playlistType,
+ baseUri,
+ tags,
+ startOffsetUs,
+ startTimeUs,
+ hasDiscontinuitySequence,
+ discontinuitySequence,
+ mediaSequence,
+ version,
+ targetDurationUs,
+ hasIndependentSegments,
+ /* hasEndTag= */ true,
+ hasProgramDateTime,
+ protectionSchemes,
+ segments);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java
new file mode 100644
index 0000000000..28f9b0eeb0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.FilterableManifest;
+import java.util.Collections;
+import java.util.List;
+
+/** Represents an HLS playlist. */
+public abstract class HlsPlaylist implements FilterableManifest<HlsPlaylist> {
+
+ /**
+ * The base uri. Used to resolve relative paths.
+ */
+ public final String baseUri;
+ /**
+ * The list of tags in the playlist.
+ */
+ public final List<String> tags;
+ /**
+ * Whether the media is formed of independent segments, as defined by the
+ * #EXT-X-INDEPENDENT-SEGMENTS tag.
+ */
+ public final boolean hasIndependentSegments;
+
+ /**
+ * @param baseUri See {@link #baseUri}.
+ * @param tags See {@link #tags}.
+ * @param hasIndependentSegments See {@link #hasIndependentSegments}.
+ */
+ protected HlsPlaylist(String baseUri, List<String> tags, boolean hasIndependentSegments) {
+ this.baseUri = baseUri;
+ this.tags = Collections.unmodifiableList(tags);
+ this.hasIndependentSegments = hasIndependentSegments;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
new file mode 100644
index 0000000000..5495d28520
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
@@ -0,0 +1,1007 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.UnrecognizedInputFormatException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry.VariantInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.UriUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Queue;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
+import org.checkerframework.checker.nullness.qual.PolyNull;
+
+/**
+ * HLS playlists parsing logic.
+ */
+public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> {
+
+ private static final String PLAYLIST_HEADER = "#EXTM3U";
+
+ private static final String TAG_PREFIX = "#EXT";
+
+ private static final String TAG_VERSION = "#EXT-X-VERSION";
+ private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE";
+ private static final String TAG_DEFINE = "#EXT-X-DEFINE";
+ private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
+ private static final String TAG_MEDIA = "#EXT-X-MEDIA";
+ private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION";
+ private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
+ private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE";
+ private static final String TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME";
+ private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP";
+ private static final String TAG_INDEPENDENT_SEGMENTS = "#EXT-X-INDEPENDENT-SEGMENTS";
+ private static final String TAG_MEDIA_DURATION = "#EXTINF";
+ private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE";
+ private static final String TAG_START = "#EXT-X-START";
+ private static final String TAG_ENDLIST = "#EXT-X-ENDLIST";
+ private static final String TAG_KEY = "#EXT-X-KEY";
+ private static final String TAG_SESSION_KEY = "#EXT-X-SESSION-KEY";
+ private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
+ private static final String TAG_GAP = "#EXT-X-GAP";
+
+ private static final String TYPE_AUDIO = "AUDIO";
+ private static final String TYPE_VIDEO = "VIDEO";
+ private static final String TYPE_SUBTITLES = "SUBTITLES";
+ private static final String TYPE_CLOSED_CAPTIONS = "CLOSED-CAPTIONS";
+
+ private static final String METHOD_NONE = "NONE";
+ private static final String METHOD_AES_128 = "AES-128";
+ private static final String METHOD_SAMPLE_AES = "SAMPLE-AES";
+ // Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility.
+ private static final String METHOD_SAMPLE_AES_CENC = "SAMPLE-AES-CENC";
+ private static final String METHOD_SAMPLE_AES_CTR = "SAMPLE-AES-CTR";
+ private static final String KEYFORMAT_PLAYREADY = "com.microsoft.playready";
+ private static final String KEYFORMAT_IDENTITY = "identity";
+ private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY =
+ "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
+ private static final String KEYFORMAT_WIDEVINE_PSSH_JSON = "com.widevine";
+
+ private static final String BOOLEAN_TRUE = "YES";
+ private static final String BOOLEAN_FALSE = "NO";
+
+ private static final String ATTR_CLOSED_CAPTIONS_NONE = "CLOSED-CAPTIONS=NONE";
+
+ private static final Pattern REGEX_AVERAGE_BANDWIDTH =
+ Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b");
+ private static final Pattern REGEX_VIDEO = Pattern.compile("VIDEO=\"(.+?)\"");
+ private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\"");
+ private static final Pattern REGEX_SUBTITLES = Pattern.compile("SUBTITLES=\"(.+?)\"");
+ private static final Pattern REGEX_CLOSED_CAPTIONS = Pattern.compile("CLOSED-CAPTIONS=\"(.+?)\"");
+ private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b");
+ private static final Pattern REGEX_CHANNELS = Pattern.compile("CHANNELS=\"(.+?)\"");
+ private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
+ private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
+ private static final Pattern REGEX_FRAME_RATE = Pattern.compile("FRAME-RATE=([\\d\\.]+)\\b");
+ private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION
+ + ":(\\d+)\\b");
+ private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b");
+ private static final Pattern REGEX_PLAYLIST_TYPE = Pattern.compile(TAG_PLAYLIST_TYPE
+ + ":(.+)\\b");
+ private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
+ + ":(\\d+)\\b");
+ private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION
+ + ":([\\d\\.]+)\\b");
+ private static final Pattern REGEX_MEDIA_TITLE =
+ Pattern.compile(TAG_MEDIA_DURATION + ":[\\d\\.]+\\b,(.+)");
+ private static final Pattern REGEX_TIME_OFFSET = Pattern.compile("TIME-OFFSET=(-?[\\d\\.]+)\\b");
+ private static final Pattern REGEX_BYTERANGE = Pattern.compile(TAG_BYTERANGE
+ + ":(\\d+(?:@\\d+)?)\\b");
+ private static final Pattern REGEX_ATTR_BYTERANGE =
+ Pattern.compile("BYTERANGE=\"(\\d+(?:@\\d+)?)\\b\"");
+ private static final Pattern REGEX_METHOD =
+ Pattern.compile(
+ "METHOD=("
+ + METHOD_NONE
+ + "|"
+ + METHOD_AES_128
+ + "|"
+ + METHOD_SAMPLE_AES
+ + "|"
+ + METHOD_SAMPLE_AES_CENC
+ + "|"
+ + METHOD_SAMPLE_AES_CTR
+ + ")"
+ + "\\s*(?:,|$)");
+ private static final Pattern REGEX_KEYFORMAT = Pattern.compile("KEYFORMAT=\"(.+?)\"");
+ private static final Pattern REGEX_KEYFORMATVERSIONS =
+ Pattern.compile("KEYFORMATVERSIONS=\"(.+?)\"");
+ private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\"");
+ private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)");
+ private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO
+ + "|" + TYPE_SUBTITLES + "|" + TYPE_CLOSED_CAPTIONS + ")");
+ private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\"");
+ private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\"");
+ private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\"");
+ private static final Pattern REGEX_CHARACTERISTICS = Pattern.compile("CHARACTERISTICS=\"(.+?)\"");
+ private static final Pattern REGEX_INSTREAM_ID =
+ Pattern.compile("INSTREAM-ID=\"((?:CC|SERVICE)\\d+)\"");
+ private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
+ private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT");
+ private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
+ private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\"");
+ private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
+ private static final Pattern REGEX_VARIABLE_REFERENCE =
+ Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}");
+
+ private final HlsMasterPlaylist masterPlaylist;
+
+ /**
+ * Creates an instance where media playlists are parsed without inheriting attributes from a
+ * master playlist.
+ */
+ public HlsPlaylistParser() {
+ this(HlsMasterPlaylist.EMPTY);
+ }
+
+ /**
+ * Creates an instance where parsed media playlists inherit attributes from the given master
+ * playlist.
+ *
+ * @param masterPlaylist The master playlist from which media playlists will inherit attributes.
+ */
+ public HlsPlaylistParser(HlsMasterPlaylist masterPlaylist) {
+ this.masterPlaylist = masterPlaylist;
+ }
+
+ @Override
+ public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+ Queue<String> extraLines = new ArrayDeque<>();
+ String line;
+ try {
+ if (!checkPlaylistHeader(reader)) {
+ throw new UnrecognizedInputFormatException("Input does not start with the #EXTM3U header.",
+ uri);
+ }
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty()) {
+ // Do nothing.
+ } else if (line.startsWith(TAG_STREAM_INF)) {
+ extraLines.add(line);
+ return parseMasterPlaylist(new LineIterator(extraLines, reader), uri.toString());
+ } else if (line.startsWith(TAG_TARGET_DURATION)
+ || line.startsWith(TAG_MEDIA_SEQUENCE)
+ || line.startsWith(TAG_MEDIA_DURATION)
+ || line.startsWith(TAG_KEY)
+ || line.startsWith(TAG_BYTERANGE)
+ || line.equals(TAG_DISCONTINUITY)
+ || line.equals(TAG_DISCONTINUITY_SEQUENCE)
+ || line.equals(TAG_ENDLIST)) {
+ extraLines.add(line);
+ return parseMediaPlaylist(
+ masterPlaylist, new LineIterator(extraLines, reader), uri.toString());
+ } else {
+ extraLines.add(line);
+ }
+ }
+ } finally {
+ Util.closeQuietly(reader);
+ }
+ throw new ParserException("Failed to parse the playlist, could not identify any tags.");
+ }
+
+ private static boolean checkPlaylistHeader(BufferedReader reader) throws IOException {
+ int last = reader.read();
+ if (last == 0xEF) {
+ if (reader.read() != 0xBB || reader.read() != 0xBF) {
+ return false;
+ }
+ // The playlist contains a Byte Order Mark, which gets discarded.
+ last = reader.read();
+ }
+ last = skipIgnorableWhitespace(reader, true, last);
+ int playlistHeaderLength = PLAYLIST_HEADER.length();
+ for (int i = 0; i < playlistHeaderLength; i++) {
+ if (last != PLAYLIST_HEADER.charAt(i)) {
+ return false;
+ }
+ last = reader.read();
+ }
+ last = skipIgnorableWhitespace(reader, false, last);
+ return Util.isLinebreak(last);
+ }
+
+ private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLinebreaks, int c)
+ throws IOException {
+ while (c != -1 && Character.isWhitespace(c) && (skipLinebreaks || !Util.isLinebreak(c))) {
+ c = reader.read();
+ }
+ return c;
+ }
+
+ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
+ throws IOException {
+ HashMap<Uri, ArrayList<VariantInfo>> urlToVariantInfos = new HashMap<>();
+ HashMap<String, String> variableDefinitions = new HashMap<>();
+ ArrayList<Variant> variants = new ArrayList<>();
+ ArrayList<Rendition> videos = new ArrayList<>();
+ ArrayList<Rendition> audios = new ArrayList<>();
+ ArrayList<Rendition> subtitles = new ArrayList<>();
+ ArrayList<Rendition> closedCaptions = new ArrayList<>();
+ ArrayList<String> mediaTags = new ArrayList<>();
+ ArrayList<DrmInitData> sessionKeyDrmInitData = new ArrayList<>();
+ ArrayList<String> tags = new ArrayList<>();
+ Format muxedAudioFormat = null;
+ List<Format> muxedCaptionFormats = null;
+ boolean noClosedCaptions = false;
+ boolean hasIndependentSegmentsTag = false;
+
+ String line;
+ while (iterator.hasNext()) {
+ line = iterator.next();
+
+ if (line.startsWith(TAG_PREFIX)) {
+ // We expose all tags through the playlist.
+ tags.add(line);
+ }
+
+ if (line.startsWith(TAG_DEFINE)) {
+ variableDefinitions.put(
+ /* key= */ parseStringAttr(line, REGEX_NAME, variableDefinitions),
+ /* value= */ parseStringAttr(line, REGEX_VALUE, variableDefinitions));
+ } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
+ hasIndependentSegmentsTag = true;
+ } else if (line.startsWith(TAG_MEDIA)) {
+ // Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF
+ // tags.
+ mediaTags.add(line);
+ } else if (line.startsWith(TAG_SESSION_KEY)) {
+ String keyFormat =
+ parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
+ SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
+ if (schemeData != null) {
+ String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
+ String scheme = parseEncryptionScheme(method);
+ sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData));
+ }
+ } else if (line.startsWith(TAG_STREAM_INF)) {
+ noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
+ int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
+ // TODO: Plumb this into Format.
+ int averageBitrate = parseOptionalIntAttr(line, REGEX_AVERAGE_BANDWIDTH, -1);
+ String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions);
+ String resolutionString =
+ parseOptionalStringAttr(line, REGEX_RESOLUTION, variableDefinitions);
+ int width;
+ int height;
+ if (resolutionString != null) {
+ String[] widthAndHeight = resolutionString.split("x");
+ width = Integer.parseInt(widthAndHeight[0]);
+ height = Integer.parseInt(widthAndHeight[1]);
+ if (width <= 0 || height <= 0) {
+ // Resolution string is invalid.
+ width = Format.NO_VALUE;
+ height = Format.NO_VALUE;
+ }
+ } else {
+ width = Format.NO_VALUE;
+ height = Format.NO_VALUE;
+ }
+ float frameRate = Format.NO_VALUE;
+ String frameRateString =
+ parseOptionalStringAttr(line, REGEX_FRAME_RATE, variableDefinitions);
+ if (frameRateString != null) {
+ frameRate = Float.parseFloat(frameRateString);
+ }
+ String videoGroupId = parseOptionalStringAttr(line, REGEX_VIDEO, variableDefinitions);
+ String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO, variableDefinitions);
+ String subtitlesGroupId =
+ parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions);
+ String closedCaptionsGroupId =
+ parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions);
+ if (!iterator.hasNext()) {
+ throw new ParserException("#EXT-X-STREAM-INF tag must be followed by another line");
+ }
+ line =
+ replaceVariableReferences(
+ iterator.next(), variableDefinitions); // #EXT-X-STREAM-INF's URI.
+ Uri uri = UriUtil.resolveToUri(baseUri, line);
+ Format format =
+ Format.createVideoContainerFormat(
+ /* id= */ Integer.toString(variants.size()),
+ /* label= */ null,
+ /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
+ /* sampleMimeType= */ null,
+ codecs,
+ /* metadata= */ null,
+ bitrate,
+ width,
+ height,
+ frameRate,
+ /* initializationData= */ null,
+ /* selectionFlags= */ 0,
+ /* roleFlags= */ 0);
+ Variant variant =
+ new Variant(
+ uri, format, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId);
+ variants.add(variant);
+ ArrayList<VariantInfo> variantInfosForUrl = urlToVariantInfos.get(uri);
+ if (variantInfosForUrl == null) {
+ variantInfosForUrl = new ArrayList<>();
+ urlToVariantInfos.put(uri, variantInfosForUrl);
+ }
+ variantInfosForUrl.add(
+ new VariantInfo(
+ bitrate, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId));
+ }
+ }
+
+ // TODO: Don't deduplicate variants by URL.
+ ArrayList<Variant> deduplicatedVariants = new ArrayList<>();
+ HashSet<Uri> urlsInDeduplicatedVariants = new HashSet<>();
+ for (int i = 0; i < variants.size(); i++) {
+ Variant variant = variants.get(i);
+ if (urlsInDeduplicatedVariants.add(variant.url)) {
+ Assertions.checkState(variant.format.metadata == null);
+ HlsTrackMetadataEntry hlsMetadataEntry =
+ new HlsTrackMetadataEntry(
+ /* groupId= */ null,
+ /* name= */ null,
+ Assertions.checkNotNull(urlToVariantInfos.get(variant.url)));
+ deduplicatedVariants.add(
+ variant.copyWithFormat(
+ variant.format.copyWithMetadata(new Metadata(hlsMetadataEntry))));
+ }
+ }
+
+ for (int i = 0; i < mediaTags.size(); i++) {
+ line = mediaTags.get(i);
+ String groupId = parseStringAttr(line, REGEX_GROUP_ID, variableDefinitions);
+ String name = parseStringAttr(line, REGEX_NAME, variableDefinitions);
+ String referenceUri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions);
+ Uri uri = referenceUri == null ? null : UriUtil.resolveToUri(baseUri, referenceUri);
+ String language = parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions);
+ @C.SelectionFlags int selectionFlags = parseSelectionFlags(line);
+ @C.RoleFlags int roleFlags = parseRoleFlags(line, variableDefinitions);
+ String formatId = groupId + ":" + name;
+ Format format;
+ Metadata metadata =
+ new Metadata(new HlsTrackMetadataEntry(groupId, name, Collections.emptyList()));
+ switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) {
+ case TYPE_VIDEO:
+ Variant variant = getVariantWithVideoGroup(variants, groupId);
+ String codecs = null;
+ int width = Format.NO_VALUE;
+ int height = Format.NO_VALUE;
+ float frameRate = Format.NO_VALUE;
+ if (variant != null) {
+ Format variantFormat = variant.format;
+ codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO);
+ width = variantFormat.width;
+ height = variantFormat.height;
+ frameRate = variantFormat.frameRate;
+ }
+ String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null;
+ format =
+ Format.createVideoContainerFormat(
+ /* id= */ formatId,
+ /* label= */ name,
+ /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
+ sampleMimeType,
+ codecs,
+ /* metadata= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ width,
+ height,
+ frameRate,
+ /* initializationData= */ null,
+ selectionFlags,
+ roleFlags)
+ .copyWithMetadata(metadata);
+ if (uri == null) {
+ // TODO: Remove this case and add a Rendition with a null uri to videos.
+ } else {
+ videos.add(new Rendition(uri, format, groupId, name));
+ }
+ break;
+ case TYPE_AUDIO:
+ variant = getVariantWithAudioGroup(variants, groupId);
+ codecs =
+ variant != null
+ ? Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_AUDIO)
+ : null;
+ sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null;
+ String channelsString =
+ parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions);
+ int channelCount = Format.NO_VALUE;
+ if (channelsString != null) {
+ channelCount = Integer.parseInt(Util.splitAtFirst(channelsString, "/")[0]);
+ if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType) && channelsString.endsWith("/JOC")) {
+ sampleMimeType = MimeTypes.AUDIO_E_AC3_JOC;
+ }
+ }
+ format =
+ Format.createAudioContainerFormat(
+ /* id= */ formatId,
+ /* label= */ name,
+ /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
+ sampleMimeType,
+ codecs,
+ /* metadata= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ channelCount,
+ /* sampleRate= */ Format.NO_VALUE,
+ /* initializationData= */ null,
+ selectionFlags,
+ roleFlags,
+ language);
+ if (uri == null) {
+ // TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios.
+ muxedAudioFormat = format;
+ } else {
+ audios.add(new Rendition(uri, format.copyWithMetadata(metadata), groupId, name));
+ }
+ break;
+ case TYPE_SUBTITLES:
+ codecs = null;
+ sampleMimeType = null;
+ variant = getVariantWithSubtitleGroup(variants, groupId);
+ if (variant != null) {
+ codecs = Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_TEXT);
+ sampleMimeType = MimeTypes.getMediaMimeType(codecs);
+ }
+ if (sampleMimeType == null) {
+ sampleMimeType = MimeTypes.TEXT_VTT;
+ }
+ format =
+ Format.createTextContainerFormat(
+ /* id= */ formatId,
+ /* label= */ name,
+ /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
+ sampleMimeType,
+ codecs,
+ /* bitrate= */ Format.NO_VALUE,
+ selectionFlags,
+ roleFlags,
+ language)
+ .copyWithMetadata(metadata);
+ subtitles.add(new Rendition(uri, format, groupId, name));
+ break;
+ case TYPE_CLOSED_CAPTIONS:
+ String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions);
+ String mimeType;
+ int accessibilityChannel;
+ if (instreamId.startsWith("CC")) {
+ mimeType = MimeTypes.APPLICATION_CEA608;
+ accessibilityChannel = Integer.parseInt(instreamId.substring(2));
+ } else /* starts with SERVICE */ {
+ mimeType = MimeTypes.APPLICATION_CEA708;
+ accessibilityChannel = Integer.parseInt(instreamId.substring(7));
+ }
+ if (muxedCaptionFormats == null) {
+ muxedCaptionFormats = new ArrayList<>();
+ }
+ muxedCaptionFormats.add(
+ Format.createTextContainerFormat(
+ /* id= */ formatId,
+ /* label= */ name,
+ /* containerMimeType= */ null,
+ /* sampleMimeType= */ mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ selectionFlags,
+ roleFlags,
+ language,
+ accessibilityChannel));
+ // TODO: Remove muxedCaptionFormats and add a Rendition with a null uri to closedCaptions.
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ }
+
+ if (noClosedCaptions) {
+ muxedCaptionFormats = Collections.emptyList();
+ }
+
+ return new HlsMasterPlaylist(
+ baseUri,
+ tags,
+ deduplicatedVariants,
+ videos,
+ audios,
+ subtitles,
+ closedCaptions,
+ muxedAudioFormat,
+ muxedCaptionFormats,
+ hasIndependentSegmentsTag,
+ variableDefinitions,
+ sessionKeyDrmInitData);
+ }
+
+ @Nullable
+ private static Variant getVariantWithAudioGroup(ArrayList<Variant> variants, String groupId) {
+ for (int i = 0; i < variants.size(); i++) {
+ Variant variant = variants.get(i);
+ if (groupId.equals(variant.audioGroupId)) {
+ return variant;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Variant getVariantWithVideoGroup(ArrayList<Variant> variants, String groupId) {
+ for (int i = 0; i < variants.size(); i++) {
+ Variant variant = variants.get(i);
+ if (groupId.equals(variant.videoGroupId)) {
+ return variant;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Variant getVariantWithSubtitleGroup(ArrayList<Variant> variants, String groupId) {
+ for (int i = 0; i < variants.size(); i++) {
+ Variant variant = variants.get(i);
+ if (groupId.equals(variant.subtitleGroupId)) {
+ return variant;
+ }
+ }
+ return null;
+ }
+
+ private static HlsMediaPlaylist parseMediaPlaylist(
+ HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException {
+ @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
+ long startOffsetUs = C.TIME_UNSET;
+ long mediaSequence = 0;
+ int version = 1; // Default version == 1.
+ long targetDurationUs = C.TIME_UNSET;
+ boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments;
+ boolean hasEndTag = false;
+ Segment initializationSegment = null;
+ HashMap<String, String> variableDefinitions = new HashMap<>();
+ List<Segment> segments = new ArrayList<>();
+ List<String> tags = new ArrayList<>();
+
+ long segmentDurationUs = 0;
+ String segmentTitle = "";
+ boolean hasDiscontinuitySequence = false;
+ int playlistDiscontinuitySequence = 0;
+ int relativeDiscontinuitySequence = 0;
+ long playlistStartTimeUs = 0;
+ long segmentStartTimeUs = 0;
+ long segmentByteRangeOffset = 0;
+ long segmentByteRangeLength = C.LENGTH_UNSET;
+ long segmentMediaSequence = 0;
+ boolean hasGapTag = false;
+
+ DrmInitData playlistProtectionSchemes = null;
+ String fullSegmentEncryptionKeyUri = null;
+ String fullSegmentEncryptionIV = null;
+ TreeMap<String, SchemeData> currentSchemeDatas = new TreeMap<>();
+ String encryptionScheme = null;
+ DrmInitData cachedDrmInitData = null;
+
+ String line;
+ while (iterator.hasNext()) {
+ line = iterator.next();
+
+ if (line.startsWith(TAG_PREFIX)) {
+ // We expose all tags through the playlist.
+ tags.add(line);
+ }
+
+ if (line.startsWith(TAG_PLAYLIST_TYPE)) {
+ String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE, variableDefinitions);
+ if ("VOD".equals(playlistTypeString)) {
+ playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;
+ } else if ("EVENT".equals(playlistTypeString)) {
+ playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
+ }
+ } else if (line.startsWith(TAG_START)) {
+ startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
+ } else if (line.startsWith(TAG_INIT_SEGMENT)) {
+ String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
+ String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
+ if (byteRange != null) {
+ String[] splitByteRange = byteRange.split("@");
+ segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
+ if (splitByteRange.length > 1) {
+ segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
+ }
+ }
+ if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) {
+ // See RFC 8216, Section 4.3.2.5.
+ throw new ParserException(
+ "The encryption IV attribute must be present when an initialization segment is "
+ + "encrypted with METHOD=AES-128.");
+ }
+ initializationSegment =
+ new Segment(
+ uri,
+ segmentByteRangeOffset,
+ segmentByteRangeLength,
+ fullSegmentEncryptionKeyUri,
+ fullSegmentEncryptionIV);
+ segmentByteRangeOffset = 0;
+ segmentByteRangeLength = C.LENGTH_UNSET;
+ } else if (line.startsWith(TAG_TARGET_DURATION)) {
+ targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND;
+ } else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
+ mediaSequence = parseLongAttr(line, REGEX_MEDIA_SEQUENCE);
+ segmentMediaSequence = mediaSequence;
+ } else if (line.startsWith(TAG_VERSION)) {
+ version = parseIntAttr(line, REGEX_VERSION);
+ } else if (line.startsWith(TAG_DEFINE)) {
+ String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions);
+ if (importName != null) {
+ String value = masterPlaylist.variableDefinitions.get(importName);
+ if (value != null) {
+ variableDefinitions.put(importName, value);
+ } else {
+ // The master playlist does not declare the imported variable. Ignore.
+ }
+ } else {
+ variableDefinitions.put(
+ parseStringAttr(line, REGEX_NAME, variableDefinitions),
+ parseStringAttr(line, REGEX_VALUE, variableDefinitions));
+ }
+ } else if (line.startsWith(TAG_MEDIA_DURATION)) {
+ segmentDurationUs =
+ (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
+ segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions);
+ } else if (line.startsWith(TAG_KEY)) {
+ String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
+ String keyFormat =
+ parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
+ fullSegmentEncryptionKeyUri = null;
+ fullSegmentEncryptionIV = null;
+ if (METHOD_NONE.equals(method)) {
+ currentSchemeDatas.clear();
+ cachedDrmInitData = null;
+ } else /* !METHOD_NONE.equals(method) */ {
+ fullSegmentEncryptionIV = parseOptionalStringAttr(line, REGEX_IV, variableDefinitions);
+ if (KEYFORMAT_IDENTITY.equals(keyFormat)) {
+ if (METHOD_AES_128.equals(method)) {
+ // The segment is fully encrypted using an identity key.
+ fullSegmentEncryptionKeyUri = parseStringAttr(line, REGEX_URI, variableDefinitions);
+ } else {
+ // Do nothing. Samples are encrypted using an identity key, but this is not supported.
+ // Hopefully, a traditional DRM alternative is also provided.
+ }
+ } else {
+ if (encryptionScheme == null) {
+ encryptionScheme = parseEncryptionScheme(method);
+ }
+ SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
+ if (schemeData != null) {
+ cachedDrmInitData = null;
+ currentSchemeDatas.put(keyFormat, schemeData);
+ }
+ }
+ }
+ } else if (line.startsWith(TAG_BYTERANGE)) {
+ String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions);
+ String[] splitByteRange = byteRange.split("@");
+ segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
+ if (splitByteRange.length > 1) {
+ segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
+ }
+ } else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) {
+ hasDiscontinuitySequence = true;
+ playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1));
+ } else if (line.equals(TAG_DISCONTINUITY)) {
+ relativeDiscontinuitySequence++;
+ } else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
+ if (playlistStartTimeUs == 0) {
+ long programDatetimeUs =
+ C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
+ playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
+ }
+ } else if (line.equals(TAG_GAP)) {
+ hasGapTag = true;
+ } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
+ hasIndependentSegmentsTag = true;
+ } else if (line.equals(TAG_ENDLIST)) {
+ hasEndTag = true;
+ } else if (!line.startsWith("#")) {
+ String segmentEncryptionIV;
+ if (fullSegmentEncryptionKeyUri == null) {
+ segmentEncryptionIV = null;
+ } else if (fullSegmentEncryptionIV != null) {
+ segmentEncryptionIV = fullSegmentEncryptionIV;
+ } else {
+ segmentEncryptionIV = Long.toHexString(segmentMediaSequence);
+ }
+
+ segmentMediaSequence++;
+ if (segmentByteRangeLength == C.LENGTH_UNSET) {
+ segmentByteRangeOffset = 0;
+ }
+
+ if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
+ SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
+ cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
+ if (playlistProtectionSchemes == null) {
+ SchemeData[] playlistSchemeDatas = new SchemeData[schemeDatas.length];
+ for (int i = 0; i < schemeDatas.length; i++) {
+ playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null);
+ }
+ playlistProtectionSchemes = new DrmInitData(encryptionScheme, playlistSchemeDatas);
+ }
+ }
+
+ segments.add(
+ new Segment(
+ replaceVariableReferences(line, variableDefinitions),
+ initializationSegment,
+ segmentTitle,
+ segmentDurationUs,
+ relativeDiscontinuitySequence,
+ segmentStartTimeUs,
+ cachedDrmInitData,
+ fullSegmentEncryptionKeyUri,
+ segmentEncryptionIV,
+ segmentByteRangeOffset,
+ segmentByteRangeLength,
+ hasGapTag));
+ segmentStartTimeUs += segmentDurationUs;
+ segmentDurationUs = 0;
+ segmentTitle = "";
+ if (segmentByteRangeLength != C.LENGTH_UNSET) {
+ segmentByteRangeOffset += segmentByteRangeLength;
+ }
+ segmentByteRangeLength = C.LENGTH_UNSET;
+ hasGapTag = false;
+ }
+ }
+ return new HlsMediaPlaylist(
+ playlistType,
+ baseUri,
+ tags,
+ startOffsetUs,
+ playlistStartTimeUs,
+ hasDiscontinuitySequence,
+ playlistDiscontinuitySequence,
+ mediaSequence,
+ version,
+ targetDurationUs,
+ hasIndependentSegmentsTag,
+ hasEndTag,
+ /* hasProgramDateTime= */ playlistStartTimeUs != 0,
+ playlistProtectionSchemes,
+ segments);
+ }
+
+ @C.SelectionFlags
+ private static int parseSelectionFlags(String line) {
+ int flags = 0;
+ if (parseOptionalBooleanAttribute(line, REGEX_DEFAULT, false)) {
+ flags |= C.SELECTION_FLAG_DEFAULT;
+ }
+ if (parseOptionalBooleanAttribute(line, REGEX_FORCED, false)) {
+ flags |= C.SELECTION_FLAG_FORCED;
+ }
+ if (parseOptionalBooleanAttribute(line, REGEX_AUTOSELECT, false)) {
+ flags |= C.SELECTION_FLAG_AUTOSELECT;
+ }
+ return flags;
+ }
+
+ @C.RoleFlags
+ private static int parseRoleFlags(String line, Map<String, String> variableDefinitions) {
+ String concatenatedCharacteristics =
+ parseOptionalStringAttr(line, REGEX_CHARACTERISTICS, variableDefinitions);
+ if (TextUtils.isEmpty(concatenatedCharacteristics)) {
+ return 0;
+ }
+ String[] characteristics = Util.split(concatenatedCharacteristics, ",");
+ @C.RoleFlags int roleFlags = 0;
+ if (Util.contains(characteristics, "public.accessibility.describes-video")) {
+ roleFlags |= C.ROLE_FLAG_DESCRIBES_VIDEO;
+ }
+ if (Util.contains(characteristics, "public.accessibility.transcribes-spoken-dialog")) {
+ roleFlags |= C.ROLE_FLAG_TRANSCRIBES_DIALOG;
+ }
+ if (Util.contains(characteristics, "public.accessibility.describes-music-and-sound")) {
+ roleFlags |= C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;
+ }
+ if (Util.contains(characteristics, "public.easy-to-read")) {
+ roleFlags |= C.ROLE_FLAG_EASY_TO_READ;
+ }
+ return roleFlags;
+ }
+
+ @Nullable
+ private static SchemeData parseDrmSchemeData(
+ String line, String keyFormat, Map<String, String> variableDefinitions)
+ throws ParserException {
+ String keyFormatVersions =
+ parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions);
+ if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) {
+ String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
+ return new SchemeData(
+ C.WIDEVINE_UUID,
+ MimeTypes.VIDEO_MP4,
+ Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT));
+ } else if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) {
+ return new SchemeData(C.WIDEVINE_UUID, "hls", Util.getUtf8Bytes(line));
+ } else if (KEYFORMAT_PLAYREADY.equals(keyFormat) && "1".equals(keyFormatVersions)) {
+ String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
+ byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT);
+ byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data);
+ return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData);
+ }
+ return null;
+ }
+
+ private static String parseEncryptionScheme(String method) {
+ return METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method)
+ ? C.CENC_TYPE_cenc
+ : C.CENC_TYPE_cbcs;
+ }
+
+ private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
+ return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap()));
+ }
+
+ private static int parseOptionalIntAttr(String line, Pattern pattern, int defaultValue) {
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ return Integer.parseInt(matcher.group(1));
+ }
+ return defaultValue;
+ }
+
+ private static long parseLongAttr(String line, Pattern pattern) throws ParserException {
+ return Long.parseLong(parseStringAttr(line, pattern, Collections.emptyMap()));
+ }
+
+ private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException {
+ return Double.parseDouble(parseStringAttr(line, pattern, Collections.emptyMap()));
+ }
+
+ private static String parseStringAttr(
+ String line, Pattern pattern, Map<String, String> variableDefinitions)
+ throws ParserException {
+ String value = parseOptionalStringAttr(line, pattern, variableDefinitions);
+ if (value != null) {
+ return value;
+ } else {
+ throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line);
+ }
+ }
+
+ private static @Nullable String parseOptionalStringAttr(
+ String line, Pattern pattern, Map<String, String> variableDefinitions) {
+ return parseOptionalStringAttr(line, pattern, null, variableDefinitions);
+ }
+
+ private static @PolyNull String parseOptionalStringAttr(
+ String line,
+ Pattern pattern,
+ @PolyNull String defaultValue,
+ Map<String, String> variableDefinitions) {
+ Matcher matcher = pattern.matcher(line);
+ String value = matcher.find() ? matcher.group(1) : defaultValue;
+ return variableDefinitions.isEmpty() || value == null
+ ? value
+ : replaceVariableReferences(value, variableDefinitions);
+ }
+
+ private static String replaceVariableReferences(
+ String string, Map<String, String> variableDefinitions) {
+ Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string);
+ // TODO: Replace StringBuffer with StringBuilder once Java 9 is available.
+ StringBuffer stringWithReplacements = new StringBuffer();
+ while (matcher.find()) {
+ String groupName = matcher.group(1);
+ if (variableDefinitions.containsKey(groupName)) {
+ matcher.appendReplacement(
+ stringWithReplacements, Matcher.quoteReplacement(variableDefinitions.get(groupName)));
+ } else {
+ // The variable is not defined. The value is ignored.
+ }
+ }
+ matcher.appendTail(stringWithReplacements);
+ return stringWithReplacements.toString();
+ }
+
+ private static boolean parseOptionalBooleanAttribute(
+ String line, Pattern pattern, boolean defaultValue) {
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ return matcher.group(1).equals(BOOLEAN_TRUE);
+ }
+ return defaultValue;
+ }
+
+ private static Pattern compileBooleanAttrPattern(String attribute) {
+ return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")");
+ }
+
+ private static class LineIterator {
+
+ private final BufferedReader reader;
+ private final Queue<String> extraLines;
+
+ @Nullable private String next;
+
+ public LineIterator(Queue<String> extraLines, BufferedReader reader) {
+ this.extraLines = extraLines;
+ this.reader = reader;
+ }
+
+ @EnsuresNonNullIf(expression = "next", result = true)
+ public boolean hasNext() throws IOException {
+ if (next != null) {
+ return true;
+ }
+ if (!extraLines.isEmpty()) {
+ next = Assertions.checkNotNull(extraLines.poll());
+ return true;
+ }
+ while ((next = reader.readLine()) != null) {
+ next = next.trim();
+ if (!next.isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Return the next line, or throw {@link NoSuchElementException} if none. */
+ public String next() throws IOException {
+ if (hasNext()) {
+ String result = next;
+ next = null;
+ return result;
+ } else {
+ throw new NoSuchElementException();
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java
new file mode 100644
index 0000000000..deb1daf8a7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable;
+
+/** Factory for {@link HlsPlaylist} parsers. */
+public interface HlsPlaylistParserFactory {
+
+ /**
+ * Returns a stand-alone playlist parser. Playlists parsed by the returned parser do not inherit
+ * any attributes from other playlists.
+ */
+ ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser();
+
+ /**
+ * Returns a playlist parser for playlists that were referenced by the given {@link
+ * HlsMasterPlaylist}. Returned {@link HlsMediaPlaylist} instances may inherit attributes from
+ * {@code masterPlaylist}.
+ *
+ * @param masterPlaylist The master playlist that referenced any parsed media playlists.
+ * @return A parser for HLS playlists.
+ */
+ ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(HlsMasterPlaylist masterPlaylist);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
new file mode 100644
index 0000000000..69f8cb02c9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
@@ -0,0 +1,226 @@
+/*
+ * 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.
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
+import java.io.IOException;
+
+/**
+ * Tracks playlists associated to an HLS stream and provides snapshots.
+ *
+ * <p>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).
+ *
+ * <p>Playlist loads might encounter errors. The tracker may choose to blacklist them to ensure a
+ * primary playlist is always available.
+ */
+public interface HlsPlaylistTracker {
+
+ /** Factory for {@link HlsPlaylistTracker} instances. */
+ interface Factory {
+
+ /**
+ * Creates a new tracker instance.
+ *
+ * @param dataSourceFactory The {@link HlsDataSourceFactory} to use for playlist loading.
+ * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for playlist load errors.
+ * @param playlistParserFactory The {@link HlsPlaylistParserFactory} for playlist parsing.
+ */
+ HlsPlaylistTracker createTracker(
+ HlsDataSourceFactory dataSourceFactory,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ HlsPlaylistParserFactory playlistParserFactory);
+ }
+
+ /** Listener for primary playlist changes. */
+ interface PrimaryPlaylistListener {
+
+ /**
+ * Called when the primary playlist changes.
+ *
+ * @param mediaPlaylist The primary playlist new snapshot.
+ */
+ void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
+ }
+
+ /** Called on playlist loading events. */
+ interface PlaylistEventListener {
+
+ /**
+ * Called a playlist changes.
+ */
+ void onPlaylistChanged();
+
+ /**
+ * Called if an error is encountered while loading a playlist.
+ *
+ * @param url The loaded url that caused the error.
+ * @param blacklistDurationMs The duration for which the playlist should be blacklisted. Or
+ * {@link C#TIME_UNSET} if the playlist should not be blacklisted.
+ * @return True if blacklisting did not encounter errors. False otherwise.
+ */
+ boolean onPlaylistError(Uri url, long blacklistDurationMs);
+ }
+
+ /** Thrown when a playlist is considered to be stuck due to a server side error. */
+ final class PlaylistStuckException extends IOException {
+
+ /** The url of the stuck playlist. */
+ public final Uri url;
+
+ /**
+ * Creates an instance.
+ *
+ * @param url See {@link #url}.
+ */
+ public PlaylistStuckException(Uri 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 Uri url;
+
+ /**
+ * Creates an instance.
+ *
+ * @param url See {@link #url}.
+ */
+ public PlaylistResetException(Uri url) {
+ this.url = url;
+ }
+ }
+
+ /**
+ * Starts the playlist tracker.
+ *
+ * <p>Must be called from the playback thread. A tracker may be restarted after a {@link #stop()}
+ * 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);
+
+ /**
+ * Stops the playlist tracker and releases any acquired resources.
+ *
+ * <p>Must be called once per {@link #start} call.
+ */
+ void stop();
+
+ /**
+ * Registers a listener to receive events from the playlist tracker.
+ *
+ * @param listener The listener.
+ */
+ void addListener(PlaylistEventListener listener);
+
+ /**
+ * Unregisters a listener.
+ *
+ * @param listener The listener to unregister.
+ */
+ void removeListener(PlaylistEventListener listener);
+
+ /**
+ * Returns the master playlist.
+ *
+ * <p>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.
+ */
+ @Nullable
+ HlsMasterPlaylist getMasterPlaylist();
+
+ /**
+ * Returns the most recent snapshot available of the playlist referenced by the provided {@link
+ * Uri}.
+ *
+ * @param url The {@link Uri} corresponding to the requested media playlist.
+ * @param isForPlayback Whether the caller might use the snapshot to request media segments for
+ * playback. If true, the primary playlist may be updated to the one requested.
+ * @return The most recent snapshot of the playlist referenced by the provided {@link Uri}. May be
+ * null if no snapshot has been loaded yet.
+ */
+ @Nullable
+ HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback);
+
+ /**
+ * Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
+ * media playlist has been loaded.
+ */
+ long getInitialStartTimeUs();
+
+ /**
+ * Returns whether the snapshot of the playlist referenced by the provided {@link Uri} 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 Uri}.
+ * @return Whether the snapshot of the playlist referenced by the provided {@link Uri} is valid.
+ */
+ boolean isSnapshotValid(Uri url);
+
+ /**
+ * If the tracker is having trouble refreshing the master playlist or the primary playlist, this
+ * method throws the underlying error. Otherwise, does nothing.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowPrimaryPlaylistRefreshError() throws IOException;
+
+ /**
+ * If the playlist is having trouble refreshing the playlist referenced by the given {@link Uri},
+ * this method throws the underlying error.
+ *
+ * @param url The {@link Uri}.
+ * @throws IOException The underyling error.
+ */
+ void maybeThrowPlaylistRefreshError(Uri url) throws IOException;
+
+ /**
+ * Requests a playlist refresh and whitelists it.
+ *
+ * <p>The playlist tracker may choose the delay the playlist refresh. The request is discarded if
+ * a refresh was already pending.
+ *
+ * @param url The {@link Uri} of the playlist to be refreshed.
+ */
+ void refreshPlaylist(Uri url);
+
+ /**
+ * Returns whether the tracked playlists describe a live stream.
+ *
+ * @return True if the content is live. False otherwise.
+ */
+ boolean isLive();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java
new file mode 100644
index 0000000000..be9f862644
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;