summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java327
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java24
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java191
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BehindLiveWindowException.java29
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java345
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java375
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java354
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java95
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java31
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java1017
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java29
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java23
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java50
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java394
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java83
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java149
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java214
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java236
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java353
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java251
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java325
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java740
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java256
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java184
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java1162
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java327
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java472
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java926
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java79
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java77
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java283
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java253
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java227
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java423
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java371
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java142
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java141
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java40
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java486
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java150
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java439
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java66
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java100
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java75
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java80
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java137
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java220
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java41
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java791
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java111
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java157
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java119
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java112
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java68
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java104
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java61
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java120
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java129
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java39
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java338
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java85
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java668
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java35
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java92
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsManifest.java44
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java519
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java858
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java528
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java97
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java1535
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java245
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java30
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java195
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java148
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java19
-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
88 files changed, 22988 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
new file mode 100644
index 0000000000..1f67f7e645
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.util.Pair;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/** Abstract base class for the concatenation of one or more {@link Timeline}s. */
+/* package */ abstract class AbstractConcatenatedTimeline extends Timeline {
+
+ private final int childCount;
+ private final ShuffleOrder shuffleOrder;
+ private final boolean isAtomic;
+
+ /**
+ * Returns UID of child timeline from a concatenated period UID.
+ *
+ * @param concatenatedUid UID of a period in a concatenated timeline.
+ * @return UID of the child timeline this period belongs to.
+ */
+ @SuppressWarnings("nullness:return.type.incompatible")
+ public static Object getChildTimelineUidFromConcatenatedUid(Object concatenatedUid) {
+ return ((Pair<?, ?>) concatenatedUid).first;
+ }
+
+ /**
+ * Returns UID of the period in the child timeline from a concatenated period UID.
+ *
+ * @param concatenatedUid UID of a period in a concatenated timeline.
+ * @return UID of the period in the child timeline.
+ */
+ @SuppressWarnings("nullness:return.type.incompatible")
+ public static Object getChildPeriodUidFromConcatenatedUid(Object concatenatedUid) {
+ return ((Pair<?, ?>) concatenatedUid).second;
+ }
+
+ /**
+ * Returns a concatenated UID for a period or window in a child timeline.
+ *
+ * @param childTimelineUid UID of the child timeline this period or window belongs to.
+ * @param childPeriodOrWindowUid UID of the period or window in the child timeline.
+ * @return UID of the period or window in the concatenated timeline.
+ */
+ public static Object getConcatenatedUid(Object childTimelineUid, Object childPeriodOrWindowUid) {
+ return Pair.create(childTimelineUid, childPeriodOrWindowUid);
+ }
+
+ /**
+ * Sets up a concatenated timeline with a shuffle order of child timelines.
+ *
+ * @param isAtomic Whether the child timelines shall be treated as atomic, i.e., treated as a
+ * single item for repeating and shuffling.
+ * @param shuffleOrder A shuffle order of child timelines. The number of child timelines must
+ * match the number of elements in the shuffle order.
+ */
+ public AbstractConcatenatedTimeline(boolean isAtomic, ShuffleOrder shuffleOrder) {
+ this.isAtomic = isAtomic;
+ this.shuffleOrder = shuffleOrder;
+ this.childCount = shuffleOrder.getLength();
+ }
+
+ @Override
+ public int getNextWindowIndex(
+ int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
+ if (isAtomic) {
+ // Adapt repeat and shuffle mode to atomic concatenation.
+ repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;
+ shuffleModeEnabled = false;
+ }
+ // Find next window within current child.
+ int childIndex = getChildIndexByWindowIndex(windowIndex);
+ int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
+ int nextWindowIndexInChild =
+ getTimelineByChildIndex(childIndex)
+ .getNextWindowIndex(
+ windowIndex - firstWindowIndexInChild,
+ repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,
+ shuffleModeEnabled);
+ if (nextWindowIndexInChild != C.INDEX_UNSET) {
+ return firstWindowIndexInChild + nextWindowIndexInChild;
+ }
+ // If not found, find first window of next non-empty child.
+ int nextChildIndex = getNextChildIndex(childIndex, shuffleModeEnabled);
+ while (nextChildIndex != C.INDEX_UNSET && getTimelineByChildIndex(nextChildIndex).isEmpty()) {
+ nextChildIndex = getNextChildIndex(nextChildIndex, shuffleModeEnabled);
+ }
+ if (nextChildIndex != C.INDEX_UNSET) {
+ return getFirstWindowIndexByChildIndex(nextChildIndex)
+ + getTimelineByChildIndex(nextChildIndex).getFirstWindowIndex(shuffleModeEnabled);
+ }
+ // If not found, this is the last window.
+ if (repeatMode == Player.REPEAT_MODE_ALL) {
+ return getFirstWindowIndex(shuffleModeEnabled);
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getPreviousWindowIndex(
+ int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
+ if (isAtomic) {
+ // Adapt repeat and shuffle mode to atomic concatenation.
+ repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;
+ shuffleModeEnabled = false;
+ }
+ // Find previous window within current child.
+ int childIndex = getChildIndexByWindowIndex(windowIndex);
+ int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
+ int previousWindowIndexInChild =
+ getTimelineByChildIndex(childIndex)
+ .getPreviousWindowIndex(
+ windowIndex - firstWindowIndexInChild,
+ repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,
+ shuffleModeEnabled);
+ if (previousWindowIndexInChild != C.INDEX_UNSET) {
+ return firstWindowIndexInChild + previousWindowIndexInChild;
+ }
+ // If not found, find last window of previous non-empty child.
+ int previousChildIndex = getPreviousChildIndex(childIndex, shuffleModeEnabled);
+ while (previousChildIndex != C.INDEX_UNSET
+ && getTimelineByChildIndex(previousChildIndex).isEmpty()) {
+ previousChildIndex = getPreviousChildIndex(previousChildIndex, shuffleModeEnabled);
+ }
+ if (previousChildIndex != C.INDEX_UNSET) {
+ return getFirstWindowIndexByChildIndex(previousChildIndex)
+ + getTimelineByChildIndex(previousChildIndex).getLastWindowIndex(shuffleModeEnabled);
+ }
+ // If not found, this is the first window.
+ if (repeatMode == Player.REPEAT_MODE_ALL) {
+ return getLastWindowIndex(shuffleModeEnabled);
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getLastWindowIndex(boolean shuffleModeEnabled) {
+ if (childCount == 0) {
+ return C.INDEX_UNSET;
+ }
+ if (isAtomic) {
+ shuffleModeEnabled = false;
+ }
+ // Find last non-empty child.
+ int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1;
+ while (getTimelineByChildIndex(lastChildIndex).isEmpty()) {
+ lastChildIndex = getPreviousChildIndex(lastChildIndex, shuffleModeEnabled);
+ if (lastChildIndex == C.INDEX_UNSET) {
+ // All children are empty.
+ return C.INDEX_UNSET;
+ }
+ }
+ return getFirstWindowIndexByChildIndex(lastChildIndex)
+ + getTimelineByChildIndex(lastChildIndex).getLastWindowIndex(shuffleModeEnabled);
+ }
+
+ @Override
+ public int getFirstWindowIndex(boolean shuffleModeEnabled) {
+ if (childCount == 0) {
+ return C.INDEX_UNSET;
+ }
+ if (isAtomic) {
+ shuffleModeEnabled = false;
+ }
+ // Find first non-empty child.
+ int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0;
+ while (getTimelineByChildIndex(firstChildIndex).isEmpty()) {
+ firstChildIndex = getNextChildIndex(firstChildIndex, shuffleModeEnabled);
+ if (firstChildIndex == C.INDEX_UNSET) {
+ // All children are empty.
+ return C.INDEX_UNSET;
+ }
+ }
+ return getFirstWindowIndexByChildIndex(firstChildIndex)
+ + getTimelineByChildIndex(firstChildIndex).getFirstWindowIndex(shuffleModeEnabled);
+ }
+
+ @Override
+ public final Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ int childIndex = getChildIndexByWindowIndex(windowIndex);
+ int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
+ int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
+ getTimelineByChildIndex(childIndex)
+ .getWindow(windowIndex - firstWindowIndexInChild, window, defaultPositionProjectionUs);
+ Object childUid = getChildUidByChildIndex(childIndex);
+ // Don't create new objects if the child is using SINGLE_WINDOW_UID.
+ window.uid =
+ Window.SINGLE_WINDOW_UID.equals(window.uid)
+ ? childUid
+ : getConcatenatedUid(childUid, window.uid);
+ window.firstPeriodIndex += firstPeriodIndexInChild;
+ window.lastPeriodIndex += firstPeriodIndexInChild;
+ return window;
+ }
+
+ @Override
+ public final Period getPeriodByUid(Object uid, Period period) {
+ Object childUid = getChildTimelineUidFromConcatenatedUid(uid);
+ Object periodUid = getChildPeriodUidFromConcatenatedUid(uid);
+ int childIndex = getChildIndexByChildUid(childUid);
+ int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
+ getTimelineByChildIndex(childIndex).getPeriodByUid(periodUid, period);
+ period.windowIndex += firstWindowIndexInChild;
+ period.uid = uid;
+ return period;
+ }
+
+ @Override
+ public final Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ int childIndex = getChildIndexByPeriodIndex(periodIndex);
+ int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
+ int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
+ getTimelineByChildIndex(childIndex)
+ .getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds);
+ period.windowIndex += firstWindowIndexInChild;
+ if (setIds) {
+ period.uid =
+ getConcatenatedUid(
+ getChildUidByChildIndex(childIndex), Assertions.checkNotNull(period.uid));
+ }
+ return period;
+ }
+
+ @Override
+ public final int getIndexOfPeriod(Object uid) {
+ if (!(uid instanceof Pair)) {
+ return C.INDEX_UNSET;
+ }
+ Object childUid = getChildTimelineUidFromConcatenatedUid(uid);
+ Object periodUid = getChildPeriodUidFromConcatenatedUid(uid);
+ int childIndex = getChildIndexByChildUid(childUid);
+ if (childIndex == C.INDEX_UNSET) {
+ return C.INDEX_UNSET;
+ }
+ int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid);
+ return periodIndexInChild == C.INDEX_UNSET
+ ? C.INDEX_UNSET
+ : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild;
+ }
+
+ @Override
+ public final Object getUidOfPeriod(int periodIndex) {
+ int childIndex = getChildIndexByPeriodIndex(periodIndex);
+ int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
+ Object periodUidInChild =
+ getTimelineByChildIndex(childIndex).getUidOfPeriod(periodIndex - firstPeriodIndexInChild);
+ return getConcatenatedUid(getChildUidByChildIndex(childIndex), periodUidInChild);
+ }
+
+ /**
+ * Returns the index of the child timeline containing the given period index.
+ *
+ * @param periodIndex A valid period index within the bounds of the timeline.
+ */
+ protected abstract int getChildIndexByPeriodIndex(int periodIndex);
+
+ /**
+ * Returns the index of the child timeline containing the given window index.
+ *
+ * @param windowIndex A valid window index within the bounds of the timeline.
+ */
+ protected abstract int getChildIndexByWindowIndex(int windowIndex);
+
+ /**
+ * Returns the index of the child timeline with the given UID or {@link C#INDEX_UNSET} if not
+ * found.
+ *
+ * @param childUid A child UID.
+ * @return Index of child timeline or {@link C#INDEX_UNSET} if UID was not found.
+ */
+ protected abstract int getChildIndexByChildUid(Object childUid);
+
+ /**
+ * Returns the child timeline for the child with the given index.
+ *
+ * @param childIndex A valid child index within the bounds of the timeline.
+ */
+ protected abstract Timeline getTimelineByChildIndex(int childIndex);
+
+ /**
+ * Returns the first period index belonging to the child timeline with the given index.
+ *
+ * @param childIndex A valid child index within the bounds of the timeline.
+ */
+ protected abstract int getFirstPeriodIndexByChildIndex(int childIndex);
+
+ /**
+ * Returns the first window index belonging to the child timeline with the given index.
+ *
+ * @param childIndex A valid child index within the bounds of the timeline.
+ */
+ protected abstract int getFirstWindowIndexByChildIndex(int childIndex);
+
+ /**
+ * Returns the UID of the child timeline with the given index.
+ *
+ * @param childIndex A valid child index within the bounds of the timeline.
+ */
+ protected abstract Object getChildUidByChildIndex(int childIndex);
+
+ private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) {
+ return shuffleModeEnabled
+ ? shuffleOrder.getNextIndex(childIndex)
+ : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET;
+ }
+
+ private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) {
+ return shuffleModeEnabled
+ ? shuffleOrder.getPreviousIndex(childIndex)
+ : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java
new file mode 100644
index 0000000000..dba911f622
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * Interface for callbacks to be notified of {@link MediaSource} events.
+ *
+ * @deprecated Use {@link MediaSourceEventListener}.
+ */
+@Deprecated
+public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java
new file mode 100644
index 0000000000..f9ca6ff311
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java
@@ -0,0 +1,191 @@
+/*
+ * 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;
+
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayList;
+import java.util.HashSet;
+
+/**
+ * Base {@link MediaSource} implementation to handle parallel reuse and to keep a list of {@link
+ * MediaSourceEventListener}s.
+ *
+ * <p>Whenever an implementing subclass needs to provide a new timeline, it must call {@link
+ * #refreshSourceInfo(Timeline)} to notify all listeners.
+ */
+public abstract class BaseMediaSource implements MediaSource {
+
+ private final ArrayList<MediaSourceCaller> mediaSourceCallers;
+ private final HashSet<MediaSourceCaller> enabledMediaSourceCallers;
+ private final MediaSourceEventListener.EventDispatcher eventDispatcher;
+
+ @Nullable private Looper looper;
+ @Nullable private Timeline timeline;
+
+ public BaseMediaSource() {
+ mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1);
+ enabledMediaSourceCallers = new HashSet<>(/* initialCapacity= */ 1);
+ eventDispatcher = new MediaSourceEventListener.EventDispatcher();
+ }
+
+ /**
+ * Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller,
+ * TransferListener)}. This method is called at most once until the next call to {@link
+ * #releaseSourceInternal()}.
+ *
+ * @param mediaTransferListener The transfer listener which should be informed of any media data
+ * transfers. May be null if no listener is available. Note that this listener should usually
+ * be only informed of transfers related to the media loads and not of auxiliary loads for
+ * manifests and other data.
+ */
+ protected abstract void prepareSourceInternal(@Nullable TransferListener mediaTransferListener);
+
+ /** Enables the source, see {@link #enable(MediaSourceCaller)}. */
+ protected void enableInternal() {}
+
+ /** Disables the source, see {@link #disable(MediaSourceCaller)}. */
+ protected void disableInternal() {}
+
+ /**
+ * Releases the source, see {@link #releaseSource(MediaSourceCaller)}. This method is called
+ * exactly once after each call to {@link #prepareSourceInternal(TransferListener)}.
+ */
+ protected abstract void releaseSourceInternal();
+
+ /**
+ * Updates timeline and manifest and notifies all listeners of the update.
+ *
+ * @param timeline The new {@link Timeline}.
+ */
+ protected final void refreshSourceInfo(Timeline timeline) {
+ this.timeline = timeline;
+ for (MediaSourceCaller caller : mediaSourceCallers) {
+ caller.onSourceInfoRefreshed(/* source= */ this, timeline);
+ }
+ }
+
+ /**
+ * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the
+ * registered listeners with the specified media period id.
+ *
+ * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. May be null, if
+ * the events do not belong to a specific media period.
+ * @return An event dispatcher with pre-configured media period id.
+ */
+ protected final MediaSourceEventListener.EventDispatcher createEventDispatcher(
+ @Nullable MediaPeriodId mediaPeriodId) {
+ return eventDispatcher.withParameters(
+ /* windowIndex= */ 0, mediaPeriodId, /* mediaTimeOffsetMs= */ 0);
+ }
+
+ /**
+ * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the
+ * registered listeners with the specified media period id and time offset.
+ *
+ * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events.
+ * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds.
+ * @return An event dispatcher with pre-configured media period id and time offset.
+ */
+ protected final MediaSourceEventListener.EventDispatcher createEventDispatcher(
+ MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {
+ Assertions.checkArgument(mediaPeriodId != null);
+ return eventDispatcher.withParameters(/* windowIndex= */ 0, mediaPeriodId, mediaTimeOffsetMs);
+ }
+
+ /**
+ * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the
+ * registered listeners with the specified window index, media period id and time offset.
+ *
+ * @param windowIndex The timeline window index to be reported with the events.
+ * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. May be null, if
+ * the events do not belong to a specific media period.
+ * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds.
+ * @return An event dispatcher with pre-configured media period id and time offset.
+ */
+ protected final MediaSourceEventListener.EventDispatcher createEventDispatcher(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {
+ return eventDispatcher.withParameters(windowIndex, mediaPeriodId, mediaTimeOffsetMs);
+ }
+
+ /** Returns whether the source is enabled. */
+ protected final boolean isEnabled() {
+ return !enabledMediaSourceCallers.isEmpty();
+ }
+
+ @Override
+ public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
+ eventDispatcher.addEventListener(handler, eventListener);
+ }
+
+ @Override
+ public final void removeEventListener(MediaSourceEventListener eventListener) {
+ eventDispatcher.removeEventListener(eventListener);
+ }
+
+ @Override
+ public final void prepareSource(
+ MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) {
+ Looper looper = Looper.myLooper();
+ Assertions.checkArgument(this.looper == null || this.looper == looper);
+ Timeline timeline = this.timeline;
+ mediaSourceCallers.add(caller);
+ if (this.looper == null) {
+ this.looper = looper;
+ enabledMediaSourceCallers.add(caller);
+ prepareSourceInternal(mediaTransferListener);
+ } else if (timeline != null) {
+ enable(caller);
+ caller.onSourceInfoRefreshed(/* source= */ this, timeline);
+ }
+ }
+
+ @Override
+ public final void enable(MediaSourceCaller caller) {
+ Assertions.checkNotNull(looper);
+ boolean wasDisabled = enabledMediaSourceCallers.isEmpty();
+ enabledMediaSourceCallers.add(caller);
+ if (wasDisabled) {
+ enableInternal();
+ }
+ }
+
+ @Override
+ public final void disable(MediaSourceCaller caller) {
+ boolean wasEnabled = !enabledMediaSourceCallers.isEmpty();
+ enabledMediaSourceCallers.remove(caller);
+ if (wasEnabled && enabledMediaSourceCallers.isEmpty()) {
+ disableInternal();
+ }
+ }
+
+ @Override
+ public final void releaseSource(MediaSourceCaller caller) {
+ mediaSourceCallers.remove(caller);
+ if (mediaSourceCallers.isEmpty()) {
+ looper = null;
+ timeline = null;
+ enabledMediaSourceCallers.clear();
+ releaseSourceInternal();
+ } else {
+ disable(caller);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BehindLiveWindowException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BehindLiveWindowException.java
new file mode 100644
index 0000000000..d5eeeb89a6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BehindLiveWindowException.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+/**
+ * Thrown when a live playback falls behind the available media window.
+ */
+public final class BehindLiveWindowException extends IOException {
+
+ public BehindLiveWindowException() {
+ super();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
new file mode 100644
index 0000000000..7467d946cc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
@@ -0,0 +1,345 @@
+/*
+ * 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;
+
+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.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+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.Util;
+import java.io.IOException;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their
+ * samples.
+ */
+public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ /**
+ * The {@link MediaPeriod} wrapped by this clipping media period.
+ */
+ public final MediaPeriod mediaPeriod;
+
+ @Nullable private MediaPeriod.Callback callback;
+ private @NullableType ClippingSampleStream[] sampleStreams;
+ private long pendingInitialDiscontinuityPositionUs;
+ /* package */ long startUs;
+ /* package */ long endUs;
+
+ /**
+ * Creates a new clipping media period that provides a clipped view of the specified {@link
+ * MediaPeriod}'s sample streams.
+ *
+ * <p>If the start point is guaranteed to be a key frame, pass {@code false} to {@code
+ * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when the period is
+ * first read from.
+ *
+ * @param mediaPeriod The media period to clip.
+ * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled.
+ * @param startUs The clipping start time, in microseconds.
+ * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to
+ * indicate the end of the period.
+ */
+ public ClippingMediaPeriod(
+ MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity, long startUs, long endUs) {
+ this.mediaPeriod = mediaPeriod;
+ sampleStreams = new ClippingSampleStream[0];
+ pendingInitialDiscontinuityPositionUs = enableInitialDiscontinuity ? startUs : C.TIME_UNSET;
+ this.startUs = startUs;
+ this.endUs = endUs;
+ }
+
+ /**
+ * Updates the clipping start/end times for this period, in microseconds.
+ *
+ * @param startUs The clipping start time, in microseconds.
+ * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to
+ * indicate the end of the period.
+ */
+ public void updateClipping(long startUs, long endUs) {
+ this.startUs = startUs;
+ this.endUs = endUs;
+ }
+
+ @Override
+ public void prepare(MediaPeriod.Callback callback, long positionUs) {
+ this.callback = callback;
+ mediaPeriod.prepare(this, positionUs);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ mediaPeriod.maybeThrowPrepareError();
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return mediaPeriod.getTrackGroups();
+ }
+
+ @Override
+ public long selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs) {
+ sampleStreams = new ClippingSampleStream[streams.length];
+ @NullableType SampleStream[] childStreams = new SampleStream[streams.length];
+ for (int i = 0; i < streams.length; i++) {
+ sampleStreams[i] = (ClippingSampleStream) streams[i];
+ childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null;
+ }
+ long enablePositionUs =
+ mediaPeriod.selectTracks(
+ selections, mayRetainStreamFlags, childStreams, streamResetFlags, positionUs);
+ pendingInitialDiscontinuityPositionUs =
+ isPendingInitialDiscontinuity()
+ && positionUs == startUs
+ && shouldKeepInitialDiscontinuity(startUs, selections)
+ ? enablePositionUs
+ : C.TIME_UNSET;
+ Assertions.checkState(
+ enablePositionUs == positionUs
+ || (enablePositionUs >= startUs
+ && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs)));
+ for (int i = 0; i < streams.length; i++) {
+ if (childStreams[i] == null) {
+ sampleStreams[i] = null;
+ } else if (sampleStreams[i] == null || sampleStreams[i].childStream != childStreams[i]) {
+ sampleStreams[i] = new ClippingSampleStream(childStreams[i]);
+ }
+ streams[i] = sampleStreams[i];
+ }
+ return enablePositionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ mediaPeriod.discardBuffer(positionUs, toKeyframe);
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ mediaPeriod.reevaluateBuffer(positionUs);
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ if (isPendingInitialDiscontinuity()) {
+ long initialDiscontinuityUs = pendingInitialDiscontinuityPositionUs;
+ pendingInitialDiscontinuityPositionUs = C.TIME_UNSET;
+ // Always read an initial discontinuity from the child, and use it if set.
+ long childDiscontinuityUs = readDiscontinuity();
+ return childDiscontinuityUs != C.TIME_UNSET ? childDiscontinuityUs : initialDiscontinuityUs;
+ }
+ long discontinuityUs = mediaPeriod.readDiscontinuity();
+ if (discontinuityUs == C.TIME_UNSET) {
+ return C.TIME_UNSET;
+ }
+ Assertions.checkState(discontinuityUs >= startUs);
+ Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs);
+ return discontinuityUs;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
+ if (bufferedPositionUs == C.TIME_END_OF_SOURCE
+ || (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) {
+ return C.TIME_END_OF_SOURCE;
+ }
+ return bufferedPositionUs;
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ pendingInitialDiscontinuityPositionUs = C.TIME_UNSET;
+ for (ClippingSampleStream sampleStream : sampleStreams) {
+ if (sampleStream != null) {
+ sampleStream.clearSentEos();
+ }
+ }
+ long seekUs = mediaPeriod.seekToUs(positionUs);
+ Assertions.checkState(
+ seekUs == positionUs
+ || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs)));
+ return seekUs;
+ }
+
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ if (positionUs == startUs) {
+ // Never adjust seeks to the start of the clipped view.
+ return startUs;
+ }
+ SeekParameters clippedSeekParameters = clipSeekParameters(positionUs, seekParameters);
+ return mediaPeriod.getAdjustedSeekPositionUs(positionUs, clippedSeekParameters);
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE
+ || (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) {
+ return C.TIME_END_OF_SOURCE;
+ }
+ return nextLoadPositionUs;
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return mediaPeriod.continueLoading(positionUs);
+ }
+
+ @Override
+ public boolean isLoading() {
+ return mediaPeriod.isLoading();
+ }
+
+ // MediaPeriod.Callback implementation.
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ Assertions.checkNotNull(callback).onPrepared(this);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
+ }
+
+ /* package */ boolean isPendingInitialDiscontinuity() {
+ return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET;
+ }
+
+ private SeekParameters clipSeekParameters(long positionUs, SeekParameters seekParameters) {
+ long toleranceBeforeUs =
+ Util.constrainValue(
+ seekParameters.toleranceBeforeUs, /* min= */ 0, /* max= */ positionUs - startUs);
+ long toleranceAfterUs =
+ Util.constrainValue(
+ seekParameters.toleranceAfterUs,
+ /* min= */ 0,
+ /* max= */ endUs == C.TIME_END_OF_SOURCE ? Long.MAX_VALUE : endUs - positionUs);
+ if (toleranceBeforeUs == seekParameters.toleranceBeforeUs
+ && toleranceAfterUs == seekParameters.toleranceAfterUs) {
+ return seekParameters;
+ } else {
+ return new SeekParameters(toleranceBeforeUs, toleranceAfterUs);
+ }
+ }
+
+ private static boolean shouldKeepInitialDiscontinuity(
+ long startUs, @NullableType TrackSelection[] selections) {
+ // If the clipping start position is non-zero, the clipping sample streams will adjust
+ // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer
+ // timestamps can be negative, because sample streams provide buffers starting at a key-frame,
+ // which may be before the clipping start point. When the renderer reads a buffer with a
+ // negative timestamp, its offset timestamp can jump backwards compared to the last timestamp
+ // read in the previous period. Renderer implementations may not allow this, so we signal a
+ // discontinuity which resets the renderers before they read the clipping sample stream.
+ // However, for audio-only track selections we assume to have random access seek behaviour and
+ // do not need an initial discontinuity to reset the renderer.
+ if (startUs != 0) {
+ for (TrackSelection trackSelection : selections) {
+ if (trackSelection != null) {
+ Format selectedFormat = trackSelection.getSelectedFormat();
+ if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Wraps a {@link SampleStream} and clips its samples.
+ */
+ private final class ClippingSampleStream implements SampleStream {
+
+ public final SampleStream childStream;
+
+ private boolean sentEos;
+
+ public ClippingSampleStream(SampleStream childStream) {
+ this.childStream = childStream;
+ }
+
+ public void clearSentEos() {
+ sentEos = false;
+ }
+
+ @Override
+ public boolean isReady() {
+ return !isPendingInitialDiscontinuity() && childStream.isReady();
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ childStream.maybeThrowError();
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean requireFormat) {
+ if (isPendingInitialDiscontinuity()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ if (sentEos) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ }
+ int result = childStream.readData(formatHolder, buffer, requireFormat);
+ if (result == C.RESULT_FORMAT_READ) {
+ Format format = Assertions.checkNotNull(formatHolder.format);
+ if (format.encoderDelay != 0 || format.encoderPadding != 0) {
+ // Clear gapless playback metadata if the start/end points don't match the media.
+ int encoderDelay = startUs != 0 ? 0 : format.encoderDelay;
+ int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding;
+ formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding);
+ }
+ return C.RESULT_FORMAT_READ;
+ }
+ if (endUs != C.TIME_END_OF_SOURCE
+ && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs)
+ || (result == C.RESULT_NOTHING_READ
+ && getBufferedPositionUs() == C.TIME_END_OF_SOURCE
+ && !buffer.waitingForKeys))) {
+ buffer.clear();
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ sentEos = true;
+ return C.RESULT_BUFFER_READ;
+ }
+ return result;
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ if (isPendingInitialDiscontinuity()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ return childStream.skipData(positionUs);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java
new file mode 100644
index 0000000000..373076957d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.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;
+
+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.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+
+/**
+ * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end
+ * positions. The wrapped source must consist of a single period.
+ */
+public final class ClippingMediaSource extends CompositeMediaSource<Void> {
+
+ /** Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. */
+ public static final class IllegalClippingException extends IOException {
+
+ /**
+ * The reason clipping failed. One of {@link #REASON_INVALID_PERIOD_COUNT}, {@link
+ * #REASON_NOT_SEEKABLE_TO_START} or {@link #REASON_START_EXCEEDS_END}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END})
+ public @interface Reason {}
+ /** The wrapped source doesn't consist of a single period. */
+ public static final int REASON_INVALID_PERIOD_COUNT = 0;
+ /** The wrapped source is not seekable and a non-zero clipping start position was specified. */
+ public static final int REASON_NOT_SEEKABLE_TO_START = 1;
+ /** The wrapped source ends before the specified clipping start position. */
+ public static final int REASON_START_EXCEEDS_END = 2;
+
+ /** The reason clipping failed. */
+ public final @Reason int reason;
+
+ /**
+ * @param reason The reason clipping failed.
+ */
+ public IllegalClippingException(@Reason int reason) {
+ super("Illegal clipping: " + getReasonDescription(reason));
+ this.reason = reason;
+ }
+
+ private static String getReasonDescription(@Reason int reason) {
+ switch (reason) {
+ case REASON_INVALID_PERIOD_COUNT:
+ return "invalid period count";
+ case REASON_NOT_SEEKABLE_TO_START:
+ return "not seekable to start";
+ case REASON_START_EXCEEDS_END:
+ return "start exceeds end";
+ default:
+ return "unknown";
+ }
+ }
+ }
+
+ private final MediaSource mediaSource;
+ private final long startUs;
+ private final long endUs;
+ private final boolean enableInitialDiscontinuity;
+ private final boolean allowDynamicClippingUpdates;
+ private final boolean relativeToDefaultPosition;
+ private final ArrayList<ClippingMediaPeriod> mediaPeriods;
+ private final Timeline.Window window;
+
+ @Nullable private ClippingTimeline clippingTimeline;
+ @Nullable private IllegalClippingException clippingError;
+ private long periodStartUs;
+ private long periodEndUs;
+
+ /**
+ * Creates a new clipping source that wraps the specified source and provides samples between the
+ * specified start and end position.
+ *
+ * @param mediaSource The single-period source to wrap.
+ * @param startPositionUs The start position within {@code mediaSource}'s window at which to start
+ * providing samples, in microseconds.
+ * @param endPositionUs The end position within {@code mediaSource}'s window at which to stop
+ * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples
+ * from the specified start point up to the end of the source. Specifying a position that
+ * exceeds the {@code mediaSource}'s duration will also result in the end of the source not
+ * being clipped.
+ */
+ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) {
+ this(
+ mediaSource,
+ startPositionUs,
+ endPositionUs,
+ /* enableInitialDiscontinuity= */ true,
+ /* allowDynamicClippingUpdates= */ false,
+ /* relativeToDefaultPosition= */ false);
+ }
+
+ /**
+ * Creates a new clipping source that wraps the specified source and provides samples from the
+ * default position for the specified duration.
+ *
+ * @param mediaSource The single-period source to wrap.
+ * @param durationUs The duration from the default position in the window in {@code mediaSource}'s
+ * timeline at which to stop providing samples. Specifying a duration that exceeds the {@code
+ * mediaSource}'s duration will result in the end of the source not being clipped.
+ */
+ public ClippingMediaSource(MediaSource mediaSource, long durationUs) {
+ this(
+ mediaSource,
+ /* startPositionUs= */ 0,
+ /* endPositionUs= */ durationUs,
+ /* enableInitialDiscontinuity= */ true,
+ /* allowDynamicClippingUpdates= */ false,
+ /* relativeToDefaultPosition= */ true);
+ }
+
+ /**
+ * Creates a new clipping source that wraps the specified source.
+ *
+ * <p>If the start point is guaranteed to be a key frame, pass {@code false} to {@code
+ * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period is first
+ * read from.
+ *
+ * <p>For live streams, if the clipping positions should move with the live window, pass {@code
+ * true} to {@code allowDynamicClippingUpdates}. Otherwise, the live stream ends when the playback
+ * reaches {@code endPositionUs} in the last reported live window at the time a media period was
+ * created.
+ *
+ * @param mediaSource The single-period source to wrap.
+ * @param startPositionUs The start position at which to start providing samples, in microseconds.
+ * If {@code relativeToDefaultPosition} is {@code false}, this position is relative to the
+ * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition}
+ * is {@code true}, this position is relative to the default position in the window in {@code
+ * mediaSource}'s timeline.
+ * @param endPositionUs The end position at which to stop providing samples, in microseconds.
+ * Specify {@link C#TIME_END_OF_SOURCE} to provide samples from the specified start point up
+ * to the end of the source. Specifying a position that exceeds the {@code mediaSource}'s
+ * duration will also result in the end of the source not being clipped. If {@code
+ * relativeToDefaultPosition} is {@code false}, the specified position is relative to the
+ * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition}
+ * is {@code true}, this position is relative to the default position in the window in {@code
+ * mediaSource}'s timeline.
+ * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled.
+ * @param allowDynamicClippingUpdates Whether the clipping of active media periods moves with a
+ * live window. If {@code false}, playback ends when it reaches {@code endPositionUs} in the
+ * last reported live window at the time a media period was created.
+ * @param relativeToDefaultPosition Whether {@code startPositionUs} and {@code endPositionUs} are
+ * relative to the default position in the window in {@code mediaSource}'s timeline.
+ */
+ public ClippingMediaSource(
+ MediaSource mediaSource,
+ long startPositionUs,
+ long endPositionUs,
+ boolean enableInitialDiscontinuity,
+ boolean allowDynamicClippingUpdates,
+ boolean relativeToDefaultPosition) {
+ Assertions.checkArgument(startPositionUs >= 0);
+ this.mediaSource = Assertions.checkNotNull(mediaSource);
+ startUs = startPositionUs;
+ endUs = endPositionUs;
+ this.enableInitialDiscontinuity = enableInitialDiscontinuity;
+ this.allowDynamicClippingUpdates = allowDynamicClippingUpdates;
+ this.relativeToDefaultPosition = relativeToDefaultPosition;
+ mediaPeriods = new ArrayList<>();
+ window = new Timeline.Window();
+ }
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return mediaSource.getTag();
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ prepareChildSource(/* id= */ null, mediaSource);
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ if (clippingError != null) {
+ throw clippingError;
+ }
+ super.maybeThrowSourceInfoRefreshError();
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ ClippingMediaPeriod mediaPeriod =
+ new ClippingMediaPeriod(
+ mediaSource.createPeriod(id, allocator, startPositionUs),
+ enableInitialDiscontinuity,
+ periodStartUs,
+ periodEndUs);
+ mediaPeriods.add(mediaPeriod);
+ return mediaPeriod;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ Assertions.checkState(mediaPeriods.remove(mediaPeriod));
+ mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
+ if (mediaPeriods.isEmpty() && !allowDynamicClippingUpdates) {
+ refreshClippedTimeline(Assertions.checkNotNull(clippingTimeline).timeline);
+ }
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ super.releaseSourceInternal();
+ clippingError = null;
+ clippingTimeline = null;
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) {
+ if (clippingError != null) {
+ return;
+ }
+ refreshClippedTimeline(timeline);
+ }
+
+ private void refreshClippedTimeline(Timeline timeline) {
+ long windowStartUs;
+ long windowEndUs;
+ timeline.getWindow(/* windowIndex= */ 0, window);
+ long windowPositionInPeriodUs = window.getPositionInFirstPeriodUs();
+ if (clippingTimeline == null || mediaPeriods.isEmpty() || allowDynamicClippingUpdates) {
+ windowStartUs = startUs;
+ windowEndUs = endUs;
+ if (relativeToDefaultPosition) {
+ long windowDefaultPositionUs = window.getDefaultPositionUs();
+ windowStartUs += windowDefaultPositionUs;
+ windowEndUs += windowDefaultPositionUs;
+ }
+ periodStartUs = windowPositionInPeriodUs + windowStartUs;
+ periodEndUs =
+ endUs == C.TIME_END_OF_SOURCE
+ ? C.TIME_END_OF_SOURCE
+ : windowPositionInPeriodUs + windowEndUs;
+ int count = mediaPeriods.size();
+ for (int i = 0; i < count; i++) {
+ mediaPeriods.get(i).updateClipping(periodStartUs, periodEndUs);
+ }
+ } else {
+ // Keep window fixed at previous period position.
+ windowStartUs = periodStartUs - windowPositionInPeriodUs;
+ windowEndUs =
+ endUs == C.TIME_END_OF_SOURCE
+ ? C.TIME_END_OF_SOURCE
+ : periodEndUs - windowPositionInPeriodUs;
+ }
+ try {
+ clippingTimeline = new ClippingTimeline(timeline, windowStartUs, windowEndUs);
+ } catch (IllegalClippingException e) {
+ clippingError = e;
+ return;
+ }
+ refreshSourceInfo(clippingTimeline);
+ }
+
+ @Override
+ protected long getMediaTimeForChildMediaTime(Void id, long mediaTimeMs) {
+ if (mediaTimeMs == C.TIME_UNSET) {
+ return C.TIME_UNSET;
+ }
+ long startMs = C.usToMs(startUs);
+ long clippedTimeMs = Math.max(0, mediaTimeMs - startMs);
+ if (endUs != C.TIME_END_OF_SOURCE) {
+ clippedTimeMs = Math.min(C.usToMs(endUs) - startMs, clippedTimeMs);
+ }
+ return clippedTimeMs;
+ }
+
+ /**
+ * Provides a clipped view of a specified timeline.
+ */
+ private static final class ClippingTimeline extends ForwardingTimeline {
+
+ private final long startUs;
+ private final long endUs;
+ private final long durationUs;
+ private final boolean isDynamic;
+
+ /**
+ * Creates a new clipping timeline that wraps the specified timeline.
+ *
+ * @param timeline The timeline to clip.
+ * @param startUs The number of microseconds to clip from the start of {@code timeline}.
+ * @param endUs The end position in microseconds for the clipped timeline relative to the start
+ * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end.
+ * @throws IllegalClippingException If the timeline could not be clipped.
+ */
+ public ClippingTimeline(Timeline timeline, long startUs, long endUs)
+ throws IllegalClippingException {
+ super(timeline);
+ if (timeline.getPeriodCount() != 1) {
+ throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT);
+ }
+ Window window = timeline.getWindow(0, new Window());
+ startUs = Math.max(0, startUs);
+ long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : Math.max(0, endUs);
+ if (window.durationUs != C.TIME_UNSET) {
+ if (resolvedEndUs > window.durationUs) {
+ resolvedEndUs = window.durationUs;
+ }
+ if (startUs != 0 && !window.isSeekable) {
+ throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START);
+ }
+ if (startUs > resolvedEndUs) {
+ throw new IllegalClippingException(IllegalClippingException.REASON_START_EXCEEDS_END);
+ }
+ }
+ this.startUs = startUs;
+ this.endUs = resolvedEndUs;
+ durationUs = resolvedEndUs == C.TIME_UNSET ? C.TIME_UNSET : (resolvedEndUs - startUs);
+ isDynamic =
+ window.isDynamic
+ && (resolvedEndUs == C.TIME_UNSET
+ || (window.durationUs != C.TIME_UNSET && resolvedEndUs == window.durationUs));
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ timeline.getWindow(/* windowIndex= */ 0, window, /* defaultPositionProjectionUs= */ 0);
+ window.positionInFirstPeriodUs += startUs;
+ window.durationUs = durationUs;
+ window.isDynamic = isDynamic;
+ if (window.defaultPositionUs != C.TIME_UNSET) {
+ window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs);
+ window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs
+ : Math.min(window.defaultPositionUs, endUs);
+ window.defaultPositionUs -= startUs;
+ }
+ long startMs = C.usToMs(startUs);
+ if (window.presentationStartTimeMs != C.TIME_UNSET) {
+ window.presentationStartTimeMs += startMs;
+ }
+ if (window.windowStartTimeMs != C.TIME_UNSET) {
+ window.windowStartTimeMs += startMs;
+ }
+ return window;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ timeline.getPeriod(/* periodIndex= */ 0, period, setIds);
+ long positionInClippedWindowUs = period.getPositionInWindowUs() - startUs;
+ long periodDurationUs =
+ durationUs == C.TIME_UNSET ? C.TIME_UNSET : durationUs - positionInClippedWindowUs;
+ return period.set(
+ period.id, period.uid, /* windowIndex= */ 0, periodDurationUs, positionInClippedWindowUs);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java
new file mode 100644
index 0000000000..ed46b8ee94
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java
@@ -0,0 +1,354 @@
+/*
+ * 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;
+
+import android.os.Handler;
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Composite {@link MediaSource} consisting of multiple child sources.
+ *
+ * @param <T> The type of the id used to identify prepared child sources.
+ */
+public abstract class CompositeMediaSource<T> extends BaseMediaSource {
+
+ private final HashMap<T, MediaSourceAndListener> childSources;
+
+ @Nullable private Handler eventHandler;
+ @Nullable private TransferListener mediaTransferListener;
+
+ /** Creates composite media source without child sources. */
+ protected CompositeMediaSource() {
+ childSources = new HashMap<>();
+ }
+
+ @Override
+ @CallSuper
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ this.mediaTransferListener = mediaTransferListener;
+ eventHandler = new Handler();
+ }
+
+ @Override
+ @CallSuper
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ for (MediaSourceAndListener childSource : childSources.values()) {
+ childSource.mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ }
+
+ @Override
+ @CallSuper
+ protected void enableInternal() {
+ for (MediaSourceAndListener childSource : childSources.values()) {
+ childSource.mediaSource.enable(childSource.caller);
+ }
+ }
+
+ @Override
+ @CallSuper
+ protected void disableInternal() {
+ for (MediaSourceAndListener childSource : childSources.values()) {
+ childSource.mediaSource.disable(childSource.caller);
+ }
+ }
+
+ @Override
+ @CallSuper
+ protected void releaseSourceInternal() {
+ for (MediaSourceAndListener childSource : childSources.values()) {
+ childSource.mediaSource.releaseSource(childSource.caller);
+ childSource.mediaSource.removeEventListener(childSource.eventListener);
+ }
+ childSources.clear();
+ }
+
+ /**
+ * Called when the source info of a child source has been refreshed.
+ *
+ * @param id The unique id used to prepare the child source.
+ * @param mediaSource The child source whose source info has been refreshed.
+ * @param timeline The timeline of the child source.
+ */
+ protected abstract void onChildSourceInfoRefreshed(
+ T id, MediaSource mediaSource, Timeline timeline);
+
+ /**
+ * Prepares a child source.
+ *
+ * <p>{@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline)} will be called when the
+ * child source updates its timeline with the same {@code id} passed to this method.
+ *
+ * <p>Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)}
+ * will be released in {@link #releaseSourceInternal()}.
+ *
+ * @param id A unique id to identify the child source preparation. Null is allowed as an id.
+ * @param mediaSource The child {@link MediaSource}.
+ */
+ protected final void prepareChildSource(final T id, MediaSource mediaSource) {
+ Assertions.checkArgument(!childSources.containsKey(id));
+ MediaSourceCaller caller =
+ (source, timeline) -> onChildSourceInfoRefreshed(id, source, timeline);
+ MediaSourceEventListener eventListener = new ForwardingEventListener(id);
+ childSources.put(id, new MediaSourceAndListener(mediaSource, caller, eventListener));
+ mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener);
+ mediaSource.prepareSource(caller, mediaTransferListener);
+ if (!isEnabled()) {
+ mediaSource.disable(caller);
+ }
+ }
+
+ /**
+ * Enables a child source.
+ *
+ * @param id The unique id used to prepare the child source.
+ */
+ protected final void enableChildSource(final T id) {
+ MediaSourceAndListener enabledChild = Assertions.checkNotNull(childSources.get(id));
+ enabledChild.mediaSource.enable(enabledChild.caller);
+ }
+
+ /**
+ * Disables a child source.
+ *
+ * @param id The unique id used to prepare the child source.
+ */
+ protected final void disableChildSource(final T id) {
+ MediaSourceAndListener disabledChild = Assertions.checkNotNull(childSources.get(id));
+ disabledChild.mediaSource.disable(disabledChild.caller);
+ }
+
+ /**
+ * Releases a child source.
+ *
+ * @param id The unique id used to prepare the child source.
+ */
+ protected final void releaseChildSource(T id) {
+ MediaSourceAndListener removedChild = Assertions.checkNotNull(childSources.remove(id));
+ removedChild.mediaSource.releaseSource(removedChild.caller);
+ removedChild.mediaSource.removeEventListener(removedChild.eventListener);
+ }
+
+ /**
+ * Returns the window index in the composite source corresponding to the specified window index in
+ * a child source. The default implementation does not change the window index.
+ *
+ * @param id The unique id used to prepare the child source.
+ * @param windowIndex A window index of the child source.
+ * @return The corresponding window index in the composite source.
+ */
+ protected int getWindowIndexForChildWindowIndex(T id, int windowIndex) {
+ return windowIndex;
+ }
+
+ /**
+ * Returns the {@link MediaPeriodId} in the composite source corresponding to the specified {@link
+ * MediaPeriodId} in a child source. The default implementation does not change the media period
+ * id.
+ *
+ * @param id The unique id used to prepare the child source.
+ * @param mediaPeriodId A {@link MediaPeriodId} of the child source.
+ * @return The corresponding {@link MediaPeriodId} in the composite source. Null if no
+ * corresponding media period id can be determined.
+ */
+ protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
+ T id, MediaPeriodId mediaPeriodId) {
+ return mediaPeriodId;
+ }
+
+ /**
+ * Returns the media time in the composite source corresponding to the specified media time in a
+ * child source. The default implementation does not change the media time.
+ *
+ * @param id The unique id used to prepare the child source.
+ * @param mediaTimeMs A media time of the child source, in milliseconds.
+ * @return The corresponding media time in the composite source, in milliseconds.
+ */
+ protected long getMediaTimeForChildMediaTime(@Nullable T id, long mediaTimeMs) {
+ return mediaTimeMs;
+ }
+
+ /**
+ * Returns whether {@link MediaSourceEventListener#onMediaPeriodCreated(int, MediaPeriodId)} and
+ * {@link MediaSourceEventListener#onMediaPeriodReleased(int, MediaPeriodId)} events of the given
+ * media period should be reported. The default implementation is to always report these events.
+ *
+ * @param mediaPeriodId A {@link MediaPeriodId} in the composite media source.
+ * @return Whether create and release events for this media period should be reported.
+ */
+ protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) {
+ return true;
+ }
+
+ private static final class MediaSourceAndListener {
+
+ public final MediaSource mediaSource;
+ public final MediaSourceCaller caller;
+ public final MediaSourceEventListener eventListener;
+
+ public MediaSourceAndListener(
+ MediaSource mediaSource, MediaSourceCaller caller, MediaSourceEventListener eventListener) {
+ this.mediaSource = mediaSource;
+ this.caller = caller;
+ this.eventListener = eventListener;
+ }
+ }
+
+ private final class ForwardingEventListener implements MediaSourceEventListener {
+
+ private final T id;
+ private EventDispatcher eventDispatcher;
+
+ public ForwardingEventListener(T id) {
+ this.eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
+ this.id = id;
+ }
+
+ @Override
+ public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
+ if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
+ if (shouldDispatchCreateOrReleaseEvent(
+ Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) {
+ eventDispatcher.mediaPeriodCreated();
+ }
+ }
+ }
+
+ @Override
+ public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {
+ if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
+ if (shouldDispatchCreateOrReleaseEvent(
+ Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) {
+ eventDispatcher.mediaPeriodReleased();
+ }
+ }
+ }
+
+ @Override
+ public void onLoadStarted(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventData,
+ MediaLoadData mediaLoadData) {
+ if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
+ eventDispatcher.loadStarted(loadEventData, maybeUpdateMediaLoadData(mediaLoadData));
+ }
+ }
+
+ @Override
+ public void onLoadCompleted(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventData,
+ MediaLoadData mediaLoadData) {
+ if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
+ eventDispatcher.loadCompleted(loadEventData, maybeUpdateMediaLoadData(mediaLoadData));
+ }
+ }
+
+ @Override
+ public void onLoadCanceled(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventData,
+ MediaLoadData mediaLoadData) {
+ if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
+ eventDispatcher.loadCanceled(loadEventData, maybeUpdateMediaLoadData(mediaLoadData));
+ }
+ }
+
+ @Override
+ public void onLoadError(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventData,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {
+ if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
+ eventDispatcher.loadError(
+ loadEventData, maybeUpdateMediaLoadData(mediaLoadData), error, wasCanceled);
+ }
+ }
+
+ @Override
+ public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {
+ if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
+ eventDispatcher.readingStarted();
+ }
+ }
+
+ @Override
+ public void onUpstreamDiscarded(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
+ if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
+ eventDispatcher.upstreamDiscarded(maybeUpdateMediaLoadData(mediaLoadData));
+ }
+ }
+
+ @Override
+ public void onDownstreamFormatChanged(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
+ if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
+ eventDispatcher.downstreamFormatChanged(maybeUpdateMediaLoadData(mediaLoadData));
+ }
+ }
+
+ /** Updates the event dispatcher and returns whether the event should be dispatched. */
+ private boolean maybeUpdateEventDispatcher(
+ int childWindowIndex, @Nullable MediaPeriodId childMediaPeriodId) {
+ MediaPeriodId mediaPeriodId = null;
+ if (childMediaPeriodId != null) {
+ mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);
+ if (mediaPeriodId == null) {
+ // Media period not found. Ignore event.
+ return false;
+ }
+ }
+ int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);
+ if (eventDispatcher.windowIndex != windowIndex
+ || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) {
+ eventDispatcher =
+ createEventDispatcher(windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0);
+ }
+ return true;
+ }
+
+ private MediaLoadData maybeUpdateMediaLoadData(MediaLoadData mediaLoadData) {
+ long mediaStartTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaStartTimeMs);
+ long mediaEndTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaEndTimeMs);
+ if (mediaStartTimeMs == mediaLoadData.mediaStartTimeMs
+ && mediaEndTimeMs == mediaLoadData.mediaEndTimeMs) {
+ return mediaLoadData;
+ }
+ return new MediaLoadData(
+ mediaLoadData.dataType,
+ mediaLoadData.trackType,
+ mediaLoadData.trackFormat,
+ mediaLoadData.trackSelectionReason,
+ mediaLoadData.trackSelectionData,
+ mediaStartTimeMs,
+ mediaEndTimeMs);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
new file mode 100644
index 0000000000..9a72903528
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
@@ -0,0 +1,95 @@
+/*
+ * 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+
+/**
+ * A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s.
+ */
+public class CompositeSequenceableLoader implements SequenceableLoader {
+
+ protected final SequenceableLoader[] loaders;
+
+ public CompositeSequenceableLoader(SequenceableLoader[] loaders) {
+ this.loaders = loaders;
+ }
+
+ @Override
+ public final long getBufferedPositionUs() {
+ long bufferedPositionUs = Long.MAX_VALUE;
+ for (SequenceableLoader loader : loaders) {
+ long loaderBufferedPositionUs = loader.getBufferedPositionUs();
+ if (loaderBufferedPositionUs != C.TIME_END_OF_SOURCE) {
+ bufferedPositionUs = Math.min(bufferedPositionUs, loaderBufferedPositionUs);
+ }
+ }
+ return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+ }
+
+ @Override
+ public final long getNextLoadPositionUs() {
+ long nextLoadPositionUs = Long.MAX_VALUE;
+ for (SequenceableLoader loader : loaders) {
+ long loaderNextLoadPositionUs = loader.getNextLoadPositionUs();
+ if (loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE) {
+ nextLoadPositionUs = Math.min(nextLoadPositionUs, loaderNextLoadPositionUs);
+ }
+ }
+ return nextLoadPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : nextLoadPositionUs;
+ }
+
+ @Override
+ public final void reevaluateBuffer(long positionUs) {
+ for (SequenceableLoader loader : loaders) {
+ loader.reevaluateBuffer(positionUs);
+ }
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ boolean madeProgress = false;
+ boolean madeProgressThisIteration;
+ do {
+ madeProgressThisIteration = false;
+ long nextLoadPositionUs = getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+ break;
+ }
+ for (SequenceableLoader loader : loaders) {
+ long loaderNextLoadPositionUs = loader.getNextLoadPositionUs();
+ boolean isLoaderBehind =
+ loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE
+ && loaderNextLoadPositionUs <= positionUs;
+ if (loaderNextLoadPositionUs == nextLoadPositionUs || isLoaderBehind) {
+ madeProgressThisIteration |= loader.continueLoading(positionUs);
+ }
+ }
+ madeProgress |= madeProgressThisIteration;
+ } while (madeProgressThisIteration);
+ return madeProgress;
+ }
+
+ @Override
+ public boolean isLoading() {
+ for (SequenceableLoader loader : loaders) {
+ if (loader.isLoading()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java
new file mode 100644
index 0000000000..1ac76d6167
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * A factory to create composite {@link SequenceableLoader}s.
+ */
+public interface CompositeSequenceableLoaderFactory {
+
+ /**
+ * Creates a composite {@link SequenceableLoader}.
+ *
+ * @param loaders The sub-loaders that make up the {@link SequenceableLoader} to be built.
+ * @return A composite {@link SequenceableLoader} that comprises the given loaders.
+ */
+ SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
new file mode 100644
index 0000000000..aa6f486473
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -0,0 +1,1017 @@
+/*
+ * 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;
+
+import android.os.Handler;
+import android.os.Message;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
+ * during playback. It is valid for the same {@link MediaSource} instance to be present more than
+ * once in the concatenation. Access to this class is thread-safe.
+ */
+public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder> {
+
+ private static final int MSG_ADD = 0;
+ private static final int MSG_REMOVE = 1;
+ private static final int MSG_MOVE = 2;
+ private static final int MSG_SET_SHUFFLE_ORDER = 3;
+ private static final int MSG_UPDATE_TIMELINE = 4;
+ private static final int MSG_ON_COMPLETION = 5;
+
+ // Accessed on any thread.
+ @GuardedBy("this")
+ private final List<MediaSourceHolder> mediaSourcesPublic;
+
+ @GuardedBy("this")
+ private final Set<HandlerAndRunnable> pendingOnCompletionActions;
+
+ @GuardedBy("this")
+ @Nullable
+ private Handler playbackThreadHandler;
+
+ // Accessed on the playback thread only.
+ private final List<MediaSourceHolder> mediaSourceHolders;
+ private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
+ private final Map<Object, MediaSourceHolder> mediaSourceByUid;
+ private final Set<MediaSourceHolder> enabledMediaSourceHolders;
+ private final boolean isAtomic;
+ private final boolean useLazyPreparation;
+
+ private boolean timelineUpdateScheduled;
+ private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;
+ private ShuffleOrder shuffleOrder;
+
+ /**
+ * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same
+ * {@link MediaSource} instance to be present more than once in the array.
+ */
+ public ConcatenatingMediaSource(MediaSource... mediaSources) {
+ this(/* isAtomic= */ false, mediaSources);
+ }
+
+ /**
+ * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
+ * as a single item for repeating and shuffling.
+ * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
+ * MediaSource} instance to be present more than once in the array.
+ */
+ public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) {
+ this(isAtomic, new DefaultShuffleOrder(0), mediaSources);
+ }
+
+ /**
+ * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
+ * as a single item for repeating and shuffling.
+ * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.
+ * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
+ * MediaSource} instance to be present more than once in the array.
+ */
+ public ConcatenatingMediaSource(
+ boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource... mediaSources) {
+ this(isAtomic, /* useLazyPreparation= */ false, shuffleOrder, mediaSources);
+ }
+
+ /**
+ * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
+ * as a single item for repeating and shuffling.
+ * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest
+ * loads and other initial preparation steps happen immediately. If true, these initial
+ * preparations are triggered only when the player starts buffering the media.
+ * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.
+ * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
+ * MediaSource} instance to be present more than once in the array.
+ */
+ @SuppressWarnings("initialization")
+ public ConcatenatingMediaSource(
+ boolean isAtomic,
+ boolean useLazyPreparation,
+ ShuffleOrder shuffleOrder,
+ MediaSource... mediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ Assertions.checkNotNull(mediaSource);
+ }
+ this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
+ this.mediaSourceByMediaPeriod = new IdentityHashMap<>();
+ this.mediaSourceByUid = new HashMap<>();
+ this.mediaSourcesPublic = new ArrayList<>();
+ this.mediaSourceHolders = new ArrayList<>();
+ this.nextTimelineUpdateOnCompletionActions = new HashSet<>();
+ this.pendingOnCompletionActions = new HashSet<>();
+ this.enabledMediaSourceHolders = new HashSet<>();
+ this.isAtomic = isAtomic;
+ this.useLazyPreparation = useLazyPreparation;
+ addMediaSources(Arrays.asList(mediaSources));
+ }
+
+ /**
+ * Appends a {@link MediaSource} to the playlist.
+ *
+ * @param mediaSource The {@link MediaSource} to be added to the list.
+ */
+ public synchronized void addMediaSource(MediaSource mediaSource) {
+ addMediaSource(mediaSourcesPublic.size(), mediaSource);
+ }
+
+ /**
+ * Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
+ *
+ * @param mediaSource The {@link MediaSource} to be added to the list.
+ * @param handler The {@link Handler} to run {@code onCompletionAction}.
+ * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
+ * source has been added to the playlist.
+ */
+ public synchronized void addMediaSource(
+ MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
+ addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction);
+ }
+
+ /**
+ * Adds a {@link MediaSource} to the playlist.
+ *
+ * @param index The index at which the new {@link MediaSource} will be inserted. This index must
+ * be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
+ * @param mediaSource The {@link MediaSource} to be added to the list.
+ */
+ public synchronized void addMediaSource(int index, MediaSource mediaSource) {
+ addPublicMediaSources(
+ index,
+ Collections.singletonList(mediaSource),
+ /* handler= */ null,
+ /* onCompletionAction= */ null);
+ }
+
+ /**
+ * Adds a {@link MediaSource} to the playlist and executes a custom action on completion.
+ *
+ * @param index The index at which the new {@link MediaSource} will be inserted. This index must
+ * be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
+ * @param mediaSource The {@link MediaSource} to be added to the list.
+ * @param handler The {@link Handler} to run {@code onCompletionAction}.
+ * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
+ * source has been added to the playlist.
+ */
+ public synchronized void addMediaSource(
+ int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
+ addPublicMediaSources(
+ index, Collections.singletonList(mediaSource), handler, onCompletionAction);
+ }
+
+ /**
+ * Appends multiple {@link MediaSource}s to the playlist.
+ *
+ * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
+ * sources are added in the order in which they appear in this collection.
+ */
+ public synchronized void addMediaSources(Collection<MediaSource> mediaSources) {
+ addPublicMediaSources(
+ mediaSourcesPublic.size(),
+ mediaSources,
+ /* handler= */ null,
+ /* onCompletionAction= */ null);
+ }
+
+ /**
+ * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on
+ * completion.
+ *
+ * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
+ * sources are added in the order in which they appear in this collection.
+ * @param handler The {@link Handler} to run {@code onCompletionAction}.
+ * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
+ * sources have been added to the playlist.
+ */
+ public synchronized void addMediaSources(
+ Collection<MediaSource> mediaSources, Handler handler, Runnable onCompletionAction) {
+ addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction);
+ }
+
+ /**
+ * Adds multiple {@link MediaSource}s to the playlist.
+ *
+ * @param index The index at which the new {@link MediaSource}s will be inserted. This index must
+ * be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
+ * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
+ * sources are added in the order in which they appear in this collection.
+ */
+ public synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
+ addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null);
+ }
+
+ /**
+ * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion.
+ *
+ * @param index The index at which the new {@link MediaSource}s will be inserted. This index must
+ * be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
+ * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
+ * sources are added in the order in which they appear in this collection.
+ * @param handler The {@link Handler} to run {@code onCompletionAction}.
+ * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
+ * sources have been added to the playlist.
+ */
+ public synchronized void addMediaSources(
+ int index,
+ Collection<MediaSource> mediaSources,
+ Handler handler,
+ Runnable onCompletionAction) {
+ addPublicMediaSources(index, mediaSources, handler, onCompletionAction);
+ }
+
+ /**
+ * Removes a {@link MediaSource} from the playlist.
+ *
+ * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
+ * int)} instead.
+ *
+ * <p>Note: If you want to remove a set of contiguous sources, it's preferable to use {@link
+ * #removeMediaSourceRange(int, int)} instead.
+ *
+ * @param index The index at which the media source will be removed. This index must be in the
+ * range of 0 &lt;= index &lt; {@link #getSize()}.
+ * @return The removed {@link MediaSource}.
+ */
+ public synchronized MediaSource removeMediaSource(int index) {
+ MediaSource removedMediaSource = getMediaSource(index);
+ removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null);
+ return removedMediaSource;
+ }
+
+ /**
+ * Removes a {@link MediaSource} from the playlist and executes a custom action on completion.
+ *
+ * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
+ * int, Handler, Runnable)} instead.
+ *
+ * <p>Note: If you want to remove a set of contiguous sources, it's preferable to use {@link
+ * #removeMediaSourceRange(int, int, Handler, Runnable)} instead.
+ *
+ * @param index The index at which the media source will be removed. This index must be in the
+ * range of 0 &lt;= index &lt; {@link #getSize()}.
+ * @param handler The {@link Handler} to run {@code onCompletionAction}.
+ * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
+ * source has been removed from the playlist.
+ * @return The removed {@link MediaSource}.
+ */
+ public synchronized MediaSource removeMediaSource(
+ int index, Handler handler, Runnable onCompletionAction) {
+ MediaSource removedMediaSource = getMediaSource(index);
+ removePublicMediaSources(index, index + 1, handler, onCompletionAction);
+ return removedMediaSource;
+ }
+
+ /**
+ * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index
+ * (included) and a final index (excluded).
+ *
+ * <p>Note: when specified range is empty, no actual media source is removed and no exception is
+ * thrown.
+ *
+ * @param fromIndex The initial range index, pointing to the first media source that will be
+ * removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
+ * @param toIndex The final range index, pointing to the first media source that will be left
+ * untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
+ * @throws IndexOutOfBoundsException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
+ * {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}
+ */
+ public synchronized void removeMediaSourceRange(int fromIndex, int toIndex) {
+ removePublicMediaSources(
+ fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null);
+ }
+
+ /**
+ * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index
+ * (included) and a final index (excluded), and executes a custom action on completion.
+ *
+ * <p>Note: when specified range is empty, no actual media source is removed and no exception is
+ * thrown.
+ *
+ * @param fromIndex The initial range index, pointing to the first media source that will be
+ * removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
+ * @param toIndex The final range index, pointing to the first media source that will be left
+ * untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
+ * @param handler The {@link Handler} to run {@code onCompletionAction}.
+ * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
+ * source range has been removed from the playlist.
+ * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
+ * {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}
+ */
+ public synchronized void removeMediaSourceRange(
+ int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) {
+ removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction);
+ }
+
+ /**
+ * Moves an existing {@link MediaSource} within the playlist.
+ *
+ * @param currentIndex The current index of the media source in the playlist. This index must be
+ * in the range of 0 &lt;= index &lt; {@link #getSize()}.
+ * @param newIndex The target index of the media source in the playlist. This index must be in the
+ * range of 0 &lt;= index &lt; {@link #getSize()}.
+ */
+ public synchronized void moveMediaSource(int currentIndex, int newIndex) {
+ movePublicMediaSource(
+ currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null);
+ }
+
+ /**
+ * Moves an existing {@link MediaSource} within the playlist and executes a custom action on
+ * completion.
+ *
+ * @param currentIndex The current index of the media source in the playlist. This index must be
+ * in the range of 0 &lt;= index &lt; {@link #getSize()}.
+ * @param newIndex The target index of the media source in the playlist. This index must be in the
+ * range of 0 &lt;= index &lt; {@link #getSize()}.
+ * @param handler The {@link Handler} to run {@code onCompletionAction}.
+ * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
+ * source has been moved.
+ */
+ public synchronized void moveMediaSource(
+ int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) {
+ movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction);
+ }
+
+ /** Clears the playlist. */
+ public synchronized void clear() {
+ removeMediaSourceRange(0, getSize());
+ }
+
+ /**
+ * Clears the playlist and executes a custom action on completion.
+ *
+ * @param handler The {@link Handler} to run {@code onCompletionAction}.
+ * @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist
+ * has been cleared.
+ */
+ public synchronized void clear(Handler handler, Runnable onCompletionAction) {
+ removeMediaSourceRange(0, getSize(), handler, onCompletionAction);
+ }
+
+ /** Returns the number of media sources in the playlist. */
+ public synchronized int getSize() {
+ return mediaSourcesPublic.size();
+ }
+
+ /**
+ * Returns the {@link MediaSource} at a specified index.
+ *
+ * @param index An index in the range of 0 &lt;= index &lt;= {@link #getSize()}.
+ * @return The {@link MediaSource} at this index.
+ */
+ public synchronized MediaSource getMediaSource(int index) {
+ return mediaSourcesPublic.get(index).mediaSource;
+ }
+
+ /**
+ * Sets a new shuffle order to use when shuffling the child media sources.
+ *
+ * @param shuffleOrder A {@link ShuffleOrder}.
+ */
+ public synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) {
+ setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null);
+ }
+
+ /**
+ * Sets a new shuffle order to use when shuffling the child media sources.
+ *
+ * @param shuffleOrder A {@link ShuffleOrder}.
+ * @param handler The {@link Handler} to run {@code onCompletionAction}.
+ * @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle
+ * order has been changed.
+ */
+ public synchronized void setShuffleOrder(
+ ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) {
+ setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction);
+ }
+
+ // CompositeMediaSource implementation.
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return null;
+ }
+
+ @Override
+ protected synchronized void prepareSourceInternal(
+ @Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
+ if (mediaSourcesPublic.isEmpty()) {
+ updateTimelineAndScheduleOnCompletionActions();
+ } else {
+ shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
+ addMediaSourcesInternal(0, mediaSourcesPublic);
+ scheduleTimelineUpdate();
+ }
+ }
+
+ @SuppressWarnings("MissingSuperCall")
+ @Override
+ protected void enableInternal() {
+ // Suppress enabling all child sources here as they can be lazily enabled when creating periods.
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
+ MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
+ MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);
+ if (holder == null) {
+ // Stale event. The media source has already been removed.
+ holder = new MediaSourceHolder(new DummyMediaSource(), useLazyPreparation);
+ holder.isRemoved = true;
+ prepareChildSource(holder, holder.mediaSource);
+ }
+ enableMediaSource(holder);
+ holder.activeMediaPeriodIds.add(childMediaPeriodId);
+ MediaPeriod mediaPeriod =
+ holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
+ mediaSourceByMediaPeriod.put(mediaPeriod, holder);
+ disableUnusedMediaSources();
+ return mediaPeriod;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ MediaSourceHolder holder =
+ Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
+ holder.mediaSource.releasePeriod(mediaPeriod);
+ holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
+ if (!mediaSourceByMediaPeriod.isEmpty()) {
+ disableUnusedMediaSources();
+ }
+ maybeReleaseChildSource(holder);
+ }
+
+ @Override
+ protected void disableInternal() {
+ super.disableInternal();
+ enabledMediaSourceHolders.clear();
+ }
+
+ @Override
+ protected synchronized void releaseSourceInternal() {
+ super.releaseSourceInternal();
+ mediaSourceHolders.clear();
+ enabledMediaSourceHolders.clear();
+ mediaSourceByUid.clear();
+ shuffleOrder = shuffleOrder.cloneAndClear();
+ if (playbackThreadHandler != null) {
+ playbackThreadHandler.removeCallbacksAndMessages(null);
+ playbackThreadHandler = null;
+ }
+ timelineUpdateScheduled = false;
+ nextTimelineUpdateOnCompletionActions.clear();
+ dispatchOnCompletionActions(pendingOnCompletionActions);
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, Timeline timeline) {
+ updateMediaSourceInternal(mediaSourceHolder, timeline);
+ }
+
+ @Override
+ @Nullable
+ protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
+ MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) {
+ for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) {
+ // Ensure the reported media period id has the same window sequence number as the one created
+ // by this media source. Otherwise it does not belong to this child source.
+ if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber
+ == mediaPeriodId.windowSequenceNumber) {
+ Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid);
+ return mediaPeriodId.copyWithPeriodUid(periodUid);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected int getWindowIndexForChildWindowIndex(
+ MediaSourceHolder mediaSourceHolder, int windowIndex) {
+ return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
+ }
+
+ // Internal methods. Called from any thread.
+
+ @GuardedBy("this")
+ private void addPublicMediaSources(
+ int index,
+ Collection<MediaSource> mediaSources,
+ @Nullable Handler handler,
+ @Nullable Runnable onCompletionAction) {
+ Assertions.checkArgument((handler == null) == (onCompletionAction == null));
+ Handler playbackThreadHandler = this.playbackThreadHandler;
+ for (MediaSource mediaSource : mediaSources) {
+ Assertions.checkNotNull(mediaSource);
+ }
+ List<MediaSourceHolder> mediaSourceHolders = new ArrayList<>(mediaSources.size());
+ for (MediaSource mediaSource : mediaSources) {
+ mediaSourceHolders.add(new MediaSourceHolder(mediaSource, useLazyPreparation));
+ }
+ mediaSourcesPublic.addAll(index, mediaSourceHolders);
+ if (playbackThreadHandler != null && !mediaSources.isEmpty()) {
+ HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
+ playbackThreadHandler
+ .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction))
+ .sendToTarget();
+ } else if (onCompletionAction != null && handler != null) {
+ handler.post(onCompletionAction);
+ }
+ }
+
+ @GuardedBy("this")
+ private void removePublicMediaSources(
+ int fromIndex,
+ int toIndex,
+ @Nullable Handler handler,
+ @Nullable Runnable onCompletionAction) {
+ Assertions.checkArgument((handler == null) == (onCompletionAction == null));
+ Handler playbackThreadHandler = this.playbackThreadHandler;
+ Util.removeRange(mediaSourcesPublic, fromIndex, toIndex);
+ if (playbackThreadHandler != null) {
+ HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
+ playbackThreadHandler
+ .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction))
+ .sendToTarget();
+ } else if (onCompletionAction != null && handler != null) {
+ handler.post(onCompletionAction);
+ }
+ }
+
+ @GuardedBy("this")
+ private void movePublicMediaSource(
+ int currentIndex,
+ int newIndex,
+ @Nullable Handler handler,
+ @Nullable Runnable onCompletionAction) {
+ Assertions.checkArgument((handler == null) == (onCompletionAction == null));
+ Handler playbackThreadHandler = this.playbackThreadHandler;
+ mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
+ if (playbackThreadHandler != null) {
+ HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
+ playbackThreadHandler
+ .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction))
+ .sendToTarget();
+ } else if (onCompletionAction != null && handler != null) {
+ handler.post(onCompletionAction);
+ }
+ }
+
+ @GuardedBy("this")
+ private void setPublicShuffleOrder(
+ ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) {
+ Assertions.checkArgument((handler == null) == (onCompletionAction == null));
+ Handler playbackThreadHandler = this.playbackThreadHandler;
+ if (playbackThreadHandler != null) {
+ int size = getSize();
+ if (shuffleOrder.getLength() != size) {
+ shuffleOrder =
+ shuffleOrder
+ .cloneAndClear()
+ .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
+ }
+ HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
+ playbackThreadHandler
+ .obtainMessage(
+ MSG_SET_SHUFFLE_ORDER,
+ new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction))
+ .sendToTarget();
+ } else {
+ this.shuffleOrder =
+ shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
+ if (onCompletionAction != null && handler != null) {
+ handler.post(onCompletionAction);
+ }
+ }
+ }
+
+ @GuardedBy("this")
+ @Nullable
+ private HandlerAndRunnable createOnCompletionAction(
+ @Nullable Handler handler, @Nullable Runnable runnable) {
+ if (handler == null || runnable == null) {
+ return null;
+ }
+ HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable);
+ pendingOnCompletionActions.add(handlerAndRunnable);
+ return handlerAndRunnable;
+ }
+
+ // Internal methods. Called on the playback thread.
+
+ @SuppressWarnings("unchecked")
+ private boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ADD:
+ MessageData<Collection<MediaSourceHolder>> addMessage =
+ (MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);
+ shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());
+ addMediaSourcesInternal(addMessage.index, addMessage.customData);
+ scheduleTimelineUpdate(addMessage.onCompletionAction);
+ break;
+ case MSG_REMOVE:
+ MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
+ int fromIndex = removeMessage.index;
+ int toIndex = removeMessage.customData;
+ if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) {
+ shuffleOrder = shuffleOrder.cloneAndClear();
+ } else {
+ shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndex);
+ }
+ for (int index = toIndex - 1; index >= fromIndex; index--) {
+ removeMediaSourceInternal(index);
+ }
+ scheduleTimelineUpdate(removeMessage.onCompletionAction);
+ break;
+ case MSG_MOVE:
+ MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
+ shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);
+ shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
+ moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
+ scheduleTimelineUpdate(moveMessage.onCompletionAction);
+ break;
+ case MSG_SET_SHUFFLE_ORDER:
+ MessageData<ShuffleOrder> shuffleOrderMessage =
+ (MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
+ shuffleOrder = shuffleOrderMessage.customData;
+ scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction);
+ break;
+ case MSG_UPDATE_TIMELINE:
+ updateTimelineAndScheduleOnCompletionActions();
+ break;
+ case MSG_ON_COMPLETION:
+ Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj);
+ dispatchOnCompletionActions(actions);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ return true;
+ }
+
+ private void scheduleTimelineUpdate() {
+ scheduleTimelineUpdate(/* onCompletionAction= */ null);
+ }
+
+ private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) {
+ if (!timelineUpdateScheduled) {
+ getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget();
+ timelineUpdateScheduled = true;
+ }
+ if (onCompletionAction != null) {
+ nextTimelineUpdateOnCompletionActions.add(onCompletionAction);
+ }
+ }
+
+ private void updateTimelineAndScheduleOnCompletionActions() {
+ timelineUpdateScheduled = false;
+ Set<HandlerAndRunnable> onCompletionActions = nextTimelineUpdateOnCompletionActions;
+ nextTimelineUpdateOnCompletionActions = new HashSet<>();
+ refreshSourceInfo(new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic));
+ getPlaybackThreadHandlerOnPlaybackThread()
+ .obtainMessage(MSG_ON_COMPLETION, onCompletionActions)
+ .sendToTarget();
+ }
+
+ @SuppressWarnings("GuardedBy")
+ private Handler getPlaybackThreadHandlerOnPlaybackThread() {
+ // Write access to this value happens on the playback thread only, so playback thread reads
+ // don't need to be synchronized.
+ return Assertions.checkNotNull(playbackThreadHandler);
+ }
+
+ private synchronized void dispatchOnCompletionActions(
+ Set<HandlerAndRunnable> onCompletionActions) {
+ for (HandlerAndRunnable pendingAction : onCompletionActions) {
+ pendingAction.dispatch();
+ }
+ pendingOnCompletionActions.removeAll(onCompletionActions);
+ }
+
+ private void addMediaSourcesInternal(
+ int index, Collection<MediaSourceHolder> mediaSourceHolders) {
+ for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
+ addMediaSourceInternal(index++, mediaSourceHolder);
+ }
+ }
+
+ private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSourceHolder) {
+ if (newIndex > 0) {
+ MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1);
+ Timeline previousTimeline = previousHolder.mediaSource.getTimeline();
+ newMediaSourceHolder.reset(
+ newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount());
+ } else {
+ newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0);
+ }
+ Timeline newTimeline = newMediaSourceHolder.mediaSource.getTimeline();
+ correctOffsets(newIndex, /* childIndexUpdate= */ 1, newTimeline.getWindowCount());
+ mediaSourceHolders.add(newIndex, newMediaSourceHolder);
+ mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder);
+ prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource);
+ if (isEnabled() && mediaSourceByMediaPeriod.isEmpty()) {
+ enabledMediaSourceHolders.add(newMediaSourceHolder);
+ } else {
+ disableChildSource(newMediaSourceHolder);
+ }
+ }
+
+ private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) {
+ if (mediaSourceHolder == null) {
+ throw new IllegalArgumentException();
+ }
+ if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) {
+ MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1);
+ int windowOffsetUpdate =
+ timeline.getWindowCount()
+ - (nextHolder.firstWindowIndexInChild - mediaSourceHolder.firstWindowIndexInChild);
+ if (windowOffsetUpdate != 0) {
+ correctOffsets(
+ mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate);
+ }
+ }
+ scheduleTimelineUpdate();
+ }
+
+ private void removeMediaSourceInternal(int index) {
+ MediaSourceHolder holder = mediaSourceHolders.remove(index);
+ mediaSourceByUid.remove(holder.uid);
+ Timeline oldTimeline = holder.mediaSource.getTimeline();
+ correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount());
+ holder.isRemoved = true;
+ maybeReleaseChildSource(holder);
+ }
+
+ private void moveMediaSourceInternal(int currentIndex, int newIndex) {
+ int startIndex = Math.min(currentIndex, newIndex);
+ int endIndex = Math.max(currentIndex, newIndex);
+ int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
+ mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex));
+ for (int i = startIndex; i <= endIndex; i++) {
+ MediaSourceHolder holder = mediaSourceHolders.get(i);
+ holder.childIndex = i;
+ holder.firstWindowIndexInChild = windowOffset;
+ windowOffset += holder.mediaSource.getTimeline().getWindowCount();
+ }
+ }
+
+ private void correctOffsets(int startIndex, int childIndexUpdate, int windowOffsetUpdate) {
+ // TODO: Replace window index with uid in reporting to get rid of this inefficient method and
+ // the childIndex and firstWindowIndexInChild variables.
+ for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
+ MediaSourceHolder holder = mediaSourceHolders.get(i);
+ holder.childIndex += childIndexUpdate;
+ holder.firstWindowIndexInChild += windowOffsetUpdate;
+ }
+ }
+
+ private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
+ // Release if the source has been removed from the playlist and no periods are still active.
+ if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
+ enabledMediaSourceHolders.remove(mediaSourceHolder);
+ releaseChildSource(mediaSourceHolder);
+ }
+ }
+
+ private void enableMediaSource(MediaSourceHolder mediaSourceHolder) {
+ enabledMediaSourceHolders.add(mediaSourceHolder);
+ enableChildSource(mediaSourceHolder);
+ }
+
+ private void disableUnusedMediaSources() {
+ Iterator<MediaSourceHolder> iterator = enabledMediaSourceHolders.iterator();
+ while (iterator.hasNext()) {
+ MediaSourceHolder holder = iterator.next();
+ if (holder.activeMediaPeriodIds.isEmpty()) {
+ disableChildSource(holder);
+ iterator.remove();
+ }
+ }
+ }
+
+ /** Return uid of media source holder from period uid of concatenated source. */
+ private static Object getMediaSourceHolderUid(Object periodUid) {
+ return ConcatenatedTimeline.getChildTimelineUidFromConcatenatedUid(periodUid);
+ }
+
+ /** Return uid of child period from period uid of concatenated source. */
+ private static Object getChildPeriodUid(Object periodUid) {
+ return ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);
+ }
+
+ private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {
+ return ConcatenatedTimeline.getConcatenatedUid(holder.uid, childPeriodUid);
+ }
+
+ /** Data class to hold playlist media sources together with meta data needed to process them. */
+ /* package */ static final class MediaSourceHolder {
+
+ public final MaskingMediaSource mediaSource;
+ public final Object uid;
+ public final List<MediaPeriodId> activeMediaPeriodIds;
+
+ public int childIndex;
+ public int firstWindowIndexInChild;
+ public boolean isRemoved;
+
+ public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) {
+ this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation);
+ this.activeMediaPeriodIds = new ArrayList<>();
+ this.uid = new Object();
+ }
+
+ public void reset(int childIndex, int firstWindowIndexInChild) {
+ this.childIndex = childIndex;
+ this.firstWindowIndexInChild = firstWindowIndexInChild;
+ this.isRemoved = false;
+ this.activeMediaPeriodIds.clear();
+ }
+ }
+
+ /** Message used to post actions from app thread to playback thread. */
+ private static final class MessageData<T> {
+
+ public final int index;
+ public final T customData;
+ @Nullable public final HandlerAndRunnable onCompletionAction;
+
+ public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) {
+ this.index = index;
+ this.customData = customData;
+ this.onCompletionAction = onCompletionAction;
+ }
+ }
+
+ /** Timeline exposing concatenated timelines of playlist media sources. */
+ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {
+
+ private final int windowCount;
+ private final int periodCount;
+ private final int[] firstPeriodInChildIndices;
+ private final int[] firstWindowInChildIndices;
+ private final Timeline[] timelines;
+ private final Object[] uids;
+ private final HashMap<Object, Integer> childIndexByUid;
+
+ public ConcatenatedTimeline(
+ Collection<MediaSourceHolder> mediaSourceHolders,
+ ShuffleOrder shuffleOrder,
+ boolean isAtomic) {
+ super(isAtomic, shuffleOrder);
+ int childCount = mediaSourceHolders.size();
+ firstPeriodInChildIndices = new int[childCount];
+ firstWindowInChildIndices = new int[childCount];
+ timelines = new Timeline[childCount];
+ uids = new Object[childCount];
+ childIndexByUid = new HashMap<>();
+ int index = 0;
+ int windowCount = 0;
+ int periodCount = 0;
+ for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
+ timelines[index] = mediaSourceHolder.mediaSource.getTimeline();
+ firstWindowInChildIndices[index] = windowCount;
+ firstPeriodInChildIndices[index] = periodCount;
+ windowCount += timelines[index].getWindowCount();
+ periodCount += timelines[index].getPeriodCount();
+ uids[index] = mediaSourceHolder.uid;
+ childIndexByUid.put(uids[index], index++);
+ }
+ this.windowCount = windowCount;
+ this.periodCount = periodCount;
+ }
+
+ @Override
+ protected int getChildIndexByPeriodIndex(int periodIndex) {
+ return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false);
+ }
+
+ @Override
+ protected int getChildIndexByWindowIndex(int windowIndex) {
+ return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false);
+ }
+
+ @Override
+ protected int getChildIndexByChildUid(Object childUid) {
+ Integer index = childIndexByUid.get(childUid);
+ return index == null ? C.INDEX_UNSET : index;
+ }
+
+ @Override
+ protected Timeline getTimelineByChildIndex(int childIndex) {
+ return timelines[childIndex];
+ }
+
+ @Override
+ protected int getFirstPeriodIndexByChildIndex(int childIndex) {
+ return firstPeriodInChildIndices[childIndex];
+ }
+
+ @Override
+ protected int getFirstWindowIndexByChildIndex(int childIndex) {
+ return firstWindowInChildIndices[childIndex];
+ }
+
+ @Override
+ protected Object getChildUidByChildIndex(int childIndex) {
+ return uids[childIndex];
+ }
+
+ @Override
+ public int getWindowCount() {
+ return windowCount;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return periodCount;
+ }
+ }
+
+ /** Dummy media source which does nothing and does not support creating periods. */
+ private static final class DummyMediaSource extends BaseMediaSource {
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ // Do nothing.
+ }
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return null;
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ // Do nothing.
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ // Do nothing.
+ }
+ }
+
+ private static final class HandlerAndRunnable {
+
+ private final Handler handler;
+ private final Runnable runnable;
+
+ public HandlerAndRunnable(Handler handler, Runnable runnable) {
+ this.handler = handler;
+ this.runnable = runnable;
+ }
+
+ public void dispatch() {
+ handler.post(runnable);
+ }
+ }
+}
+
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java
new file mode 100644
index 0000000000..237510bea3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * Default implementation of {@link CompositeSequenceableLoaderFactory}.
+ */
+public final class DefaultCompositeSequenceableLoaderFactory
+ implements CompositeSequenceableLoaderFactory {
+
+ @Override
+ public SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders) {
+ return new CompositeSequenceableLoader(loaders);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java
new file mode 100644
index 0000000000..c25750247f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+/**
+ * @deprecated Use {@link MediaSourceEventListener} interface directly for selective overrides as
+ * all methods are implemented as no-op default methods.
+ */
+@Deprecated
+public abstract class DefaultMediaSourceEventListener implements MediaSourceEventListener {}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java
new file mode 100644
index 0000000000..398c6b91fc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import java.io.IOException;
+
+/**
+ * An empty {@link SampleStream}.
+ */
+public final class EmptySampleStream implements SampleStream {
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ return 0;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java
new file mode 100644
index 0000000000..3b72f51c44
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java
@@ -0,0 +1,394 @@
+/*
+ * 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;
+
+import android.net.Uri;
+import android.os.Handler;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/** @deprecated Use {@link ProgressiveMediaSource} instead. */
+@Deprecated
+@SuppressWarnings("deprecation")
+public final class ExtractorMediaSource extends CompositeMediaSource<Void> {
+
+ /** @deprecated Use {@link MediaSourceEventListener} instead. */
+ @Deprecated
+ public interface EventListener {
+
+ /**
+ * Called when an error occurs loading media data.
+ * <p>
+ * This method being called does not indicate that playback has failed, or that it will fail.
+ * The player may be able to recover from the error and continue. Hence applications should
+ * <em>not</em> implement this method to display a user visible error or initiate an application
+ * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement
+ * such behavior). This method is called to provide the application with an opportunity to log
+ * the error if it wishes to do so.
+ *
+ * @param error The load error.
+ */
+ void onLoadError(IOException error);
+
+ }
+
+ /** @deprecated Use {@link ProgressiveMediaSource.Factory} instead. */
+ @Deprecated
+ public static final class Factory implements MediaSourceFactory {
+
+ private final DataSource.Factory dataSourceFactory;
+
+ @Nullable private ExtractorsFactory extractorsFactory;
+ @Nullable private String customCacheKey;
+ @Nullable private Object tag;
+ private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private int continueLoadingCheckIntervalBytes;
+ private boolean isCreateCalled;
+
+ /**
+ * Creates a new factory for {@link ExtractorMediaSource}s.
+ *
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ */
+ public Factory(DataSource.Factory dataSourceFactory) {
+ this.dataSourceFactory = dataSourceFactory;
+ loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
+ continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
+ }
+
+ /**
+ * Sets the factory for {@link Extractor}s to process the media stream. The default value is an
+ * instance of {@link DefaultExtractorsFactory}.
+ *
+ * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
+ * possible formats are known, pass a factory that instantiates extractors for those
+ * formats.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) {
+ Assertions.checkState(!isCreateCalled);
+ this.extractorsFactory = extractorsFactory;
+ return this;
+ }
+
+ /**
+ * Sets the custom key that uniquely identifies the original stream. Used for cache indexing.
+ * The default value is {@code null}.
+ *
+ * @param customCacheKey A custom key that uniquely identifies the original stream. Used for
+ * cache indexing.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setCustomCacheKey(String customCacheKey) {
+ Assertions.checkState(!isCreateCalled);
+ this.customCacheKey = customCacheKey;
+ return this;
+ }
+
+ /**
+ * Sets a tag for the media source which will be published in the {@link
+ * com.google.android.exoplayer2.Timeline} of the source as {@link
+ * com.google.android.exoplayer2.Timeline.Window#tag}.
+ *
+ * @param tag A tag for the media source.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setTag(Object tag) {
+ Assertions.checkState(!isCreateCalled);
+ this.tag = tag;
+ return this;
+ }
+
+ /**
+ * Sets the minimum number of times to retry if a loading error occurs. See {@link
+ * #setLoadErrorHandlingPolicy} for the default value.
+ *
+ * <p>Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with
+ * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int)
+ * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)}
+ *
+ * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead.
+ */
+ @Deprecated
+ public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {
+ return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount));
+ }
+
+ /**
+ * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link
+ * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.
+ *
+ * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}.
+ *
+ * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
+ Assertions.checkState(!isCreateCalled);
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ return this;
+ }
+
+ /**
+ * Sets the number of bytes that should be loaded between each invocation of {@link
+ * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is
+ * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}.
+ *
+ * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between
+ * each invocation of {@link
+ * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) {
+ Assertions.checkState(!isCreateCalled);
+ this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
+ return this;
+ }
+
+ /** @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmSessionManager} instead. */
+ @Override
+ @Deprecated
+ public Factory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns a new {@link ExtractorMediaSource} using the current parameters.
+ *
+ * @param uri The {@link Uri}.
+ * @return The new {@link ExtractorMediaSource}.
+ */
+ @Override
+ public ExtractorMediaSource createMediaSource(Uri uri) {
+ isCreateCalled = true;
+ if (extractorsFactory == null) {
+ extractorsFactory = new DefaultExtractorsFactory();
+ }
+ return new ExtractorMediaSource(
+ uri,
+ dataSourceFactory,
+ extractorsFactory,
+ loadErrorHandlingPolicy,
+ customCacheKey,
+ continueLoadingCheckIntervalBytes,
+ tag);
+ }
+
+ /**
+ * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler,
+ * MediaSourceEventListener)} instead.
+ */
+ @Deprecated
+ public ExtractorMediaSource createMediaSource(
+ Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) {
+ ExtractorMediaSource mediaSource = createMediaSource(uri);
+ if (eventHandler != null && eventListener != null) {
+ mediaSource.addEventListener(eventHandler, eventListener);
+ }
+ return mediaSource;
+ }
+
+ @Override
+ public int[] getSupportedTypes() {
+ return new int[] {C.TYPE_OTHER};
+ }
+ }
+
+ /**
+ * @deprecated Use {@link ProgressiveMediaSource#DEFAULT_LOADING_CHECK_INTERVAL_BYTES} instead.
+ */
+ @Deprecated
+ public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES =
+ ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
+
+ private final ProgressiveMediaSource progressiveMediaSource;
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
+ * possible formats are known, pass a factory that instantiates extractors for those formats.
+ * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @deprecated Use {@link Factory} instead.
+ */
+ @Deprecated
+ public ExtractorMediaSource(
+ Uri uri,
+ DataSource.Factory dataSourceFactory,
+ ExtractorsFactory extractorsFactory,
+ @Nullable Handler eventHandler,
+ @Nullable EventListener eventListener) {
+ this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null);
+ }
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
+ * possible formats are known, pass a factory that instantiates extractors for those formats.
+ * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
+ * indexing. May be null.
+ * @deprecated Use {@link Factory} instead.
+ */
+ @Deprecated
+ public ExtractorMediaSource(
+ Uri uri,
+ DataSource.Factory dataSourceFactory,
+ ExtractorsFactory extractorsFactory,
+ @Nullable Handler eventHandler,
+ @Nullable EventListener eventListener,
+ @Nullable String customCacheKey) {
+ this(
+ uri,
+ dataSourceFactory,
+ extractorsFactory,
+ eventHandler,
+ eventListener,
+ customCacheKey,
+ DEFAULT_LOADING_CHECK_INTERVAL_BYTES);
+ }
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
+ * possible formats are known, pass a factory that instantiates extractors for those formats.
+ * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
+ * indexing. May be null.
+ * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
+ * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
+ * @deprecated Use {@link Factory} instead.
+ */
+ @Deprecated
+ public ExtractorMediaSource(
+ Uri uri,
+ DataSource.Factory dataSourceFactory,
+ ExtractorsFactory extractorsFactory,
+ @Nullable Handler eventHandler,
+ @Nullable EventListener eventListener,
+ @Nullable String customCacheKey,
+ int continueLoadingCheckIntervalBytes) {
+ this(
+ uri,
+ dataSourceFactory,
+ extractorsFactory,
+ new DefaultLoadErrorHandlingPolicy(),
+ customCacheKey,
+ continueLoadingCheckIntervalBytes,
+ /* tag= */ null);
+ if (eventListener != null && eventHandler != null) {
+ addEventListener(eventHandler, new EventListenerWrapper(eventListener));
+ }
+ }
+
+ private ExtractorMediaSource(
+ Uri uri,
+ DataSource.Factory dataSourceFactory,
+ ExtractorsFactory extractorsFactory,
+ LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
+ @Nullable String customCacheKey,
+ int continueLoadingCheckIntervalBytes,
+ @Nullable Object tag) {
+ progressiveMediaSource =
+ new ProgressiveMediaSource(
+ uri,
+ dataSourceFactory,
+ extractorsFactory,
+ DrmSessionManager.getDummyDrmSessionManager(),
+ loadableLoadErrorHandlingPolicy,
+ customCacheKey,
+ continueLoadingCheckIntervalBytes,
+ tag);
+ }
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return progressiveMediaSource.getTag();
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ prepareChildSource(/* id= */ null, progressiveMediaSource);
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ @Nullable Void id, MediaSource mediaSource, Timeline timeline) {
+ refreshSourceInfo(timeline);
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ return progressiveMediaSource.createPeriod(id, allocator, startPositionUs);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ progressiveMediaSource.releasePeriod(mediaPeriod);
+ }
+
+ @Deprecated
+ private static final class EventListenerWrapper implements MediaSourceEventListener {
+
+ private final EventListener eventListener;
+
+ public EventListenerWrapper(EventListener eventListener) {
+ this.eventListener = Assertions.checkNotNull(eventListener);
+ }
+
+ @Override
+ public void onLoadError(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {
+ eventListener.onLoadError(error);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java
new file mode 100644
index 0000000000..ce985708d0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+
+/**
+ * An overridable {@link Timeline} implementation forwarding all methods to another timeline.
+ */
+public abstract class ForwardingTimeline extends Timeline {
+
+ protected final Timeline timeline;
+
+ public ForwardingTimeline(Timeline timeline) {
+ this.timeline = timeline;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return timeline.getWindowCount();
+ }
+
+ @Override
+ public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
+ boolean shuffleModeEnabled) {
+ return timeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
+ boolean shuffleModeEnabled) {
+ return timeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
+ }
+
+ @Override
+ public int getLastWindowIndex(boolean shuffleModeEnabled) {
+ return timeline.getLastWindowIndex(shuffleModeEnabled);
+ }
+
+ @Override
+ public int getFirstWindowIndex(boolean shuffleModeEnabled) {
+ return timeline.getFirstWindowIndex(shuffleModeEnabled);
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ return timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return timeline.getPeriodCount();
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ return timeline.getPeriod(periodIndex, period, setIds);
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return timeline.getIndexOfPeriod(uid);
+ }
+
+ @Override
+ public Object getUidOfPeriod(int periodIndex) {
+ return timeline.getUidOfPeriod(periodIndex);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java
new file mode 100644
index 0000000000..b35525743a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java
@@ -0,0 +1,149 @@
+/*
+ * 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;
+
+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.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Splits ICY stream metadata out from a stream.
+ *
+ * <p>Note: {@link #open(DataSpec)} and {@link #close()} are not supported. This implementation is
+ * intended to wrap upstream {@link DataSource} instances that are opened and closed directly.
+ */
+/* package */ final class IcyDataSource implements DataSource {
+
+ public interface Listener {
+
+ /**
+ * Called when ICY stream metadata has been split from the stream.
+ *
+ * @param metadata The stream metadata in binary form.
+ */
+ void onIcyMetadata(ParsableByteArray metadata);
+ }
+
+ private final DataSource upstream;
+ private final int metadataIntervalBytes;
+ private final Listener listener;
+ private final byte[] metadataLengthByteHolder;
+ private int bytesUntilMetadata;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param metadataIntervalBytes The interval between ICY stream metadata, in bytes.
+ * @param listener A listener to which stream metadata is delivered.
+ */
+ public IcyDataSource(DataSource upstream, int metadataIntervalBytes, Listener listener) {
+ Assertions.checkArgument(metadataIntervalBytes > 0);
+ this.upstream = upstream;
+ this.metadataIntervalBytes = metadataIntervalBytes;
+ this.listener = listener;
+ metadataLengthByteHolder = new byte[1];
+ bytesUntilMetadata = metadataIntervalBytes;
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ upstream.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ if (bytesUntilMetadata == 0) {
+ if (readMetadata()) {
+ bytesUntilMetadata = metadataIntervalBytes;
+ } else {
+ return C.RESULT_END_OF_INPUT;
+ }
+ }
+ int bytesRead = upstream.read(buffer, offset, Math.min(bytesUntilMetadata, readLength));
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ bytesUntilMetadata -= bytesRead;
+ }
+ return bytesRead;
+ }
+
+ @Nullable
+ @Override
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return upstream.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Reads an ICY stream metadata block, passing it to {@link #listener} unless the block is empty.
+ *
+ * @return True if the block was extracted, including if its length byte indicated a length of
+ * zero. False if the end of the stream was reached.
+ * @throws IOException If an error occurs reading from the wrapped {@link DataSource}.
+ */
+ private boolean readMetadata() throws IOException {
+ int bytesRead = upstream.read(metadataLengthByteHolder, 0, 1);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ return false;
+ }
+ int metadataLength = (metadataLengthByteHolder[0] & 0xFF) << 4;
+ if (metadataLength == 0) {
+ return true;
+ }
+
+ int offset = 0;
+ int lengthRemaining = metadataLength;
+ byte[] metadata = new byte[metadataLength];
+ while (lengthRemaining > 0) {
+ bytesRead = upstream.read(metadata, offset, lengthRemaining);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ return false;
+ }
+ offset += bytesRead;
+ lengthRemaining -= bytesRead;
+ }
+
+ // Discard trailing zero bytes.
+ while (metadataLength > 0 && metadata[metadataLength - 1] == 0) {
+ metadataLength--;
+ }
+
+ if (metadataLength > 0) {
+ listener.onIcyMetadata(new ParsableByteArray(metadata, metadataLength));
+ }
+ return true;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java
new file mode 100644
index 0000000000..880bfd6a4f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java
@@ -0,0 +1,214 @@
+/*
+ * 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;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Loops a {@link MediaSource} a specified number of times.
+ *
+ * <p>Note: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link
+ * ExoPlayer#setRepeatMode(int)} instead of this class.
+ */
+public final class LoopingMediaSource extends CompositeMediaSource<Void> {
+
+ private final MediaSource childSource;
+ private final int loopCount;
+ private final Map<MediaPeriodId, MediaPeriodId> childMediaPeriodIdToMediaPeriodId;
+ private final Map<MediaPeriod, MediaPeriodId> mediaPeriodToChildMediaPeriodId;
+
+ /**
+ * Loops the provided source indefinitely. Note that it is usually better to use
+ * {@link ExoPlayer#setRepeatMode(int)}.
+ *
+ * @param childSource The {@link MediaSource} to loop.
+ */
+ public LoopingMediaSource(MediaSource childSource) {
+ this(childSource, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Loops the provided source a specified number of times.
+ *
+ * @param childSource The {@link MediaSource} to loop.
+ * @param loopCount The desired number of loops. Must be strictly positive.
+ */
+ public LoopingMediaSource(MediaSource childSource, int loopCount) {
+ Assertions.checkArgument(loopCount > 0);
+ this.childSource = childSource;
+ this.loopCount = loopCount;
+ childMediaPeriodIdToMediaPeriodId = new HashMap<>();
+ mediaPeriodToChildMediaPeriodId = new HashMap<>();
+ }
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return childSource.getTag();
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ prepareChildSource(/* id= */ null, childSource);
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ if (loopCount == Integer.MAX_VALUE) {
+ return childSource.createPeriod(id, allocator, startPositionUs);
+ }
+ Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);
+ MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);
+ childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);
+ MediaPeriod mediaPeriod =
+ childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
+ mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);
+ return mediaPeriod;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ childSource.releasePeriod(mediaPeriod);
+ MediaPeriodId childMediaPeriodId = mediaPeriodToChildMediaPeriodId.remove(mediaPeriod);
+ if (childMediaPeriodId != null) {
+ childMediaPeriodIdToMediaPeriodId.remove(childMediaPeriodId);
+ }
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) {
+ Timeline loopingTimeline =
+ loopCount != Integer.MAX_VALUE
+ ? new LoopingTimeline(timeline, loopCount)
+ : new InfinitelyLoopingTimeline(timeline);
+ refreshSourceInfo(loopingTimeline);
+ }
+
+ @Override
+ protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
+ Void id, MediaPeriodId mediaPeriodId) {
+ return loopCount != Integer.MAX_VALUE
+ ? childMediaPeriodIdToMediaPeriodId.get(mediaPeriodId)
+ : mediaPeriodId;
+ }
+
+ private static final class LoopingTimeline extends AbstractConcatenatedTimeline {
+
+ private final Timeline childTimeline;
+ private final int childPeriodCount;
+ private final int childWindowCount;
+ private final int loopCount;
+
+ public LoopingTimeline(Timeline childTimeline, int loopCount) {
+ super(/* isAtomic= */ false, new UnshuffledShuffleOrder(loopCount));
+ this.childTimeline = childTimeline;
+ childPeriodCount = childTimeline.getPeriodCount();
+ childWindowCount = childTimeline.getWindowCount();
+ this.loopCount = loopCount;
+ if (childPeriodCount > 0) {
+ Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount,
+ "LoopingMediaSource contains too many periods");
+ }
+ }
+
+ @Override
+ public int getWindowCount() {
+ return childWindowCount * loopCount;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return childPeriodCount * loopCount;
+ }
+
+ @Override
+ protected int getChildIndexByPeriodIndex(int periodIndex) {
+ return periodIndex / childPeriodCount;
+ }
+
+ @Override
+ protected int getChildIndexByWindowIndex(int windowIndex) {
+ return windowIndex / childWindowCount;
+ }
+
+ @Override
+ protected int getChildIndexByChildUid(Object childUid) {
+ if (!(childUid instanceof Integer)) {
+ return C.INDEX_UNSET;
+ }
+ return (Integer) childUid;
+ }
+
+ @Override
+ protected Timeline getTimelineByChildIndex(int childIndex) {
+ return childTimeline;
+ }
+
+ @Override
+ protected int getFirstPeriodIndexByChildIndex(int childIndex) {
+ return childIndex * childPeriodCount;
+ }
+
+ @Override
+ protected int getFirstWindowIndexByChildIndex(int childIndex) {
+ return childIndex * childWindowCount;
+ }
+
+ @Override
+ protected Object getChildUidByChildIndex(int childIndex) {
+ return childIndex;
+ }
+
+ }
+
+ private static final class InfinitelyLoopingTimeline extends ForwardingTimeline {
+
+ public InfinitelyLoopingTimeline(Timeline timeline) {
+ super(timeline);
+ }
+
+ @Override
+ public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
+ boolean shuffleModeEnabled) {
+ int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode,
+ shuffleModeEnabled);
+ return childNextWindowIndex == C.INDEX_UNSET ? getFirstWindowIndex(shuffleModeEnabled)
+ : childNextWindowIndex;
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
+ boolean shuffleModeEnabled) {
+ int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode,
+ shuffleModeEnabled);
+ return childPreviousWindowIndex == C.INDEX_UNSET ? getLastWindowIndex(shuffleModeEnabled)
+ : childPreviousWindowIndex;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java
new file mode 100644
index 0000000000..4fe7b137b6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2017 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;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import java.io.IOException;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * Media period that wraps a media source and defers calling its {@link
+ * MediaSource#createPeriod(MediaPeriodId, Allocator, long)} method until {@link
+ * #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media
+ * period immediately but the media source that should create it is not yet prepared.
+ */
+public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ /** Listener for preparation errors. */
+ public interface PrepareErrorListener {
+
+ /**
+ * Called the first time an error occurs while refreshing source info or preparing the period.
+ */
+ void onPrepareError(MediaPeriodId mediaPeriodId, IOException exception);
+ }
+
+ /** The {@link MediaSource} which will create the actual media period. */
+ public final MediaSource mediaSource;
+ /** The {@link MediaPeriodId} used to create the masking media period. */
+ public final MediaPeriodId id;
+
+ private final Allocator allocator;
+
+ @Nullable private MediaPeriod mediaPeriod;
+ @Nullable private Callback callback;
+ private long preparePositionUs;
+ @Nullable private PrepareErrorListener listener;
+ private boolean notifiedPrepareError;
+ private long preparePositionOverrideUs;
+
+ /**
+ * Creates a new masking media period.
+ *
+ * @param mediaSource The media source to wrap.
+ * @param id The identifier used to create the masking media period.
+ * @param allocator The allocator used to create the media period.
+ * @param preparePositionUs The expected start position, in microseconds.
+ */
+ public MaskingMediaPeriod(
+ MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) {
+ this.id = id;
+ this.allocator = allocator;
+ this.mediaSource = mediaSource;
+ this.preparePositionUs = preparePositionUs;
+ preparePositionOverrideUs = C.TIME_UNSET;
+ }
+
+ /**
+ * Sets a listener for preparation errors.
+ *
+ * @param listener An listener to be notified of media period preparation errors. If a listener is
+ * set, {@link #maybeThrowPrepareError()} will not throw but will instead pass the first
+ * preparation error (if any) to the listener.
+ */
+ public void setPrepareErrorListener(PrepareErrorListener listener) {
+ this.listener = listener;
+ }
+
+ /** Returns the position at which the masking media period was prepared, in microseconds. */
+ public long getPreparePositionUs() {
+ return preparePositionUs;
+ }
+
+ /**
+ * Overrides the default prepare position at which to prepare the media period. This value is only
+ * used if called before {@link #createPeriod(MediaPeriodId)}.
+ *
+ * @param preparePositionUs The default prepare position to use, in microseconds.
+ */
+ public void overridePreparePositionUs(long preparePositionUs) {
+ preparePositionOverrideUs = preparePositionUs;
+ }
+
+ /**
+ * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source
+ * then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link
+ * #releasePeriod()} to release the period.
+ *
+ * @param id The identifier that should be used to create the media period from the media source.
+ */
+ public void createPeriod(MediaPeriodId id) {
+ long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs);
+ mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs);
+ if (callback != null) {
+ mediaPeriod.prepare(this, preparePositionUs);
+ }
+ }
+
+ /**
+ * Releases the period.
+ */
+ public void releasePeriod() {
+ if (mediaPeriod != null) {
+ mediaSource.releasePeriod(mediaPeriod);
+ }
+ }
+
+ @Override
+ public void prepare(Callback callback, long preparePositionUs) {
+ this.callback = callback;
+ if (mediaPeriod != null) {
+ mediaPeriod.prepare(this, getPreparePositionWithOverride(this.preparePositionUs));
+ }
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ try {
+ if (mediaPeriod != null) {
+ mediaPeriod.maybeThrowPrepareError();
+ } else {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ } catch (final IOException e) {
+ if (listener == null) {
+ throw e;
+ }
+ if (!notifiedPrepareError) {
+ notifiedPrepareError = true;
+ listener.onPrepareError(id, e);
+ }
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return castNonNull(mediaPeriod).getTrackGroups();
+ }
+
+ @Override
+ public long selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs) {
+ if (preparePositionOverrideUs != C.TIME_UNSET && positionUs == preparePositionUs) {
+ positionUs = preparePositionOverrideUs;
+ preparePositionOverrideUs = C.TIME_UNSET;
+ }
+ return castNonNull(mediaPeriod)
+ .selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs);
+ }
+
+ @Override
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ castNonNull(mediaPeriod).discardBuffer(positionUs, toKeyframe);
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ return castNonNull(mediaPeriod).readDiscontinuity();
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return castNonNull(mediaPeriod).getBufferedPositionUs();
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ return castNonNull(mediaPeriod).seekToUs(positionUs);
+ }
+
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return castNonNull(mediaPeriod).getAdjustedSeekPositionUs(positionUs, seekParameters);
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return castNonNull(mediaPeriod).getNextLoadPositionUs();
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ castNonNull(mediaPeriod).reevaluateBuffer(positionUs);
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
+ }
+
+ @Override
+ public boolean isLoading() {
+ return mediaPeriod != null && mediaPeriod.isLoading();
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ castNonNull(callback).onContinueLoadingRequested(this);
+ }
+
+ // MediaPeriod.Callback implementation
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ castNonNull(callback).onPrepared(this);
+ }
+
+ private long getPreparePositionWithOverride(long preparePositionUs) {
+ return preparePositionOverrideUs != C.TIME_UNSET
+ ? preparePositionOverrideUs
+ : preparePositionUs;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java
new file mode 100644
index 0000000000..8c867a8c26
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java
@@ -0,0 +1,353 @@
+/*
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.source;
+
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Window;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media
+ * structure is known.
+ */
+public final class MaskingMediaSource extends CompositeMediaSource<Void> {
+
+ private final MediaSource mediaSource;
+ private final boolean useLazyPreparation;
+ private final Timeline.Window window;
+ private final Timeline.Period period;
+
+ private MaskingTimeline timeline;
+ @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod;
+ @Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher;
+ private boolean hasStartedPreparing;
+ private boolean isPrepared;
+
+ /**
+ * Creates the masking media source.
+ *
+ * @param mediaSource A {@link MediaSource}.
+ * @param useLazyPreparation Whether the {@code mediaSource} is prepared lazily. If false, all
+ * manifest loads and other initial preparation steps happen immediately. If true, these
+ * initial preparations are triggered only when the player starts buffering the media.
+ */
+ public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) {
+ this.mediaSource = mediaSource;
+ this.useLazyPreparation = useLazyPreparation;
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag());
+ }
+
+ /** Returns the {@link Timeline}. */
+ public Timeline getTimeline() {
+ return timeline;
+ }
+
+ @Override
+ public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ if (!useLazyPreparation) {
+ hasStartedPreparing = true;
+ prepareChildSource(/* id= */ null, mediaSource);
+ }
+ }
+
+ @Nullable
+ @Override
+ public Object getTag() {
+ return mediaSource.getTag();
+ }
+
+ @Override
+ @SuppressWarnings("MissingSuperCall")
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ // Do nothing. Source info refresh errors will be thrown when calling
+ // MaskingMediaPeriod.maybeThrowPrepareError.
+ }
+
+ @Override
+ public MaskingMediaPeriod createPeriod(
+ MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ MaskingMediaPeriod mediaPeriod =
+ new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs);
+ if (isPrepared) {
+ MediaPeriodId idInSource = id.copyWithPeriodUid(getInternalPeriodUid(id.periodUid));
+ mediaPeriod.createPeriod(idInSource);
+ } else {
+ // We should have at most one media period while source is unprepared because the duration is
+ // unset and we don't load beyond periods with unset duration. We need to figure out how to
+ // handle the prepare positions of multiple deferred media periods, should that ever change.
+ unpreparedMaskingMediaPeriod = mediaPeriod;
+ unpreparedMaskingMediaPeriodEventDispatcher =
+ createEventDispatcher(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0);
+ unpreparedMaskingMediaPeriodEventDispatcher.mediaPeriodCreated();
+ if (!hasStartedPreparing) {
+ hasStartedPreparing = true;
+ prepareChildSource(/* id= */ null, mediaSource);
+ }
+ }
+ return mediaPeriod;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ ((MaskingMediaPeriod) mediaPeriod).releasePeriod();
+ if (mediaPeriod == unpreparedMaskingMediaPeriod) {
+ Assertions.checkNotNull(unpreparedMaskingMediaPeriodEventDispatcher).mediaPeriodReleased();
+ unpreparedMaskingMediaPeriodEventDispatcher = null;
+ unpreparedMaskingMediaPeriod = null;
+ }
+ }
+
+ @Override
+ public void releaseSourceInternal() {
+ isPrepared = false;
+ hasStartedPreparing = false;
+ super.releaseSourceInternal();
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ Void id, MediaSource mediaSource, Timeline newTimeline) {
+ if (isPrepared) {
+ timeline = timeline.cloneWithUpdatedTimeline(newTimeline);
+ } else if (newTimeline.isEmpty()) {
+ timeline =
+ MaskingTimeline.createWithRealTimeline(
+ newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID);
+ } else {
+ // Determine first period and the start position.
+ // This will be:
+ // 1. The default window start position if no deferred period has been created yet.
+ // 2. The non-zero prepare position of the deferred period under the assumption that this is
+ // a non-zero initial seek position in the window.
+ // 3. The default window start position if the deferred period has a prepare position of zero
+ // under the assumption that the prepare position of zero was used because it's the
+ // default position of the DummyTimeline window. Note that this will override an
+ // intentional seek to zero for a window with a non-zero default position. This is
+ // unlikely to be a problem as a non-zero default position usually only occurs for live
+ // playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions
+ // anyway.
+ newTimeline.getWindow(/* windowIndex= */ 0, window);
+ long windowStartPositionUs = window.getDefaultPositionUs();
+ if (unpreparedMaskingMediaPeriod != null) {
+ long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs();
+ if (periodPreparePositionUs != 0) {
+ windowStartPositionUs = periodPreparePositionUs;
+ }
+ }
+ Object windowUid = window.uid;
+ Pair<Object, Long> periodPosition =
+ newTimeline.getPeriodPosition(
+ window, period, /* windowIndex= */ 0, windowStartPositionUs);
+ Object periodUid = periodPosition.first;
+ long periodPositionUs = periodPosition.second;
+ timeline = MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid);
+ if (unpreparedMaskingMediaPeriod != null) {
+ MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;
+ maskingPeriod.overridePreparePositionUs(periodPositionUs);
+ MediaPeriodId idInSource =
+ maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid));
+ maskingPeriod.createPeriod(idInSource);
+ }
+ }
+ isPrepared = true;
+ refreshSourceInfo(this.timeline);
+ }
+
+ @Nullable
+ @Override
+ protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
+ Void id, MediaPeriodId mediaPeriodId) {
+ return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid));
+ }
+
+ @Override
+ protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) {
+ // Suppress create and release events for the period created while the source was still
+ // unprepared, as we send these events from this class.
+ return unpreparedMaskingMediaPeriod == null
+ || !mediaPeriodId.equals(unpreparedMaskingMediaPeriod.id);
+ }
+
+ private Object getInternalPeriodUid(Object externalPeriodUid) {
+ return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID)
+ ? timeline.replacedInternalPeriodUid
+ : externalPeriodUid;
+ }
+
+ private Object getExternalPeriodUid(Object internalPeriodUid) {
+ return timeline.replacedInternalPeriodUid.equals(internalPeriodUid)
+ ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID
+ : internalPeriodUid;
+ }
+
+ /**
+ * Timeline used as placeholder for an unprepared media source. After preparation, a
+ * MaskingTimeline is used to keep the originally assigned dummy period ID.
+ */
+ private static final class MaskingTimeline extends ForwardingTimeline {
+
+ public static final Object DUMMY_EXTERNAL_PERIOD_UID = new Object();
+
+ private final Object replacedInternalWindowUid;
+ private final Object replacedInternalPeriodUid;
+
+ /**
+ * Returns an instance with a dummy timeline using the provided window tag.
+ *
+ * @param windowTag A window tag.
+ */
+ public static MaskingTimeline createWithDummyTimeline(@Nullable Object windowTag) {
+ return new MaskingTimeline(
+ new DummyTimeline(windowTag), Window.SINGLE_WINDOW_UID, DUMMY_EXTERNAL_PERIOD_UID);
+ }
+
+ /**
+ * Returns an instance with a real timeline, replacing the provided period ID with the already
+ * assigned dummy period ID.
+ *
+ * @param timeline The real timeline.
+ * @param firstWindowUid The window UID in the timeline which will be replaced by the already
+ * assigned {@link Window#SINGLE_WINDOW_UID}.
+ * @param firstPeriodUid The period UID in the timeline which will be replaced by the already
+ * assigned {@link #DUMMY_EXTERNAL_PERIOD_UID}.
+ */
+ public static MaskingTimeline createWithRealTimeline(
+ Timeline timeline, Object firstWindowUid, Object firstPeriodUid) {
+ return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid);
+ }
+
+ private MaskingTimeline(
+ Timeline timeline, Object replacedInternalWindowUid, Object replacedInternalPeriodUid) {
+ super(timeline);
+ this.replacedInternalWindowUid = replacedInternalWindowUid;
+ this.replacedInternalPeriodUid = replacedInternalPeriodUid;
+ }
+
+ /**
+ * Returns a copy with an updated timeline. This keeps the existing period replacement.
+ *
+ * @param timeline The new timeline.
+ */
+ public MaskingTimeline cloneWithUpdatedTimeline(Timeline timeline) {
+ return new MaskingTimeline(timeline, replacedInternalWindowUid, replacedInternalPeriodUid);
+ }
+
+ /** Returns the wrapped timeline. */
+ public Timeline getTimeline() {
+ return timeline;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
+ if (Util.areEqual(window.uid, replacedInternalWindowUid)) {
+ window.uid = Window.SINGLE_WINDOW_UID;
+ }
+ return window;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ timeline.getPeriod(periodIndex, period, setIds);
+ if (Util.areEqual(period.uid, replacedInternalPeriodUid)) {
+ period.uid = DUMMY_EXTERNAL_PERIOD_UID;
+ }
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return timeline.getIndexOfPeriod(
+ DUMMY_EXTERNAL_PERIOD_UID.equals(uid) ? replacedInternalPeriodUid : uid);
+ }
+
+ @Override
+ public Object getUidOfPeriod(int periodIndex) {
+ Object uid = timeline.getUidOfPeriod(periodIndex);
+ return Util.areEqual(uid, replacedInternalPeriodUid) ? DUMMY_EXTERNAL_PERIOD_UID : uid;
+ }
+ }
+
+ /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
+ private static final class DummyTimeline extends Timeline {
+
+ @Nullable private final Object tag;
+
+ public DummyTimeline(@Nullable Object tag) {
+ this.tag = tag;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return 1;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ return window.set(
+ Window.SINGLE_WINDOW_UID,
+ tag,
+ /* manifest= */ null,
+ /* presentationStartTimeMs= */ C.TIME_UNSET,
+ /* windowStartTimeMs= */ C.TIME_UNSET,
+ /* isSeekable= */ false,
+ // Dynamic window to indicate pending timeline updates.
+ /* isDynamic= */ true,
+ /* isLive= */ false,
+ /* defaultPositionUs= */ 0,
+ /* durationUs= */ C.TIME_UNSET,
+ /* firstPeriodIndex= */ 0,
+ /* lastPeriodIndex= */ 0,
+ /* positionInFirstPeriodUs= */ 0);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ return period.set(
+ /* id= */ 0,
+ /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID,
+ /* windowIndex= */ 0,
+ /* durationUs = */ C.TIME_UNSET,
+ /* positionInWindowUs= */ 0);
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return uid == MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public Object getUidOfPeriod(int periodIndex) {
+ return MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java
new file mode 100644
index 0000000000..3effcec904
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -0,0 +1,251 @@
+/*
+ * 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * Loads media corresponding to a {@link Timeline.Period}, and allows that media to be read. All
+ * methods are called on the player's internal playback thread, as described in the
+ * {@link ExoPlayer} Javadoc.
+ */
+public interface MediaPeriod extends SequenceableLoader {
+
+ /**
+ * A callback to be notified of {@link MediaPeriod} events.
+ */
+ interface Callback extends SequenceableLoader.Callback<MediaPeriod> {
+
+ /**
+ * Called when preparation completes.
+ *
+ * <p>Called on the playback thread. After invoking this method, the {@link MediaPeriod} can
+ * expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[],
+ * long)} to be called with the initial track selection.
+ *
+ * @param mediaPeriod The prepared {@link MediaPeriod}.
+ */
+ void onPrepared(MediaPeriod mediaPeriod);
+ }
+
+ /**
+ * Prepares this media period asynchronously.
+ *
+ * <p>{@code callback.onPrepared} is called when preparation completes. If preparation fails,
+ * {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
+ *
+ * <p>If preparation succeeds and results in a source timeline change (e.g. the period duration
+ * becoming known), {@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be
+ * called before {@code callback.onPrepared}.
+ *
+ * @param callback Callback to receive updates from this period, including being notified when
+ * preparation completes.
+ * @param positionUs The expected starting position, in microseconds.
+ */
+ void prepare(Callback callback, long positionUs);
+
+ /**
+ * Throws an error that's preventing the period from becoming prepared. Does nothing if no such
+ * error exists.
+ *
+ * <p>This method is only called before the period has completed preparation.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowPrepareError() throws IOException;
+
+ /**
+ * Returns the {@link TrackGroup}s exposed by the period.
+ *
+ * <p>This method is only called after the period has been prepared.
+ *
+ * @return The {@link TrackGroup}s.
+ */
+ TrackGroupArray getTrackGroups();
+
+ /**
+ * Returns a list of {@link StreamKey StreamKeys} which allow to filter the media in this period
+ * to load only the parts needed to play the provided {@link TrackSelection TrackSelections}.
+ *
+ * <p>This method is only called after the period has been prepared.
+ *
+ * @param trackSelections The {@link TrackSelection TrackSelections} describing the tracks for
+ * which stream keys are requested.
+ * @return The corresponding {@link StreamKey StreamKeys} for the selected tracks, or an empty
+ * list if filtering is not possible and the entire media needs to be loaded to play the
+ * selected tracks.
+ */
+ default List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Performs a track selection.
+ *
+ * <p>The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
+ * indicating whether the existing {@link SampleStream} can be retained for each selection, and
+ * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the
+ * provided selections, clearing, setting and replacing entries as required. If an existing sample
+ * stream is retained but with the requirement that the consuming renderer be reset, then the
+ * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
+ * if a new sample stream is created.
+ *
+ * <p>Note that previously passed {@link TrackSelection TrackSelections} are no longer valid, and
+ * any references to them must be updated to point to the new selections.
+ *
+ * <p>This method is only called after the period has been prepared.
+ *
+ * @param selections The renderer track selections.
+ * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
+ * for each track selection. A {@code true} value indicates that the selection is equivalent
+ * to the one that was previously passed, and that the caller does not require that the sample
+ * stream be recreated. If a retained sample stream holds any references to the track
+ * selection then they must be updated to point to the new selection.
+ * @param streams The existing sample streams, which will be updated to reflect the provided
+ * selections.
+ * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that
+ * have been retained but with the requirement that the consuming renderer be reset.
+ * @param positionUs The current playback position in microseconds. If playback of this period has
+ * not yet started, the value will be the starting position.
+ * @return The actual position at which the tracks were enabled, in microseconds.
+ */
+ long selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs);
+
+ /**
+ * Discards buffered media up to the specified position.
+ *
+ * <p>This method is only called after the period has been prepared.
+ *
+ * @param positionUs The position in microseconds.
+ * @param toKeyframe If true then for each track discards samples up to the keyframe before or at
+ * the specified position, rather than any sample before or at that position.
+ */
+ void discardBuffer(long positionUs, boolean toKeyframe);
+
+ /**
+ * Attempts to read a discontinuity.
+ *
+ * <p>After this method has returned a value other than {@link C#TIME_UNSET}, all {@link
+ * SampleStream}s provided by the period are guaranteed to start from a key frame.
+ *
+ * <p>This method is only called after the period has been prepared and before reading from any
+ * {@link SampleStream}s provided by the period.
+ *
+ * @return If a discontinuity was read then the playback position in microseconds after the
+ * discontinuity. Else {@link C#TIME_UNSET}.
+ */
+ long readDiscontinuity();
+
+ /**
+ * Attempts to seek to the specified position in microseconds.
+ *
+ * <p>After this method has been called, all {@link SampleStream}s provided by the period are
+ * guaranteed to start from a key frame.
+ *
+ * <p>This method is only called when at least one track is selected.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @return The actual position to which the period was seeked, in microseconds.
+ */
+ long seekToUs(long positionUs);
+
+ /**
+ * Returns the position to which a seek will be performed, given the specified seek position and
+ * {@link SeekParameters}.
+ *
+ * <p>This method is only called after the period has been prepared.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @param seekParameters Parameters that control how the seek is performed. Implementations may
+ * apply seek parameters on a best effort basis.
+ * @return The actual position to which a seek will be performed, in microseconds.
+ */
+ long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters);
+
+ // SequenceableLoader interface. Overridden to provide more specific documentation.
+
+ /**
+ * Returns an estimate of the position up to which data is buffered for the enabled tracks.
+ *
+ * <p>This method is only called when at least one track is selected.
+ *
+ * @return An estimate of the absolute position in microseconds up to which data is buffered, or
+ * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
+ */
+ @Override
+ long getBufferedPositionUs();
+
+ /**
+ * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
+ *
+ * <p>This method is only called after the period has been prepared. It may be called when no
+ * tracks are selected.
+ */
+ @Override
+ long getNextLoadPositionUs();
+
+ /**
+ * Attempts to continue loading.
+ *
+ * <p>This method may be called both during and after the period has been prepared.
+ *
+ * <p>A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
+ * {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be
+ * called when the period is permitted to continue loading data. A period may do this both during
+ * and after preparation.
+ *
+ * @param positionUs The current playback position in microseconds. If playback of this period has
+ * not yet started, the value will be the starting position in this period minus the duration
+ * of any media in previous periods still to be played.
+ * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return a
+ * different value than prior to the call. False otherwise.
+ */
+ @Override
+ boolean continueLoading(long positionUs);
+
+ /** Returns whether the media period is currently loading. */
+ boolean isLoading();
+
+ /**
+ * Re-evaluates the buffer given the playback position.
+ *
+ * <p>This method is only called after the period has been prepared.
+ *
+ * <p>A period may choose to discard buffered media so that it can be re-buffered in a different
+ * quality.
+ *
+ * @param positionUs The current playback position in microseconds. If playback of this period has
+ * not yet started, the value will be the starting position in this period minus the duration
+ * of any media in previous periods still to be played.
+ */
+ @Override
+ void reevaluateBuffer(long positionUs);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java
new file mode 100644
index 0000000000..7e757d5ade
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java
@@ -0,0 +1,325 @@
+/*
+ * 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;
+
+import android.os.Handler;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import java.io.IOException;
+
+/**
+ * Defines and provides media to be played by an {@link org.mozilla.thirdparty.com.google.android.exoplayer2ExoPlayer}. A
+ * MediaSource has two main responsibilities:
+ *
+ * <ul>
+ * <li>To provide the player with a {@link Timeline} defining the structure of its media, and to
+ * provide a new timeline whenever the structure of the media changes. The MediaSource
+ * provides these timelines by calling {@link MediaSourceCaller#onSourceInfoRefreshed} on the
+ * {@link MediaSourceCaller}s passed to {@link #prepareSource(MediaSourceCaller,
+ * TransferListener)}.
+ * <li>To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are
+ * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a
+ * way for the player to load and read the media.
+ * </ul>
+ *
+ * All methods are called on the player's internal playback thread, as described in the {@link
+ * com.google.android.exoplayer2.ExoPlayer} Javadoc. They should not be called directly from
+ * application code. Instances can be re-used, but only for one {@link
+ * com.google.android.exoplayer2.ExoPlayer} instance simultaneously.
+ */
+public interface MediaSource {
+
+ /** A caller of media sources, which will be notified of source events. */
+ interface MediaSourceCaller {
+
+ /**
+ * Called when the {@link Timeline} has been refreshed.
+ *
+ * <p>Called on the playback thread.
+ *
+ * @param source The {@link MediaSource} whose info has been refreshed.
+ * @param timeline The source's timeline.
+ */
+ void onSourceInfoRefreshed(MediaSource source, Timeline timeline);
+ }
+
+ /** Identifier for a {@link MediaPeriod}. */
+ final class MediaPeriodId {
+
+ /** The unique id of the timeline period. */
+ public final Object periodUid;
+
+ /**
+ * If the media period is in an ad group, the index of the ad group in the period.
+ * {@link C#INDEX_UNSET} otherwise.
+ */
+ public final int adGroupIndex;
+
+ /**
+ * If the media period is in an ad group, the index of the ad in its ad group in the period.
+ * {@link C#INDEX_UNSET} otherwise.
+ */
+ public final int adIndexInAdGroup;
+
+ /**
+ * The sequence number of the window in the buffered sequence of windows this media period is
+ * part of. {@link C#INDEX_UNSET} if the media period id is not part of a buffered sequence of
+ * windows.
+ */
+ public final long windowSequenceNumber;
+
+ /**
+ * The index of the next ad group to which the media period's content is clipped, or {@link
+ * C#INDEX_UNSET} if there is no following ad group or if this media period is an ad.
+ */
+ public final int nextAdGroupIndex;
+
+ /**
+ * Creates a media period identifier for a dummy period which is not part of a buffered sequence
+ * of windows.
+ *
+ * @param periodUid The unique id of the timeline period.
+ */
+ public MediaPeriodId(Object periodUid) {
+ this(periodUid, /* windowSequenceNumber= */ C.INDEX_UNSET);
+ }
+
+ /**
+ * Creates a media period identifier for the specified period in the timeline.
+ *
+ * @param periodUid The unique id of the timeline period.
+ * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
+ * windows this media period is part of.
+ */
+ public MediaPeriodId(Object periodUid, long windowSequenceNumber) {
+ this(
+ periodUid,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET,
+ windowSequenceNumber,
+ /* nextAdGroupIndex= */ C.INDEX_UNSET);
+ }
+
+ /**
+ * Creates a media period identifier for the specified clipped period in the timeline.
+ *
+ * @param periodUid The unique id of the timeline period.
+ * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
+ * windows this media period is part of.
+ * @param nextAdGroupIndex The index of the next ad group to which the media period's content is
+ * clipped.
+ */
+ public MediaPeriodId(Object periodUid, long windowSequenceNumber, int nextAdGroupIndex) {
+ this(
+ periodUid,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET,
+ windowSequenceNumber,
+ nextAdGroupIndex);
+ }
+
+ /**
+ * Creates a media period identifier that identifies an ad within an ad group at the specified
+ * timeline period.
+ *
+ * @param periodUid The unique id of the timeline period that contains the ad group.
+ * @param adGroupIndex The index of the ad group.
+ * @param adIndexInAdGroup The index of the ad in the ad group.
+ * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
+ * windows this media period is part of.
+ */
+ public MediaPeriodId(
+ Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {
+ this(
+ periodUid,
+ adGroupIndex,
+ adIndexInAdGroup,
+ windowSequenceNumber,
+ /* nextAdGroupIndex= */ C.INDEX_UNSET);
+ }
+
+ private MediaPeriodId(
+ Object periodUid,
+ int adGroupIndex,
+ int adIndexInAdGroup,
+ long windowSequenceNumber,
+ int nextAdGroupIndex) {
+ this.periodUid = periodUid;
+ this.adGroupIndex = adGroupIndex;
+ this.adIndexInAdGroup = adIndexInAdGroup;
+ this.windowSequenceNumber = windowSequenceNumber;
+ this.nextAdGroupIndex = nextAdGroupIndex;
+ }
+
+ /** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */
+ public MediaPeriodId copyWithPeriodUid(Object newPeriodUid) {
+ return periodUid.equals(newPeriodUid)
+ ? this
+ : new MediaPeriodId(
+ newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex);
+ }
+
+ /**
+ * Returns whether this period identifier identifies an ad in an ad group in a period.
+ */
+ public boolean isAd() {
+ return adGroupIndex != C.INDEX_UNSET;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ MediaPeriodId periodId = (MediaPeriodId) obj;
+ return periodUid.equals(periodId.periodUid)
+ && adGroupIndex == periodId.adGroupIndex
+ && adIndexInAdGroup == periodId.adIndexInAdGroup
+ && windowSequenceNumber == periodId.windowSequenceNumber
+ && nextAdGroupIndex == periodId.nextAdGroupIndex;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + periodUid.hashCode();
+ result = 31 * result + adGroupIndex;
+ result = 31 * result + adIndexInAdGroup;
+ result = 31 * result + (int) windowSequenceNumber;
+ result = 31 * result + nextAdGroupIndex;
+ return result;
+ }
+ }
+
+ /**
+ * Adds a {@link MediaSourceEventListener} to the list of listeners which are notified of media
+ * source events.
+ *
+ * @param handler A handler on the which listener events will be posted.
+ * @param eventListener The listener to be added.
+ */
+ void addEventListener(Handler handler, MediaSourceEventListener eventListener);
+
+ /**
+ * Removes a {@link MediaSourceEventListener} from the list of listeners which are notified of
+ * media source events.
+ *
+ * @param eventListener The listener to be removed.
+ */
+ void removeEventListener(MediaSourceEventListener eventListener);
+
+ /** Returns the tag set on the media source, or null if none was set. */
+ @Nullable
+ default Object getTag() {
+ return null;
+ }
+
+ /**
+ * Registers a {@link MediaSourceCaller}. Starts source preparation if needed and enables the
+ * source for the creation of {@link MediaPeriod MediaPerods}.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>{@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be called once
+ * the source has a {@link Timeline}.
+ *
+ * <p>For each call to this method, a call to {@link #releaseSource(MediaSourceCaller)} is needed
+ * to remove the caller and to release the source if no longer required.
+ *
+ * @param caller The {@link MediaSourceCaller} to be registered.
+ * @param mediaTransferListener The transfer listener which should be informed of any media data
+ * transfers. May be null if no listener is available. Note that this listener should be only
+ * informed of transfers related to the media loads and not of auxiliary loads for manifests
+ * and other data.
+ */
+ void prepareSource(MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener);
+
+ /**
+ * Throws any pending error encountered while loading or refreshing source information.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}.
+ */
+ void maybeThrowSourceInfoRefreshError() throws IOException;
+
+ /**
+ * Enables the source for the creation of {@link MediaPeriod MediaPeriods}.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}.
+ *
+ * @param caller The {@link MediaSourceCaller} enabling the source.
+ */
+ void enable(MediaSourceCaller caller);
+
+ /**
+ * Returns a new {@link MediaPeriod} identified by {@code periodId}.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>Must only be called if the source is enabled.
+ *
+ * @param id The identifier of the period.
+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+ * @param startPositionUs The expected start position, in microseconds.
+ * @return A new {@link MediaPeriod}.
+ */
+ MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs);
+
+ /**
+ * Releases the period.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * @param mediaPeriod The period to release.
+ */
+ void releasePeriod(MediaPeriod mediaPeriod);
+
+ /**
+ * Disables the source for the creation of {@link MediaPeriod MediaPeriods}. The implementation
+ * should not hold onto limited resources used for the creation of media periods.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>Must only be called after all {@link MediaPeriod MediaPeriods} previously created by {@link
+ * #createPeriod(MediaPeriodId, Allocator, long)} have been released by {@link
+ * #releasePeriod(MediaPeriod)}.
+ *
+ * @param caller The {@link MediaSourceCaller} disabling the source.
+ */
+ void disable(MediaSourceCaller caller);
+
+ /**
+ * Unregisters a caller, and disables and releases the source if no longer required.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>Must only be called if all created {@link MediaPeriod MediaPeriods} have been released by
+ * {@link #releasePeriod(MediaPeriod)}.
+ *
+ * @param caller The {@link MediaSourceCaller} to be unregistered.
+ */
+ void releaseSource(MediaSourceCaller caller);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java
new file mode 100644
index 0000000000..53c50d8a26
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java
@@ -0,0 +1,740 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import androidx.annotation.CheckResult;
+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.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** Interface for callbacks to be notified of {@link MediaSource} events. */
+public interface MediaSourceEventListener {
+
+ /** Media source load event information. */
+ final class LoadEventInfo {
+
+ /** Defines the requested data. */
+ public final DataSpec dataSpec;
+ /**
+ * The {@link Uri} from which data is being read. The uri will be identical to the one in {@link
+ * #dataSpec}.uri unless redirection has occurred. If redirection has occurred, this is the uri
+ * after redirection.
+ */
+ public final Uri uri;
+ /** The response headers associated with the load, or an empty map if unavailable. */
+ public final Map<String, List<String>> responseHeaders;
+ /** The value of {@link SystemClock#elapsedRealtime} at the time of the load event. */
+ public final long elapsedRealtimeMs;
+ /** The duration of the load up to the event time. */
+ public final long loadDurationMs;
+ /** The number of bytes that were loaded up to the event time. */
+ public final long bytesLoaded;
+
+ /**
+ * Creates load event info.
+ *
+ * @param dataSpec Defines the requested data.
+ * @param uri The {@link Uri} from which data is being read. The uri must be identical to the
+ * one in {@code dataSpec.uri} unless redirection has occurred. If redirection has occurred,
+ * this is the uri after redirection.
+ * @param responseHeaders The response headers associated with the load, or an empty map if
+ * unavailable.
+ * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} at the time of the
+ * load event.
+ * @param loadDurationMs The duration of the load up to the event time.
+ * @param bytesLoaded The number of bytes that were loaded up to the event time. For compressed
+ * network responses, this is the decompressed size.
+ */
+ public LoadEventInfo(
+ DataSpec dataSpec,
+ Uri uri,
+ Map<String, List<String>> responseHeaders,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ long bytesLoaded) {
+ this.dataSpec = dataSpec;
+ this.uri = uri;
+ this.responseHeaders = responseHeaders;
+ this.elapsedRealtimeMs = elapsedRealtimeMs;
+ this.loadDurationMs = loadDurationMs;
+ this.bytesLoaded = bytesLoaded;
+ }
+ }
+
+ /** Descriptor for data being loaded or selected by a media source. */
+ final class MediaLoadData {
+
+ /** One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data. */
+ public final int dataType;
+ /**
+ * One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to media of a
+ * specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+ */
+ public final int trackType;
+ /**
+ * The format of the track to which the data belongs. Null if the data does not belong to a
+ * specific track.
+ */
+ @Nullable public final Format trackFormat;
+ /**
+ * One of the {@link C} {@code SELECTION_REASON_*} constants if the data belongs to a track.
+ * {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+ */
+ public final int trackSelectionReason;
+ /**
+ * Optional data associated with the selection of the track to which the data belongs. Null if
+ * the data does not belong to a track.
+ */
+ @Nullable public final Object trackSelectionData;
+ /**
+ * The start time of the media, or {@link C#TIME_UNSET} if the data does not belong to a
+ * specific media period.
+ */
+ public final long mediaStartTimeMs;
+ /**
+ * The end time of the media, or {@link C#TIME_UNSET} if the data does not belong to a specific
+ * media period or the end time is unknown.
+ */
+ public final long mediaEndTimeMs;
+
+ /**
+ * Creates media load data.
+ *
+ * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data.
+ * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
+ * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+ * @param trackFormat The format of the track to which the data belongs. Null if the data does
+ * not belong to a track.
+ * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+ * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+ * @param trackSelectionData Optional data associated with the selection of the track to which
+ * the data belongs. Null if the data does not belong to a track.
+ * @param mediaStartTimeMs The start time of the media, or {@link C#TIME_UNSET} if the data does
+ * not belong to a specific media period.
+ * @param mediaEndTimeMs The end time of the media, or {@link C#TIME_UNSET} if the data does not
+ * belong to a specific media period or the end time is unknown.
+ */
+ public MediaLoadData(
+ int dataType,
+ int trackType,
+ @Nullable Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long mediaStartTimeMs,
+ long mediaEndTimeMs) {
+ this.dataType = dataType;
+ this.trackType = trackType;
+ this.trackFormat = trackFormat;
+ this.trackSelectionReason = trackSelectionReason;
+ this.trackSelectionData = trackSelectionData;
+ this.mediaStartTimeMs = mediaStartTimeMs;
+ this.mediaEndTimeMs = mediaEndTimeMs;
+ }
+ }
+
+ /**
+ * Called when a media period is created by the media source.
+ *
+ * @param windowIndex The window index in the timeline this media period belongs to.
+ * @param mediaPeriodId The {@link MediaPeriodId} of the created media period.
+ */
+ default void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {}
+
+ /**
+ * Called when a media period is released by the media source.
+ *
+ * @param windowIndex The window index in the timeline this media period belongs to.
+ * @param mediaPeriodId The {@link MediaPeriodId} of the released media period.
+ */
+ default void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {}
+
+ /**
+ * Called when a load begins.
+ *
+ * @param windowIndex The window index in the timeline of the media source this load belongs to.
+ * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not
+ * belong to a specific media period.
+ * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The value of {@link
+ * LoadEventInfo#uri} won't reflect potential redirection yet and {@link
+ * LoadEventInfo#responseHeaders} will be empty.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ */
+ default void onLoadStarted(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData) {}
+
+ /**
+ * Called when a load ends.
+ *
+ * @param windowIndex The window index in the timeline of the media source this load belongs to.
+ * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not
+ * belong to a specific media period.
+ * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link
+ * LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the
+ * corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}
+ * event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ */
+ default void onLoadCompleted(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData) {}
+
+ /**
+ * Called when a load is canceled.
+ *
+ * @param windowIndex The window index in the timeline of the media source this load belongs to.
+ * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not
+ * belong to a specific media period.
+ * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link
+ * LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the
+ * corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}
+ * event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ */
+ default void onLoadCanceled(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData) {}
+
+ /**
+ * Called when a load error occurs.
+ *
+ * <p>The error may or may not have resulted in the load being canceled, as indicated by the
+ * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will
+ * <em>not</em> be called in addition to this method.
+ *
+ * <p>This method being called does not indicate that playback has failed, or that it will fail.
+ * The player may be able to recover from the error and continue. Hence applications should
+ * <em>not</em> implement this method to display a user visible error or initiate an application
+ * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement
+ * such behavior). This method is called to provide the application with an opportunity to log the
+ * error if it wishes to do so.
+ *
+ * @param windowIndex The window index in the timeline of the media source this load belongs to.
+ * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not
+ * belong to a specific media period.
+ * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link
+ * LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the
+ * corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}
+ * event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ * @param error The load error.
+ * @param wasCanceled Whether the load was canceled as a result of the error.
+ */
+ default void onLoadError(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {}
+
+ /**
+ * Called when a media period is first being read from.
+ *
+ * @param windowIndex The window index in the timeline this media period belongs to.
+ * @param mediaPeriodId The {@link MediaPeriodId} of the media period being read from.
+ */
+ default void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {}
+
+ /**
+ * Called when data is removed from the back of a media buffer, typically so that it can be
+ * re-buffered in a different format.
+ *
+ * @param windowIndex The window index in the timeline of the media source this load belongs to.
+ * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to.
+ * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded.
+ */
+ default void onUpstreamDiscarded(
+ int windowIndex, MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {}
+
+ /**
+ * Called when a downstream format change occurs (i.e. when the format of the media being read
+ * from one or more {@link SampleStream}s provided by the source changes).
+ *
+ * @param windowIndex The window index in the timeline of the media source this load belongs to.
+ * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to.
+ * @param mediaLoadData The {@link MediaLoadData} defining the newly selected downstream data.
+ */
+ default void onDownstreamFormatChanged(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {}
+
+ /** Dispatches events to {@link MediaSourceEventListener}s. */
+ final class EventDispatcher {
+
+ /** The timeline window index reported with the events. */
+ public final int windowIndex;
+ /** The {@link MediaPeriodId} reported with the events. */
+ @Nullable public final MediaPeriodId mediaPeriodId;
+
+ private final CopyOnWriteArrayList<ListenerAndHandler> listenerAndHandlers;
+ private final long mediaTimeOffsetMs;
+
+ /** Creates an event dispatcher. */
+ public EventDispatcher() {
+ this(
+ /* listenerAndHandlers= */ new CopyOnWriteArrayList<>(),
+ /* windowIndex= */ 0,
+ /* mediaPeriodId= */ null,
+ /* mediaTimeOffsetMs= */ 0);
+ }
+
+ private EventDispatcher(
+ CopyOnWriteArrayList<ListenerAndHandler> listenerAndHandlers,
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ long mediaTimeOffsetMs) {
+ this.listenerAndHandlers = listenerAndHandlers;
+ this.windowIndex = windowIndex;
+ this.mediaPeriodId = mediaPeriodId;
+ this.mediaTimeOffsetMs = mediaTimeOffsetMs;
+ }
+
+ /**
+ * Creates a view of the event dispatcher with pre-configured window index, media period id, and
+ * media time offset.
+ *
+ * @param windowIndex The timeline window index to be reported with the events.
+ * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events.
+ * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds.
+ * @return A view of the event dispatcher with the pre-configured parameters.
+ */
+ @CheckResult
+ public EventDispatcher withParameters(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {
+ return new EventDispatcher(
+ listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs);
+ }
+
+ /**
+ * Adds a listener to the event dispatcher.
+ *
+ * @param handler A handler on the which listener events will be posted.
+ * @param eventListener The listener to be added.
+ */
+ public void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
+ Assertions.checkArgument(handler != null && eventListener != null);
+ listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener));
+ }
+
+ /**
+ * Removes a listener from the event dispatcher.
+ *
+ * @param eventListener The listener to be removed.
+ */
+ public void removeEventListener(MediaSourceEventListener eventListener) {
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ if (listenerAndHandler.listener == eventListener) {
+ listenerAndHandlers.remove(listenerAndHandler);
+ }
+ }
+ }
+
+ /** Dispatches {@link #onMediaPeriodCreated(int, MediaPeriodId)}. */
+ public void mediaPeriodCreated() {
+ MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId);
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ final MediaSourceEventListener listener = listenerAndHandler.listener;
+ postOrRun(
+ listenerAndHandler.handler,
+ () -> listener.onMediaPeriodCreated(windowIndex, mediaPeriodId));
+ }
+ }
+
+ /** Dispatches {@link #onMediaPeriodReleased(int, MediaPeriodId)}. */
+ public void mediaPeriodReleased() {
+ MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId);
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ final MediaSourceEventListener listener = listenerAndHandler.listener;
+ postOrRun(
+ listenerAndHandler.handler,
+ () -> listener.onMediaPeriodReleased(windowIndex, mediaPeriodId));
+ }
+ }
+
+ /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
+ public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {
+ loadStarted(
+ dataSpec,
+ dataType,
+ C.TRACK_TYPE_UNKNOWN,
+ null,
+ C.SELECTION_REASON_UNKNOWN,
+ null,
+ C.TIME_UNSET,
+ C.TIME_UNSET,
+ elapsedRealtimeMs);
+ }
+
+ /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
+ public void loadStarted(
+ DataSpec dataSpec,
+ int dataType,
+ int trackType,
+ @Nullable Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long mediaStartTimeUs,
+ long mediaEndTimeUs,
+ long elapsedRealtimeMs) {
+ loadStarted(
+ new LoadEventInfo(
+ dataSpec,
+ dataSpec.uri,
+ /* responseHeaders= */ Collections.emptyMap(),
+ elapsedRealtimeMs,
+ /* loadDurationMs= */ 0,
+ /* bytesLoaded= */ 0),
+ new MediaLoadData(
+ dataType,
+ trackType,
+ trackFormat,
+ trackSelectionReason,
+ trackSelectionData,
+ adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs)));
+ }
+
+ /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
+ public void loadStarted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ final MediaSourceEventListener listener = listenerAndHandler.listener;
+ postOrRun(
+ listenerAndHandler.handler,
+ () -> listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData));
+ }
+ }
+
+ /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
+ public void loadCompleted(
+ DataSpec dataSpec,
+ Uri uri,
+ Map<String, List<String>> responseHeaders,
+ int dataType,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ long bytesLoaded) {
+ loadCompleted(
+ dataSpec,
+ uri,
+ responseHeaders,
+ dataType,
+ C.TRACK_TYPE_UNKNOWN,
+ null,
+ C.SELECTION_REASON_UNKNOWN,
+ null,
+ C.TIME_UNSET,
+ C.TIME_UNSET,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ bytesLoaded);
+ }
+
+ /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
+ public void loadCompleted(
+ DataSpec dataSpec,
+ Uri uri,
+ Map<String, List<String>> responseHeaders,
+ int dataType,
+ int trackType,
+ @Nullable Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long mediaStartTimeUs,
+ long mediaEndTimeUs,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ long bytesLoaded) {
+ loadCompleted(
+ new LoadEventInfo(
+ dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
+ new MediaLoadData(
+ dataType,
+ trackType,
+ trackFormat,
+ trackSelectionReason,
+ trackSelectionData,
+ adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs)));
+ }
+
+ /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
+ public void loadCompleted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ final MediaSourceEventListener listener = listenerAndHandler.listener;
+ postOrRun(
+ listenerAndHandler.handler,
+ () ->
+ listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData));
+ }
+ }
+
+ /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
+ public void loadCanceled(
+ DataSpec dataSpec,
+ Uri uri,
+ Map<String, List<String>> responseHeaders,
+ int dataType,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ long bytesLoaded) {
+ loadCanceled(
+ dataSpec,
+ uri,
+ responseHeaders,
+ dataType,
+ C.TRACK_TYPE_UNKNOWN,
+ null,
+ C.SELECTION_REASON_UNKNOWN,
+ null,
+ C.TIME_UNSET,
+ C.TIME_UNSET,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ bytesLoaded);
+ }
+
+ /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
+ public void loadCanceled(
+ DataSpec dataSpec,
+ Uri uri,
+ Map<String, List<String>> responseHeaders,
+ int dataType,
+ int trackType,
+ @Nullable Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long mediaStartTimeUs,
+ long mediaEndTimeUs,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ long bytesLoaded) {
+ loadCanceled(
+ new LoadEventInfo(
+ dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
+ new MediaLoadData(
+ dataType,
+ trackType,
+ trackFormat,
+ trackSelectionReason,
+ trackSelectionData,
+ adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs)));
+ }
+
+ /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
+ public void loadCanceled(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ MediaSourceEventListener listener = listenerAndHandler.listener;
+ postOrRun(
+ listenerAndHandler.handler,
+ () ->
+ listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData));
+ }
+ }
+
+ /**
+ * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException,
+ * boolean)}.
+ */
+ public void loadError(
+ DataSpec dataSpec,
+ Uri uri,
+ Map<String, List<String>> responseHeaders,
+ int dataType,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ long bytesLoaded,
+ IOException error,
+ boolean wasCanceled) {
+ loadError(
+ dataSpec,
+ uri,
+ responseHeaders,
+ dataType,
+ C.TRACK_TYPE_UNKNOWN,
+ null,
+ C.SELECTION_REASON_UNKNOWN,
+ null,
+ C.TIME_UNSET,
+ C.TIME_UNSET,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ bytesLoaded,
+ error,
+ wasCanceled);
+ }
+
+ /**
+ * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException,
+ * boolean)}.
+ */
+ public void loadError(
+ DataSpec dataSpec,
+ Uri uri,
+ Map<String, List<String>> responseHeaders,
+ int dataType,
+ int trackType,
+ @Nullable Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long mediaStartTimeUs,
+ long mediaEndTimeUs,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ long bytesLoaded,
+ IOException error,
+ boolean wasCanceled) {
+ loadError(
+ new LoadEventInfo(
+ dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
+ new MediaLoadData(
+ dataType,
+ trackType,
+ trackFormat,
+ trackSelectionReason,
+ trackSelectionData,
+ adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs)),
+ error,
+ wasCanceled);
+ }
+
+ /**
+ * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException,
+ * boolean)}.
+ */
+ public void loadError(
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ final MediaSourceEventListener listener = listenerAndHandler.listener;
+ postOrRun(
+ listenerAndHandler.handler,
+ () ->
+ listener.onLoadError(
+ windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled));
+ }
+ }
+
+ /** Dispatches {@link #onReadingStarted(int, MediaPeriodId)}. */
+ public void readingStarted() {
+ MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId);
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ final MediaSourceEventListener listener = listenerAndHandler.listener;
+ postOrRun(
+ listenerAndHandler.handler,
+ () -> listener.onReadingStarted(windowIndex, mediaPeriodId));
+ }
+ }
+
+ /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */
+ public void upstreamDiscarded(int trackType, long mediaStartTimeUs, long mediaEndTimeUs) {
+ upstreamDiscarded(
+ new MediaLoadData(
+ C.DATA_TYPE_MEDIA,
+ trackType,
+ /* trackFormat= */ null,
+ C.SELECTION_REASON_ADAPTIVE,
+ /* trackSelectionData= */ null,
+ adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs)));
+ }
+
+ /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */
+ public void upstreamDiscarded(MediaLoadData mediaLoadData) {
+ MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId);
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ final MediaSourceEventListener listener = listenerAndHandler.listener;
+ postOrRun(
+ listenerAndHandler.handler,
+ () -> listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData));
+ }
+ }
+
+ /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */
+ public void downstreamFormatChanged(
+ int trackType,
+ @Nullable Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long mediaTimeUs) {
+ downstreamFormatChanged(
+ new MediaLoadData(
+ C.DATA_TYPE_MEDIA,
+ trackType,
+ trackFormat,
+ trackSelectionReason,
+ trackSelectionData,
+ adjustMediaTime(mediaTimeUs),
+ /* mediaEndTimeMs= */ C.TIME_UNSET));
+ }
+
+ /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */
+ public void downstreamFormatChanged(MediaLoadData mediaLoadData) {
+ for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
+ final MediaSourceEventListener listener = listenerAndHandler.listener;
+ postOrRun(
+ listenerAndHandler.handler,
+ () -> listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData));
+ }
+ }
+
+ private long adjustMediaTime(long mediaTimeUs) {
+ long mediaTimeMs = C.usToMs(mediaTimeUs);
+ return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
+ }
+
+ private void postOrRun(Handler handler, Runnable runnable) {
+ if (handler.getLooper() == Looper.myLooper()) {
+ runnable.run();
+ } else {
+ handler.post(runnable);
+ }
+ }
+
+ private static final class ListenerAndHandler {
+
+ public final Handler handler;
+ public final MediaSourceEventListener listener;
+
+ public ListenerAndHandler(Handler handler, MediaSourceEventListener listener) {
+ this.handler = handler;
+ this.listener = listener;
+ }
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java
new file mode 100644
index 0000000000..37c9dcee25
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey;
+import java.util.List;
+
+/** Factory for creating {@link MediaSource}s from URIs. */
+public interface MediaSourceFactory {
+
+ /**
+ * Sets a list of {@link StreamKey StreamKeys} by which the manifest is filtered.
+ *
+ * @param streamKeys A list of {@link StreamKey StreamKeys}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.
+ */
+ default MediaSourceFactory setStreamKeys(List<StreamKey> streamKeys) {
+ return this;
+ }
+
+ /**
+ * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}.
+ *
+ * @param drmSessionManager The {@link DrmSessionManager}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ MediaSourceFactory setDrmSessionManager(DrmSessionManager<?> drmSessionManager);
+
+ /**
+ * Creates a new {@link MediaSource} with the specified {@code uri}.
+ *
+ * @param uri The URI to play.
+ * @return The new {@link MediaSource media source}.
+ */
+ MediaSource createMediaSource(Uri uri);
+
+ /**
+ * Returns the {@link C.ContentType content types} supported by media sources created by this
+ * factory.
+ */
+ @C.ContentType
+ int[] getSupportedTypes();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java
new file mode 100644
index 0000000000..f3315ec5cd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -0,0 +1,256 @@
+/*
+ * 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;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * Merges multiple {@link MediaPeriod}s.
+ */
+/* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ public final MediaPeriod[] periods;
+
+ private final IdentityHashMap<SampleStream, Integer> streamPeriodIndices;
+ private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
+ private final ArrayList<MediaPeriod> childrenPendingPreparation;
+
+ @Nullable private Callback callback;
+ @Nullable private TrackGroupArray trackGroups;
+ private MediaPeriod[] enabledPeriods;
+ private SequenceableLoader compositeSequenceableLoader;
+
+ public MergingMediaPeriod(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
+ MediaPeriod... periods) {
+ this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
+ this.periods = periods;
+ childrenPendingPreparation = new ArrayList<>();
+ compositeSequenceableLoader =
+ compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();
+ streamPeriodIndices = new IdentityHashMap<>();
+ enabledPeriods = new MediaPeriod[0];
+ }
+
+ @Override
+ public void prepare(Callback callback, long positionUs) {
+ this.callback = callback;
+ Collections.addAll(childrenPendingPreparation, periods);
+ for (MediaPeriod period : periods) {
+ period.prepare(this, positionUs);
+ }
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ for (MediaPeriod period : periods) {
+ period.maybeThrowPrepareError();
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return Assertions.checkNotNull(trackGroups);
+ }
+
+ @Override
+ public long selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs) {
+ // Map each selection and stream onto a child period index.
+ int[] streamChildIndices = new int[selections.length];
+ int[] selectionChildIndices = new int[selections.length];
+ for (int i = 0; i < selections.length; i++) {
+ streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET
+ : streamPeriodIndices.get(streams[i]);
+ selectionChildIndices[i] = C.INDEX_UNSET;
+ if (selections[i] != null) {
+ TrackGroup trackGroup = selections[i].getTrackGroup();
+ for (int j = 0; j < periods.length; j++) {
+ if (periods[j].getTrackGroups().indexOf(trackGroup) != C.INDEX_UNSET) {
+ selectionChildIndices[i] = j;
+ break;
+ }
+ }
+ }
+ }
+ streamPeriodIndices.clear();
+ // Select tracks for each child, copying the resulting streams back into a new streams array.
+ @NullableType SampleStream[] newStreams = new SampleStream[selections.length];
+ @NullableType SampleStream[] childStreams = new SampleStream[selections.length];
+ @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length];
+ ArrayList<MediaPeriod> enabledPeriodsList = new ArrayList<>(periods.length);
+ for (int i = 0; i < periods.length; i++) {
+ for (int j = 0; j < selections.length; j++) {
+ childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
+ childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
+ }
+ long selectPositionUs = periods[i].selectTracks(childSelections, mayRetainStreamFlags,
+ childStreams, streamResetFlags, positionUs);
+ if (i == 0) {
+ positionUs = selectPositionUs;
+ } else if (selectPositionUs != positionUs) {
+ throw new IllegalStateException("Children enabled at different positions.");
+ }
+ boolean periodEnabled = false;
+ for (int j = 0; j < selections.length; j++) {
+ if (selectionChildIndices[j] == i) {
+ // Assert that the child provided a stream for the selection.
+ SampleStream childStream = Assertions.checkNotNull(childStreams[j]);
+ newStreams[j] = childStreams[j];
+ periodEnabled = true;
+ streamPeriodIndices.put(childStream, i);
+ } else if (streamChildIndices[j] == i) {
+ // Assert that the child cleared any previous stream.
+ Assertions.checkState(childStreams[j] == null);
+ }
+ }
+ if (periodEnabled) {
+ enabledPeriodsList.add(periods[i]);
+ }
+ }
+ // Copy the new streams back into the streams array.
+ System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
+ // Update the local state.
+ enabledPeriods = new MediaPeriod[enabledPeriodsList.size()];
+ enabledPeriodsList.toArray(enabledPeriods);
+ compositeSequenceableLoader =
+ compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(enabledPeriods);
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ for (MediaPeriod period : enabledPeriods) {
+ period.discardBuffer(positionUs, toKeyframe);
+ }
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ compositeSequenceableLoader.reevaluateBuffer(positionUs);
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (!childrenPendingPreparation.isEmpty()) {
+ // Preparation is still going on.
+ int childrenPendingPreparationSize = childrenPendingPreparation.size();
+ for (int i = 0; i < childrenPendingPreparationSize; i++) {
+ childrenPendingPreparation.get(i).continueLoading(positionUs);
+ }
+ return false;
+ } else {
+ return compositeSequenceableLoader.continueLoading(positionUs);
+ }
+ }
+
+ @Override
+ public boolean isLoading() {
+ return compositeSequenceableLoader.isLoading();
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return compositeSequenceableLoader.getNextLoadPositionUs();
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ long positionUs = periods[0].readDiscontinuity();
+ // Periods other than the first one are not allowed to report discontinuities.
+ for (int i = 1; i < periods.length; i++) {
+ if (periods[i].readDiscontinuity() != C.TIME_UNSET) {
+ throw new IllegalStateException("Child reported discontinuity.");
+ }
+ }
+ // It must be possible to seek enabled periods to the new position, if there is one.
+ if (positionUs != C.TIME_UNSET) {
+ for (MediaPeriod enabledPeriod : enabledPeriods) {
+ if (enabledPeriod != periods[0]
+ && enabledPeriod.seekToUs(positionUs) != positionUs) {
+ throw new IllegalStateException("Unexpected child seekToUs result.");
+ }
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return compositeSequenceableLoader.getBufferedPositionUs();
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ positionUs = enabledPeriods[0].seekToUs(positionUs);
+ // Additional periods must seek to the same position.
+ for (int i = 1; i < enabledPeriods.length; i++) {
+ if (enabledPeriods[i].seekToUs(positionUs) != positionUs) {
+ throw new IllegalStateException("Unexpected child seekToUs result.");
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ MediaPeriod queryPeriod = enabledPeriods.length > 0 ? enabledPeriods[0] : periods[0];
+ return queryPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters);
+ }
+
+ // MediaPeriod.Callback implementation
+
+ @Override
+ public void onPrepared(MediaPeriod preparedPeriod) {
+ childrenPendingPreparation.remove(preparedPeriod);
+ if (!childrenPendingPreparation.isEmpty()) {
+ return;
+ }
+ int totalTrackGroupCount = 0;
+ for (MediaPeriod period : periods) {
+ totalTrackGroupCount += period.getTrackGroups().length;
+ }
+ TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
+ int trackGroupIndex = 0;
+ for (MediaPeriod period : periods) {
+ TrackGroupArray periodTrackGroups = period.getTrackGroups();
+ int periodTrackGroupCount = periodTrackGroups.length;
+ for (int j = 0; j < periodTrackGroupCount; j++) {
+ trackGroupArray[trackGroupIndex++] = periodTrackGroups.get(j);
+ }
+ }
+ trackGroups = new TrackGroupArray(trackGroupArray);
+ Assertions.checkNotNull(callback).onPrepared(this);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod ignored) {
+ Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java
new file mode 100644
index 0000000000..ac2ef3c7da
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Merges multiple {@link MediaSource}s.
+ *
+ * <p>The {@link Timeline}s of the sources being merged must have the same number of periods.
+ */
+public final class MergingMediaSource extends CompositeMediaSource<Integer> {
+
+ /**
+ * Thrown when a {@link MergingMediaSource} cannot merge its sources.
+ */
+ public static final class IllegalMergeException extends IOException {
+
+ /** The reason the merge failed. One of {@link #REASON_PERIOD_COUNT_MISMATCH}. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REASON_PERIOD_COUNT_MISMATCH})
+ public @interface Reason {}
+ /**
+ * The sources have different period counts.
+ */
+ public static final int REASON_PERIOD_COUNT_MISMATCH = 0;
+
+ /**
+ * The reason the merge failed.
+ */
+ @Reason public final int reason;
+
+ /**
+ * @param reason The reason the merge failed.
+ */
+ public IllegalMergeException(@Reason int reason) {
+ this.reason = reason;
+ }
+
+ }
+
+ private static final int PERIOD_COUNT_UNSET = -1;
+
+ private final MediaSource[] mediaSources;
+ private final Timeline[] timelines;
+ private final ArrayList<MediaSource> pendingTimelineSources;
+ private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
+
+ private int periodCount;
+ @Nullable private IllegalMergeException mergeError;
+
+ /**
+ * @param mediaSources The {@link MediaSource}s to merge.
+ */
+ public MergingMediaSource(MediaSource... mediaSources) {
+ this(new DefaultCompositeSequenceableLoaderFactory(), mediaSources);
+ }
+
+ /**
+ * @param compositeSequenceableLoaderFactory A factory to create composite
+ * {@link SequenceableLoader}s for when this media source loads data from multiple streams
+ * (video, audio etc...).
+ * @param mediaSources The {@link MediaSource}s to merge.
+ */
+ public MergingMediaSource(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
+ MediaSource... mediaSources) {
+ this.mediaSources = mediaSources;
+ this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
+ pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));
+ periodCount = PERIOD_COUNT_UNSET;
+ timelines = new Timeline[mediaSources.length];
+ }
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return mediaSources.length > 0 ? mediaSources[0].getTag() : null;
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ for (int i = 0; i < mediaSources.length; i++) {
+ prepareChildSource(i, mediaSources[i]);
+ }
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ if (mergeError != null) {
+ throw mergeError;
+ }
+ super.maybeThrowSourceInfoRefreshError();
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
+ int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
+ for (int i = 0; i < periods.length; i++) {
+ MediaPeriodId childMediaPeriodId =
+ id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
+ periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs);
+ }
+ return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod;
+ for (int i = 0; i < mediaSources.length; i++) {
+ mediaSources[i].releasePeriod(mergingPeriod.periods[i]);
+ }
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ super.releaseSourceInternal();
+ Arrays.fill(timelines, null);
+ periodCount = PERIOD_COUNT_UNSET;
+ mergeError = null;
+ pendingTimelineSources.clear();
+ Collections.addAll(pendingTimelineSources, mediaSources);
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ Integer id, MediaSource mediaSource, Timeline timeline) {
+ if (mergeError == null) {
+ mergeError = checkTimelineMerges(timeline);
+ }
+ if (mergeError != null) {
+ return;
+ }
+ pendingTimelineSources.remove(mediaSource);
+ timelines[id] = timeline;
+ if (pendingTimelineSources.isEmpty()) {
+ refreshSourceInfo(timelines[0]);
+ }
+ }
+
+ @Override
+ @Nullable
+ protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
+ Integer id, MediaPeriodId mediaPeriodId) {
+ return id == 0 ? mediaPeriodId : null;
+ }
+
+ @Nullable
+ private IllegalMergeException checkTimelineMerges(Timeline timeline) {
+ if (periodCount == PERIOD_COUNT_UNSET) {
+ periodCount = timeline.getPeriodCount();
+ } else if (timeline.getPeriodCount() != periodCount) {
+ return new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH);
+ }
+ return null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
new file mode 100644
index 0000000000..4c62a73edb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
@@ -0,0 +1,1162 @@
+/*
+ * 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;
+
+import android.net.Uri;
+import android.os.Handler;
+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.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap.Unseekable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy.IcyHeaders;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+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.Loader.Loadable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.StatsDataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ConditionVariable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */
+/* package */ final class ProgressiveMediaPeriod
+ implements MediaPeriod,
+ ExtractorOutput,
+ Loader.Callback<ProgressiveMediaPeriod.ExtractingLoadable>,
+ Loader.ReleaseCallback,
+ UpstreamFormatChangedListener {
+
+ /**
+ * Listener for information about the period.
+ */
+ interface Listener {
+
+ /**
+ * Called when the duration, the ability to seek within the period, or the categorization as
+ * live stream changes.
+ *
+ * @param durationUs The duration of the period, or {@link C#TIME_UNSET}.
+ * @param isSeekable Whether the period is seekable.
+ * @param isLive Whether the period is live.
+ */
+ void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive);
+ }
+
+ /**
+ * When the source's duration is unknown, it is calculated by adding this value to the largest
+ * sample timestamp seen when buffering completes.
+ */
+ private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000;
+
+ private static final Map<String, String> ICY_METADATA_HEADERS = createIcyMetadataHeaders();
+
+ private static final Format ICY_FORMAT =
+ Format.createSampleFormat("icy", MimeTypes.APPLICATION_ICY, Format.OFFSET_SAMPLE_RELATIVE);
+
+ private final Uri uri;
+ private final DataSource dataSource;
+ private final DrmSessionManager<?> drmSessionManager;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final EventDispatcher eventDispatcher;
+ private final Listener listener;
+ private final Allocator allocator;
+ @Nullable private final String customCacheKey;
+ private final long continueLoadingCheckIntervalBytes;
+ private final Loader loader;
+ private final ExtractorHolder extractorHolder;
+ private final ConditionVariable loadCondition;
+ private final Runnable maybeFinishPrepareRunnable;
+ private final Runnable onContinueLoadingRequestedRunnable;
+ private final Handler handler;
+
+ @Nullable private Callback callback;
+ @Nullable private SeekMap seekMap;
+ @Nullable private IcyHeaders icyHeaders;
+ private SampleQueue[] sampleQueues;
+ private TrackId[] sampleQueueTrackIds;
+ private boolean sampleQueuesBuilt;
+ private boolean prepared;
+
+ @Nullable private PreparedState preparedState;
+ private boolean haveAudioVideoTracks;
+ private int dataType;
+
+ private boolean seenFirstTrackSelection;
+ private boolean notifyDiscontinuity;
+ private boolean notifiedReadingStarted;
+ private int enabledTrackCount;
+ private long durationUs;
+ private long length;
+ private boolean isLive;
+
+ private long lastSeekPositionUs;
+ private long pendingResetPositionUs;
+ private boolean pendingDeferredRetry;
+
+ private int extractedSamplesCountAtStartOfLoad;
+ private boolean loadingFinished;
+ private boolean released;
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSource The data source to read the media.
+ * @param extractors The extractors to use to read the data source.
+ * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
+ * @param eventDispatcher A dispatcher to notify of events.
+ * @param listener A listener to notify when information about the period changes.
+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+ * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
+ * indexing. May be null.
+ * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
+ * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
+ */
+ // maybeFinishPrepare is not posted to the handler until initialization completes.
+ @SuppressWarnings({
+ "nullness:argument.type.incompatible",
+ "nullness:methodref.receiver.bound.invalid"
+ })
+ public ProgressiveMediaPeriod(
+ Uri uri,
+ DataSource dataSource,
+ Extractor[] extractors,
+ DrmSessionManager<?> drmSessionManager,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ EventDispatcher eventDispatcher,
+ Listener listener,
+ Allocator allocator,
+ @Nullable String customCacheKey,
+ int continueLoadingCheckIntervalBytes) {
+ this.uri = uri;
+ this.dataSource = dataSource;
+ this.drmSessionManager = drmSessionManager;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ this.eventDispatcher = eventDispatcher;
+ this.listener = listener;
+ this.allocator = allocator;
+ this.customCacheKey = customCacheKey;
+ this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
+ loader = new Loader("Loader:ProgressiveMediaPeriod");
+ extractorHolder = new ExtractorHolder(extractors);
+ loadCondition = new ConditionVariable();
+ maybeFinishPrepareRunnable = this::maybeFinishPrepare;
+ onContinueLoadingRequestedRunnable =
+ () -> {
+ if (!released) {
+ Assertions.checkNotNull(callback)
+ .onContinueLoadingRequested(ProgressiveMediaPeriod.this);
+ }
+ };
+ handler = new Handler();
+ sampleQueueTrackIds = new TrackId[0];
+ sampleQueues = new SampleQueue[0];
+ pendingResetPositionUs = C.TIME_UNSET;
+ length = C.LENGTH_UNSET;
+ durationUs = C.TIME_UNSET;
+ dataType = C.DATA_TYPE_MEDIA;
+ eventDispatcher.mediaPeriodCreated();
+ }
+
+ public void release() {
+ if (prepared) {
+ // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise
+ // sampleQueues may still be being modified by the loading thread.
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.preRelease();
+ }
+ }
+ loader.release(/* callback= */ this);
+ handler.removeCallbacksAndMessages(null);
+ callback = null;
+ released = true;
+ eventDispatcher.mediaPeriodReleased();
+ }
+
+ @Override
+ public void onLoaderReleased() {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.release();
+ }
+ extractorHolder.release();
+ }
+
+ @Override
+ public void prepare(Callback callback, long positionUs) {
+ this.callback = callback;
+ loadCondition.open();
+ startLoading();
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ maybeThrowError();
+ if (loadingFinished && !prepared) {
+ throw new ParserException("Loading finished before preparation is complete.");
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return getPreparedState().tracks;
+ }
+
+ @Override
+ public long selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs) {
+ PreparedState preparedState = getPreparedState();
+ TrackGroupArray tracks = preparedState.tracks;
+ boolean[] trackEnabledStates = preparedState.trackEnabledStates;
+ int oldEnabledTrackCount = enabledTrackCount;
+ // Deselect old tracks.
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+ int track = ((SampleStreamImpl) streams[i]).track;
+ Assertions.checkState(trackEnabledStates[track]);
+ enabledTrackCount--;
+ trackEnabledStates[track] = false;
+ streams[i] = null;
+ }
+ }
+ // We'll always need to seek if this is a first selection to a non-zero position, or if we're
+ // making a selection having previously disabled all tracks.
+ boolean seekRequired = seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0;
+ // Select new tracks.
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] == null && selections[i] != null) {
+ TrackSelection selection = selections[i];
+ Assertions.checkState(selection.length() == 1);
+ Assertions.checkState(selection.getIndexInTrackGroup(0) == 0);
+ int track = tracks.indexOf(selection.getTrackGroup());
+ Assertions.checkState(!trackEnabledStates[track]);
+ enabledTrackCount++;
+ trackEnabledStates[track] = true;
+ streams[i] = new SampleStreamImpl(track);
+ streamResetFlags[i] = true;
+ // If there's still a chance of avoiding a seek, try and seek within the sample queue.
+ if (!seekRequired) {
+ SampleQueue sampleQueue = sampleQueues[track];
+ // A seek can be avoided if we're able to seek to the current playback position in the
+ // sample queue, or if we haven't read anything from the queue since the previous seek
+ // (this case is common for sparse tracks such as metadata tracks). In all other cases a
+ // seek is required.
+ seekRequired =
+ !sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true)
+ && sampleQueue.getReadIndex() != 0;
+ }
+ }
+ }
+ if (enabledTrackCount == 0) {
+ pendingDeferredRetry = false;
+ notifyDiscontinuity = false;
+ if (loader.isLoading()) {
+ // Discard as much as we can synchronously.
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.discardToEnd();
+ }
+ loader.cancelLoading();
+ } else {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ }
+ } else if (seekRequired) {
+ positionUs = seekToUs(positionUs);
+ // We'll need to reset renderers consuming from all streams due to the seek.
+ for (int i = 0; i < streams.length; i++) {
+ if (streams[i] != null) {
+ streamResetFlags[i] = true;
+ }
+ }
+ }
+ seenFirstTrackSelection = true;
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ if (isPendingReset()) {
+ return;
+ }
+ boolean[] trackEnabledStates = getPreparedState().trackEnabledStates;
+ int trackCount = sampleQueues.length;
+ for (int i = 0; i < trackCount; i++) {
+ sampleQueues[i].discardTo(positionUs, toKeyframe, trackEnabledStates[i]);
+ }
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ // Do nothing.
+ }
+
+ @Override
+ public boolean continueLoading(long playbackPositionUs) {
+ if (loadingFinished
+ || loader.hasFatalError()
+ || pendingDeferredRetry
+ || (prepared && enabledTrackCount == 0)) {
+ return false;
+ }
+ boolean continuedLoading = loadCondition.open();
+ if (!loader.isLoading()) {
+ startLoading();
+ continuedLoading = true;
+ }
+ return continuedLoading;
+ }
+
+ @Override
+ public boolean isLoading() {
+ return loader.isLoading() && loadCondition.isOpen();
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs();
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ if (!notifiedReadingStarted) {
+ eventDispatcher.readingStarted();
+ notifiedReadingStarted = true;
+ }
+ if (notifyDiscontinuity
+ && (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) {
+ notifyDiscontinuity = false;
+ return lastSeekPositionUs;
+ }
+ return C.TIME_UNSET;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags;
+ if (loadingFinished) {
+ return C.TIME_END_OF_SOURCE;
+ } else if (isPendingReset()) {
+ return pendingResetPositionUs;
+ }
+ long largestQueuedTimestampUs = Long.MAX_VALUE;
+ if (haveAudioVideoTracks) {
+ // Ignore non-AV tracks, which may be sparse or poorly interleaved.
+ int trackCount = sampleQueues.length;
+ for (int i = 0; i < trackCount; i++) {
+ if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {
+ largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
+ sampleQueues[i].getLargestQueuedTimestampUs());
+ }
+ }
+ }
+ if (largestQueuedTimestampUs == Long.MAX_VALUE) {
+ largestQueuedTimestampUs = getLargestQueuedTimestampUs();
+ }
+ return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
+ : largestQueuedTimestampUs;
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ PreparedState preparedState = getPreparedState();
+ SeekMap seekMap = preparedState.seekMap;
+ boolean[] trackIsAudioVideoFlags = preparedState.trackIsAudioVideoFlags;
+ // Treat all seeks into non-seekable media as being to t=0.
+ positionUs = seekMap.isSeekable() ? positionUs : 0;
+
+ notifyDiscontinuity = false;
+ lastSeekPositionUs = positionUs;
+ if (isPendingReset()) {
+ // A reset is already pending. We only need to update its position.
+ pendingResetPositionUs = positionUs;
+ return positionUs;
+ }
+
+ // If we're not playing a live stream, try and seek within the buffer.
+ if (dataType != C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE
+ && seekInsideBufferUs(trackIsAudioVideoFlags, positionUs)) {
+ return positionUs;
+ }
+
+ // We can't seek inside the buffer, and so need to reset.
+ pendingDeferredRetry = false;
+ pendingResetPositionUs = positionUs;
+ loadingFinished = false;
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ } else {
+ loader.clearFatalError();
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ SeekMap seekMap = getPreparedState().seekMap;
+ if (!seekMap.isSeekable()) {
+ // Treat all seeks into non-seekable media as being to t=0.
+ return 0;
+ }
+ SeekPoints seekPoints = seekMap.getSeekPoints(positionUs);
+ return Util.resolveSeekPositionUs(
+ positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs);
+ }
+
+ // SampleStream methods.
+
+ /* package */ boolean isReady(int track) {
+ return !suppressRead() && sampleQueues[track].isReady(loadingFinished);
+ }
+
+ /* package */ void maybeThrowError(int sampleQueueIndex) throws IOException {
+ sampleQueues[sampleQueueIndex].maybeThrowError();
+ maybeThrowError();
+ }
+
+ /* package */ void maybeThrowError() throws IOException {
+ loader.maybeThrowError(loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType));
+ }
+
+ /* package */ int readData(
+ int sampleQueueIndex,
+ FormatHolder formatHolder,
+ DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ if (suppressRead()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ maybeNotifyDownstreamFormat(sampleQueueIndex);
+ int result =
+ sampleQueues[sampleQueueIndex].read(
+ formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs);
+ if (result == C.RESULT_NOTHING_READ) {
+ maybeStartDeferredRetry(sampleQueueIndex);
+ }
+ return result;
+ }
+
+ /* package */ int skipData(int track, long positionUs) {
+ if (suppressRead()) {
+ return 0;
+ }
+ maybeNotifyDownstreamFormat(track);
+ SampleQueue sampleQueue = sampleQueues[track];
+ int skipCount;
+ if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
+ skipCount = sampleQueue.advanceToEnd();
+ } else {
+ skipCount = sampleQueue.advanceTo(positionUs);
+ }
+ if (skipCount == 0) {
+ maybeStartDeferredRetry(track);
+ }
+ return skipCount;
+ }
+
+ private void maybeNotifyDownstreamFormat(int track) {
+ PreparedState preparedState = getPreparedState();
+ boolean[] trackNotifiedDownstreamFormats = preparedState.trackNotifiedDownstreamFormats;
+ if (!trackNotifiedDownstreamFormats[track]) {
+ Format trackFormat = preparedState.tracks.get(track).getFormat(/* index= */ 0);
+ eventDispatcher.downstreamFormatChanged(
+ MimeTypes.getTrackType(trackFormat.sampleMimeType),
+ trackFormat,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ lastSeekPositionUs);
+ trackNotifiedDownstreamFormats[track] = true;
+ }
+ }
+
+ private void maybeStartDeferredRetry(int track) {
+ boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags;
+ if (!pendingDeferredRetry
+ || !trackIsAudioVideoFlags[track]
+ || sampleQueues[track].isReady(/* loadingFinished= */ false)) {
+ return;
+ }
+ pendingResetPositionUs = 0;
+ pendingDeferredRetry = false;
+ notifyDiscontinuity = true;
+ lastSeekPositionUs = 0;
+ extractedSamplesCountAtStartOfLoad = 0;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
+ }
+
+ private boolean suppressRead() {
+ return notifyDiscontinuity || isPendingReset();
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs,
+ long loadDurationMs) {
+ if (durationUs == C.TIME_UNSET && seekMap != null) {
+ boolean isSeekable = seekMap.isSeekable();
+ long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
+ durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0
+ : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
+ listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive);
+ }
+ eventDispatcher.loadCompleted(
+ loadable.dataSpec,
+ loadable.dataSource.getLastOpenedUri(),
+ loadable.dataSource.getLastResponseHeaders(),
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ loadable.seekTimeUs,
+ durationUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.dataSource.getBytesRead());
+ copyLengthFromLoader(loadable);
+ loadingFinished = true;
+ Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
+ }
+
+ @Override
+ public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,
+ long loadDurationMs, boolean released) {
+ eventDispatcher.loadCanceled(
+ loadable.dataSpec,
+ loadable.dataSource.getLastOpenedUri(),
+ loadable.dataSource.getLastResponseHeaders(),
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ loadable.seekTimeUs,
+ durationUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.dataSource.getBytesRead());
+ if (!released) {
+ copyLengthFromLoader(loadable);
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ if (enabledTrackCount > 0) {
+ Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
+ }
+ }
+ }
+
+ @Override
+ public LoadErrorAction onLoadError(
+ ExtractingLoadable loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ IOException error,
+ int errorCount) {
+ copyLengthFromLoader(loadable);
+ LoadErrorAction loadErrorAction;
+ long retryDelayMs =
+ loadErrorHandlingPolicy.getRetryDelayMsFor(dataType, loadDurationMs, error, errorCount);
+ if (retryDelayMs == C.TIME_UNSET) {
+ loadErrorAction = Loader.DONT_RETRY_FATAL;
+ } else /* the load should be retried */ {
+ int extractedSamplesCount = getExtractedSamplesCount();
+ boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad;
+ loadErrorAction =
+ configureRetry(loadable, extractedSamplesCount)
+ ? Loader.createRetryAction(/* resetErrorCount= */ madeProgress, retryDelayMs)
+ : Loader.DONT_RETRY;
+ }
+
+ eventDispatcher.loadError(
+ loadable.dataSpec,
+ loadable.dataSource.getLastOpenedUri(),
+ loadable.dataSource.getLastResponseHeaders(),
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ loadable.seekTimeUs,
+ durationUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.dataSource.getBytesRead(),
+ error,
+ !loadErrorAction.isRetry());
+ return loadErrorAction;
+ }
+
+ // ExtractorOutput implementation. Called by the loading thread.
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ return prepareTrackOutput(new TrackId(id, /* isIcyTrack= */ false));
+ }
+
+ @Override
+ public void endTracks() {
+ sampleQueuesBuilt = true;
+ handler.post(maybeFinishPrepareRunnable);
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs */ C.TIME_UNSET);
+ handler.post(maybeFinishPrepareRunnable);
+ }
+
+ // Icy metadata. Called by the loading thread.
+
+ /* package */ TrackOutput icyTrack() {
+ return prepareTrackOutput(new TrackId(0, /* isIcyTrack= */ true));
+ }
+
+ // UpstreamFormatChangedListener implementation. Called by the loading thread.
+
+ @Override
+ public void onUpstreamFormatChanged(Format format) {
+ handler.post(maybeFinishPrepareRunnable);
+ }
+
+ // Internal methods.
+
+ private TrackOutput prepareTrackOutput(TrackId id) {
+ int trackCount = sampleQueues.length;
+ for (int i = 0; i < trackCount; i++) {
+ if (id.equals(sampleQueueTrackIds[i])) {
+ return sampleQueues[i];
+ }
+ }
+ SampleQueue trackOutput = new SampleQueue(allocator, drmSessionManager);
+ trackOutput.setUpstreamFormatChangeListener(this);
+ @NullableType
+ TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1);
+ sampleQueueTrackIds[trackCount] = id;
+ this.sampleQueueTrackIds = Util.castNonNullTypeArray(sampleQueueTrackIds);
+ @NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1);
+ sampleQueues[trackCount] = trackOutput;
+ this.sampleQueues = Util.castNonNullTypeArray(sampleQueues);
+ return trackOutput;
+ }
+
+ private void maybeFinishPrepare() {
+ SeekMap seekMap = this.seekMap;
+ if (released || prepared || !sampleQueuesBuilt || seekMap == null) {
+ return;
+ }
+ for (SampleQueue sampleQueue : sampleQueues) {
+ if (sampleQueue.getUpstreamFormat() == null) {
+ return;
+ }
+ }
+ loadCondition.close();
+ int trackCount = sampleQueues.length;
+ TrackGroup[] trackArray = new TrackGroup[trackCount];
+ boolean[] trackIsAudioVideoFlags = new boolean[trackCount];
+ durationUs = seekMap.getDurationUs();
+ for (int i = 0; i < trackCount; i++) {
+ Format trackFormat = sampleQueues[i].getUpstreamFormat();
+ String mimeType = trackFormat.sampleMimeType;
+ boolean isAudio = MimeTypes.isAudio(mimeType);
+ boolean isAudioVideo = isAudio || MimeTypes.isVideo(mimeType);
+ trackIsAudioVideoFlags[i] = isAudioVideo;
+ haveAudioVideoTracks |= isAudioVideo;
+ IcyHeaders icyHeaders = this.icyHeaders;
+ if (icyHeaders != null) {
+ if (isAudio || sampleQueueTrackIds[i].isIcyTrack) {
+ Metadata metadata = trackFormat.metadata;
+ trackFormat =
+ trackFormat.copyWithMetadata(
+ metadata == null
+ ? new Metadata(icyHeaders)
+ : metadata.copyWithAppendedEntries(icyHeaders));
+ }
+ if (isAudio
+ && trackFormat.bitrate == Format.NO_VALUE
+ && icyHeaders.bitrate != Format.NO_VALUE) {
+ trackFormat = trackFormat.copyWithBitrate(icyHeaders.bitrate);
+ }
+ }
+ trackArray[i] = new TrackGroup(trackFormat);
+ }
+ isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET;
+ dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA;
+ preparedState =
+ new PreparedState(seekMap, new TrackGroupArray(trackArray), trackIsAudioVideoFlags);
+ prepared = true;
+ listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive);
+ Assertions.checkNotNull(callback).onPrepared(this);
+ }
+
+ private PreparedState getPreparedState() {
+ return Assertions.checkNotNull(preparedState);
+ }
+
+ private void copyLengthFromLoader(ExtractingLoadable loadable) {
+ if (length == C.LENGTH_UNSET) {
+ length = loadable.length;
+ }
+ }
+
+ private void startLoading() {
+ ExtractingLoadable loadable =
+ new ExtractingLoadable(
+ uri, dataSource, extractorHolder, /* extractorOutput= */ this, loadCondition);
+ if (prepared) {
+ SeekMap seekMap = getPreparedState().seekMap;
+ Assertions.checkState(isPendingReset());
+ if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) {
+ loadingFinished = true;
+ pendingResetPositionUs = C.TIME_UNSET;
+ return;
+ }
+ loadable.setLoadPosition(
+ seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs);
+ pendingResetPositionUs = C.TIME_UNSET;
+ }
+ extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
+ long elapsedRealtimeMs =
+ loader.startLoading(
+ loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType));
+ eventDispatcher.loadStarted(
+ loadable.dataSpec,
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ loadable.seekTimeUs,
+ durationUs,
+ elapsedRealtimeMs);
+ }
+
+ /**
+ * Called to configure a retry when a load error occurs.
+ *
+ * @param loadable The current loadable for which the error was encountered.
+ * @param currentExtractedSampleCount The current number of samples that have been extracted into
+ * the sample queues.
+ * @return Whether the loader should retry with the current loadable. False indicates a deferred
+ * retry.
+ */
+ private boolean configureRetry(ExtractingLoadable loadable, int currentExtractedSampleCount) {
+ if (length != C.LENGTH_UNSET
+ || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)) {
+ // We're playing an on-demand stream. Resume the current loadable, which will
+ // request data starting from the point it left off.
+ extractedSamplesCountAtStartOfLoad = currentExtractedSampleCount;
+ return true;
+ } else if (prepared && !suppressRead()) {
+ // We're playing a stream of unknown length and duration. Assume it's live, and therefore that
+ // the data at the uri is a continuously shifting window of the latest available media. For
+ // this case there's no way to continue loading from where a previous load finished, so it's
+ // necessary to load from the start whenever commencing a new load. Deferring the retry until
+ // we run out of buffered data makes for a much better user experience. See:
+ // https://github.com/google/ExoPlayer/issues/1606.
+ // Note that the suppressRead() check means only a single deferred retry can occur without
+ // progress being made. Any subsequent failures without progress will go through the else
+ // block below.
+ pendingDeferredRetry = true;
+ return false;
+ } else {
+ // This is the same case as above, except in this case there's no value in deferring the retry
+ // because there's no buffered data to be read. This case also covers an on-demand stream with
+ // unknown length that has yet to be prepared. This case cannot be disambiguated from the live
+ // stream case, so we have no option but to load from the start.
+ notifyDiscontinuity = prepared;
+ lastSeekPositionUs = 0;
+ extractedSamplesCountAtStartOfLoad = 0;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ loadable.setLoadPosition(0, 0);
+ return true;
+ }
+ }
+
+ /**
+ * Attempts to seek to the specified position within the sample queues.
+ *
+ * @param trackIsAudioVideoFlags Whether each track is audio/video.
+ * @param positionUs The seek position in microseconds.
+ * @return Whether the in-buffer seek was successful.
+ */
+ private boolean seekInsideBufferUs(boolean[] trackIsAudioVideoFlags, long positionUs) {
+ int trackCount = sampleQueues.length;
+ for (int i = 0; i < trackCount; i++) {
+ SampleQueue sampleQueue = sampleQueues[i];
+ boolean seekInsideQueue = sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false);
+ // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue
+ // is successful. We ignore whether seeks within non-AV queues are successful in this case, as
+ // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is
+ // successful only if the seek into every queue succeeds.
+ if (!seekInsideQueue && (trackIsAudioVideoFlags[i] || !haveAudioVideoTracks)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private int getExtractedSamplesCount() {
+ int extractedSamplesCount = 0;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ extractedSamplesCount += sampleQueue.getWriteIndex();
+ }
+ return extractedSamplesCount;
+ }
+
+ private long getLargestQueuedTimestampUs() {
+ long largestQueuedTimestampUs = Long.MIN_VALUE;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
+ sampleQueue.getLargestQueuedTimestampUs());
+ }
+ return largestQueuedTimestampUs;
+ }
+
+ private boolean isPendingReset() {
+ return pendingResetPositionUs != C.TIME_UNSET;
+ }
+
+ private final class SampleStreamImpl implements SampleStream {
+
+ private final int track;
+
+ public SampleStreamImpl(int track) {
+ this.track = track;
+ }
+
+ @Override
+ public boolean isReady() {
+ return ProgressiveMediaPeriod.this.isReady(track);
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ ProgressiveMediaPeriod.this.maybeThrowError(track);
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ return ProgressiveMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ return ProgressiveMediaPeriod.this.skipData(track, positionUs);
+ }
+
+ }
+
+ /** Loads the media stream and extracts sample data from it. */
+ /* package */ final class ExtractingLoadable implements Loadable, IcyDataSource.Listener {
+
+ private final Uri uri;
+ private final StatsDataSource dataSource;
+ private final ExtractorHolder extractorHolder;
+ private final ExtractorOutput extractorOutput;
+ private final ConditionVariable loadCondition;
+ private final PositionHolder positionHolder;
+
+ private volatile boolean loadCanceled;
+
+ private boolean pendingExtractorSeek;
+ private long seekTimeUs;
+ private DataSpec dataSpec;
+ private long length;
+ @Nullable private TrackOutput icyTrackOutput;
+ private boolean seenIcyMetadata;
+
+ @SuppressWarnings("method.invocation.invalid")
+ public ExtractingLoadable(
+ Uri uri,
+ DataSource dataSource,
+ ExtractorHolder extractorHolder,
+ ExtractorOutput extractorOutput,
+ ConditionVariable loadCondition) {
+ this.uri = uri;
+ this.dataSource = new StatsDataSource(dataSource);
+ this.extractorHolder = extractorHolder;
+ this.extractorOutput = extractorOutput;
+ this.loadCondition = loadCondition;
+ this.positionHolder = new PositionHolder();
+ this.pendingExtractorSeek = true;
+ this.length = C.LENGTH_UNSET;
+ dataSpec = buildDataSpec(/* position= */ 0);
+ }
+
+ // Loadable implementation.
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ ExtractorInput input = null;
+ try {
+ long position = positionHolder.position;
+ dataSpec = buildDataSpec(position);
+ length = dataSource.open(dataSpec);
+ if (length != C.LENGTH_UNSET) {
+ length += position;
+ }
+ Uri uri = Assertions.checkNotNull(dataSource.getUri());
+ icyHeaders = IcyHeaders.parse(dataSource.getResponseHeaders());
+ DataSource extractorDataSource = dataSource;
+ if (icyHeaders != null && icyHeaders.metadataInterval != C.LENGTH_UNSET) {
+ extractorDataSource = new IcyDataSource(dataSource, icyHeaders.metadataInterval, this);
+ icyTrackOutput = icyTrack();
+ icyTrackOutput.format(ICY_FORMAT);
+ }
+ input = new DefaultExtractorInput(extractorDataSource, position, length);
+ Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri);
+
+ // MP3 live streams commonly have seekable metadata, despite being unseekable.
+ if (icyHeaders != null && extractor instanceof Mp3Extractor) {
+ ((Mp3Extractor) extractor).disableSeeking();
+ }
+
+ if (pendingExtractorSeek) {
+ extractor.seek(position, seekTimeUs);
+ pendingExtractorSeek = false;
+ }
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ loadCondition.block();
+ result = extractor.read(input, positionHolder);
+ if (input.getPosition() > position + continueLoadingCheckIntervalBytes) {
+ position = input.getPosition();
+ loadCondition.close();
+ handler.post(onContinueLoadingRequestedRunnable);
+ }
+ }
+ } finally {
+ if (result == Extractor.RESULT_SEEK) {
+ result = Extractor.RESULT_CONTINUE;
+ } else if (input != null) {
+ positionHolder.position = input.getPosition();
+ }
+ Util.closeQuietly(dataSource);
+ }
+ }
+ }
+
+ // IcyDataSource.Listener
+
+ @Override
+ public void onIcyMetadata(ParsableByteArray metadata) {
+ // Always output the first ICY metadata at the start time. This helps minimize any delay
+ // between the start of playback and the first ICY metadata event.
+ long timeUs =
+ !seenIcyMetadata ? seekTimeUs : Math.max(getLargestQueuedTimestampUs(), seekTimeUs);
+ int length = metadata.bytesLeft();
+ TrackOutput icyTrackOutput = Assertions.checkNotNull(this.icyTrackOutput);
+ icyTrackOutput.sampleData(metadata, length);
+ icyTrackOutput.sampleMetadata(
+ timeUs, C.BUFFER_FLAG_KEY_FRAME, length, /* offset= */ 0, /* encryptionData= */ null);
+ seenIcyMetadata = true;
+ }
+
+ // Internal methods.
+
+ private DataSpec buildDataSpec(long position) {
+ // Disable caching if the content length cannot be resolved, since this is indicative of a
+ // progressive live stream.
+ return new DataSpec(
+ uri,
+ position,
+ C.LENGTH_UNSET,
+ customCacheKey,
+ DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION,
+ ICY_METADATA_HEADERS);
+ }
+
+ private void setLoadPosition(long position, long timeUs) {
+ positionHolder.position = position;
+ seekTimeUs = timeUs;
+ pendingExtractorSeek = true;
+ seenIcyMetadata = false;
+ }
+ }
+
+ /** Stores a list of extractors and a selected extractor when the format has been detected. */
+ private static final class ExtractorHolder {
+
+ private final Extractor[] extractors;
+
+ @Nullable private Extractor extractor;
+
+ /**
+ * Creates a holder that will select an extractor and initialize it using the specified output.
+ *
+ * @param extractors One or more extractors to choose from.
+ */
+ public ExtractorHolder(Extractor[] extractors) {
+ this.extractors = extractors;
+ }
+
+ /**
+ * Returns an initialized extractor for reading {@code input}, and returns the same extractor on
+ * later calls.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param output The {@link ExtractorOutput} that will be used to initialize the selected
+ * extractor.
+ * @param uri The {@link Uri} of the data.
+ * @return An initialized extractor for reading {@code input}.
+ * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected.
+ * @throws IOException Thrown if the input could not be read.
+ * @throws InterruptedException Thrown if the thread was interrupted.
+ */
+ public Extractor selectExtractor(ExtractorInput input, ExtractorOutput output, Uri uri)
+ throws IOException, InterruptedException {
+ if (extractor != null) {
+ return extractor;
+ }
+ if (extractors.length == 1) {
+ this.extractor = extractors[0];
+ } else {
+ for (Extractor extractor : extractors) {
+ try {
+ if (extractor.sniff(input)) {
+ this.extractor = extractor;
+ break;
+ }
+ } catch (EOFException e) {
+ // Do nothing.
+ } finally {
+ input.resetPeekPosition();
+ }
+ }
+ if (extractor == null) {
+ throw new UnrecognizedInputFormatException(
+ "None of the available extractors ("
+ + Util.getCommaDelimitedSimpleClassNames(extractors)
+ + ") could read the stream.",
+ uri);
+ }
+ }
+ extractor.init(output);
+ return extractor;
+ }
+
+ public void release() {
+ if (extractor != null) {
+ extractor.release();
+ extractor = null;
+ }
+ }
+ }
+
+ /** Stores state that is initialized when preparation completes. */
+ private static final class PreparedState {
+
+ public final SeekMap seekMap;
+ public final TrackGroupArray tracks;
+ public final boolean[] trackIsAudioVideoFlags;
+ public final boolean[] trackEnabledStates;
+ public final boolean[] trackNotifiedDownstreamFormats;
+
+ public PreparedState(
+ SeekMap seekMap, TrackGroupArray tracks, boolean[] trackIsAudioVideoFlags) {
+ this.seekMap = seekMap;
+ this.tracks = tracks;
+ this.trackIsAudioVideoFlags = trackIsAudioVideoFlags;
+ this.trackEnabledStates = new boolean[tracks.length];
+ this.trackNotifiedDownstreamFormats = new boolean[tracks.length];
+ }
+ }
+
+ /** Identifies a track. */
+ private static final class TrackId {
+
+ public final int id;
+ public final boolean isIcyTrack;
+
+ public TrackId(int id, boolean isIcyTrack) {
+ this.id = id;
+ this.isIcyTrack = isIcyTrack;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackId other = (TrackId) obj;
+ return id == other.id && isIcyTrack == other.isIcyTrack;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * id + (isIcyTrack ? 1 : 0);
+ }
+ }
+
+ private static Map<String, String> createIcyMetadataHeaders() {
+ Map<String, String> headers = new HashMap<>();
+ headers.put(
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
+ return Collections.unmodifiableMap(headers);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java
new file mode 100644
index 0000000000..bed34a354b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java
@@ -0,0 +1,327 @@
+/*
+ * 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;
+
+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.drm.DrmSession;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}.
+ *
+ * <p>If the possible input stream container formats are known, pass a factory that instantiates
+ * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use
+ * the default extractors. When reading a new stream, the first {@link Extractor} in the array of
+ * extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be
+ * used to extract samples from the input stream.
+ *
+ * <p>Note that the built-in extractor for FLV streams does not support seeking.
+ */
+public final class ProgressiveMediaSource extends BaseMediaSource
+ implements ProgressiveMediaPeriod.Listener {
+
+ /** Factory for {@link ProgressiveMediaSource}s. */
+ public static final class Factory implements MediaSourceFactory {
+
+ private final DataSource.Factory dataSourceFactory;
+
+ private ExtractorsFactory extractorsFactory;
+ @Nullable private String customCacheKey;
+ @Nullable private Object tag;
+ private DrmSessionManager<?> drmSessionManager;
+ private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private int continueLoadingCheckIntervalBytes;
+ private boolean isCreateCalled;
+
+ /**
+ * Creates a new factory for {@link ProgressiveMediaSource}s, using the extractors provided by
+ * {@link DefaultExtractorsFactory}.
+ *
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ */
+ public Factory(DataSource.Factory dataSourceFactory) {
+ this(dataSourceFactory, new DefaultExtractorsFactory());
+ }
+
+ /**
+ * Creates a new factory for {@link ProgressiveMediaSource}s.
+ *
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @param extractorsFactory A factory for extractors used to extract media from its container.
+ */
+ public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
+ this.dataSourceFactory = dataSourceFactory;
+ this.extractorsFactory = extractorsFactory;
+ drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
+ continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
+ }
+
+ /**
+ * Sets the factory for {@link Extractor}s to process the media stream. The default value is an
+ * instance of {@link DefaultExtractorsFactory}.
+ *
+ * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
+ * possible formats are known, pass a factory that instantiates extractors for those
+ * formats.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.
+ * @deprecated Pass the {@link ExtractorsFactory} via {@link #Factory(DataSource.Factory,
+ * ExtractorsFactory)}. This is necessary so that proguard can treat the default extractors
+ * factory as unused.
+ */
+ @Deprecated
+ public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) {
+ Assertions.checkState(!isCreateCalled);
+ this.extractorsFactory = extractorsFactory;
+ return this;
+ }
+
+ /**
+ * Sets the custom key that uniquely identifies the original stream. Used for cache indexing.
+ * The default value is {@code null}.
+ *
+ * @param customCacheKey A custom key that uniquely identifies the original stream. Used for
+ * cache indexing.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.
+ */
+ public Factory setCustomCacheKey(@Nullable String customCacheKey) {
+ Assertions.checkState(!isCreateCalled);
+ this.customCacheKey = customCacheKey;
+ return this;
+ }
+
+ /**
+ * Sets a tag for the media source which will be published in the {@link
+ * com.google.android.exoplayer2.Timeline} of the source as {@link
+ * com.google.android.exoplayer2.Timeline.Window#tag}.
+ *
+ * @param tag A tag for the media source.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.
+ */
+ public Factory setTag(Object tag) {
+ Assertions.checkState(!isCreateCalled);
+ this.tag = tag;
+ return this;
+ }
+
+ /**
+ * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link
+ * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.
+ *
+ * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.
+ */
+ public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
+ Assertions.checkState(!isCreateCalled);
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ return this;
+ }
+
+ /**
+ * Sets the number of bytes that should be loaded between each invocation of {@link
+ * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is
+ * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}.
+ *
+ * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between
+ * each invocation of {@link
+ * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.
+ */
+ public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) {
+ Assertions.checkState(!isCreateCalled);
+ this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
+ * default value is {@link DrmSessionManager#DUMMY}.
+ *
+ * @param drmSessionManager The {@link DrmSessionManager}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ @Override
+ public Factory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
+ Assertions.checkState(!isCreateCalled);
+ this.drmSessionManager = drmSessionManager;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link ProgressiveMediaSource} using the current parameters.
+ *
+ * @param uri The {@link Uri}.
+ * @return The new {@link ProgressiveMediaSource}.
+ */
+ @Override
+ public ProgressiveMediaSource createMediaSource(Uri uri) {
+ isCreateCalled = true;
+ return new ProgressiveMediaSource(
+ uri,
+ dataSourceFactory,
+ extractorsFactory,
+ drmSessionManager,
+ loadErrorHandlingPolicy,
+ customCacheKey,
+ continueLoadingCheckIntervalBytes,
+ tag);
+ }
+
+ @Override
+ public int[] getSupportedTypes() {
+ return new int[] {C.TYPE_OTHER};
+ }
+ }
+
+ /**
+ * The default number of bytes that should be loaded between each each invocation of {@link
+ * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
+ */
+ public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
+
+ private final Uri uri;
+ private final DataSource.Factory dataSourceFactory;
+ private final ExtractorsFactory extractorsFactory;
+ private final DrmSessionManager<?> drmSessionManager;
+ private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
+ @Nullable private final String customCacheKey;
+ private final int continueLoadingCheckIntervalBytes;
+ @Nullable private final Object tag;
+
+ private long timelineDurationUs;
+ private boolean timelineIsSeekable;
+ private boolean timelineIsLive;
+ @Nullable private TransferListener transferListener;
+
+ // TODO: Make private when ExtractorMediaSource is deleted.
+ /* package */ ProgressiveMediaSource(
+ Uri uri,
+ DataSource.Factory dataSourceFactory,
+ ExtractorsFactory extractorsFactory,
+ DrmSessionManager<?> drmSessionManager,
+ LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
+ @Nullable String customCacheKey,
+ int continueLoadingCheckIntervalBytes,
+ @Nullable Object tag) {
+ this.uri = uri;
+ this.dataSourceFactory = dataSourceFactory;
+ this.extractorsFactory = extractorsFactory;
+ this.drmSessionManager = drmSessionManager;
+ this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;
+ this.customCacheKey = customCacheKey;
+ this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
+ this.timelineDurationUs = C.TIME_UNSET;
+ this.tag = tag;
+ }
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return tag;
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ transferListener = mediaTransferListener;
+ drmSessionManager.prepare();
+ notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable, timelineIsLive);
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ DataSource dataSource = dataSourceFactory.createDataSource();
+ if (transferListener != null) {
+ dataSource.addTransferListener(transferListener);
+ }
+ return new ProgressiveMediaPeriod(
+ uri,
+ dataSource,
+ extractorsFactory.createExtractors(),
+ drmSessionManager,
+ loadableLoadErrorHandlingPolicy,
+ createEventDispatcher(id),
+ this,
+ allocator,
+ customCacheKey,
+ continueLoadingCheckIntervalBytes);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ ((ProgressiveMediaPeriod) mediaPeriod).release();
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ drmSessionManager.release();
+ }
+
+ // ProgressiveMediaPeriod.Listener implementation.
+
+ @Override
+ public void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) {
+ // If we already have the duration from a previous source info refresh, use it.
+ durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs;
+ if (timelineDurationUs == durationUs
+ && timelineIsSeekable == isSeekable
+ && timelineIsLive == isLive) {
+ // Suppress no-op source info changes.
+ return;
+ }
+ notifySourceInfoRefreshed(durationUs, isSeekable, isLive);
+ }
+
+ // Internal methods.
+
+ private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) {
+ timelineDurationUs = durationUs;
+ timelineIsSeekable = isSeekable;
+ timelineIsLive = isLive;
+ // TODO: Split up isDynamic into multiple fields to indicate which values may change. Then
+ // indicate that the duration may change until it's known. See [internal: b/69703223].
+ refreshSourceInfo(
+ new SinglePeriodTimeline(
+ timelineDurationUs,
+ timelineIsSeekable,
+ /* isDynamic= */ false,
+ /* isLive= */ timelineIsLive,
+ /* manifest= */ null,
+ tag));
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java
new file mode 100644
index 0000000000..81933a468d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java
@@ -0,0 +1,472 @@
+/*
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.source;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.CryptoInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput.CryptoData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocation;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/** A queue of media sample data. */
+/* package */ class SampleDataQueue {
+
+ private static final int INITIAL_SCRATCH_SIZE = 32;
+
+ private final Allocator allocator;
+ private final int allocationLength;
+ private final ParsableByteArray scratch;
+
+ // References into the linked list of allocations.
+ private AllocationNode firstAllocationNode;
+ private AllocationNode readAllocationNode;
+ private AllocationNode writeAllocationNode;
+
+ // Accessed only by the loading thread (or the consuming thread when there is no loading thread).
+ private long totalBytesWritten;
+
+ public SampleDataQueue(Allocator allocator) {
+ this.allocator = allocator;
+ allocationLength = allocator.getIndividualAllocationLength();
+ scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
+ firstAllocationNode = new AllocationNode(/* startPosition= */ 0, allocationLength);
+ readAllocationNode = firstAllocationNode;
+ writeAllocationNode = firstAllocationNode;
+ }
+
+ // Called by the consuming thread, but only when there is no loading thread.
+
+ /** Clears all sample data. */
+ public void reset() {
+ clearAllocationNodes(firstAllocationNode);
+ firstAllocationNode = new AllocationNode(0, allocationLength);
+ readAllocationNode = firstAllocationNode;
+ writeAllocationNode = firstAllocationNode;
+ totalBytesWritten = 0;
+ allocator.trim();
+ }
+
+ /**
+ * Discards sample data bytes from the write side of the queue.
+ *
+ * @param totalBytesWritten The reduced total number of bytes written after the samples have been
+ * discarded, or 0 if the queue is now empty.
+ */
+ public void discardUpstreamSampleBytes(long totalBytesWritten) {
+ this.totalBytesWritten = totalBytesWritten;
+ if (this.totalBytesWritten == 0
+ || this.totalBytesWritten == firstAllocationNode.startPosition) {
+ clearAllocationNodes(firstAllocationNode);
+ firstAllocationNode = new AllocationNode(this.totalBytesWritten, allocationLength);
+ readAllocationNode = firstAllocationNode;
+ writeAllocationNode = firstAllocationNode;
+ } else {
+ // Find the last node containing at least 1 byte of data that we need to keep.
+ AllocationNode lastNodeToKeep = firstAllocationNode;
+ while (this.totalBytesWritten > lastNodeToKeep.endPosition) {
+ lastNodeToKeep = lastNodeToKeep.next;
+ }
+ // Discard all subsequent nodes.
+ AllocationNode firstNodeToDiscard = lastNodeToKeep.next;
+ clearAllocationNodes(firstNodeToDiscard);
+ // Reset the successor of the last node to be an uninitialized node.
+ lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength);
+ // Update writeAllocationNode and readAllocationNode as necessary.
+ writeAllocationNode =
+ this.totalBytesWritten == lastNodeToKeep.endPosition
+ ? lastNodeToKeep.next
+ : lastNodeToKeep;
+ if (readAllocationNode == firstNodeToDiscard) {
+ readAllocationNode = lastNodeToKeep.next;
+ }
+ }
+ }
+
+ // Called by the consuming thread.
+
+ /** Rewinds the read position to the first sample in the queue. */
+ public void rewind() {
+ readAllocationNode = firstAllocationNode;
+ }
+
+ /**
+ * Reads data from the rolling buffer to populate a decoder input buffer.
+ *
+ * @param buffer The buffer to populate.
+ * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
+ */
+ public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
+ // Read encryption data if the sample is encrypted.
+ if (buffer.isEncrypted()) {
+ readEncryptionData(buffer, extrasHolder);
+ }
+ // Read sample data, extracting supplemental data into a separate buffer if needed.
+ if (buffer.hasSupplementalData()) {
+ // If there is supplemental data, the sample data is prefixed by its size.
+ scratch.reset(4);
+ readData(extrasHolder.offset, scratch.data, 4);
+ int sampleSize = scratch.readUnsignedIntToInt();
+ extrasHolder.offset += 4;
+ extrasHolder.size -= 4;
+
+ // Write the sample data.
+ buffer.ensureSpaceForWrite(sampleSize);
+ readData(extrasHolder.offset, buffer.data, sampleSize);
+ extrasHolder.offset += sampleSize;
+ extrasHolder.size -= sampleSize;
+
+ // Write the remaining data as supplemental data.
+ buffer.resetSupplementalData(extrasHolder.size);
+ readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size);
+ } else {
+ // Write the sample data.
+ buffer.ensureSpaceForWrite(extrasHolder.size);
+ readData(extrasHolder.offset, buffer.data, extrasHolder.size);
+ }
+ }
+
+ /**
+ * Advances the read position to the specified absolute position.
+ *
+ * @param absolutePosition The new absolute read position. May be {@link C#POSITION_UNSET}, in
+ * which case calling this method is a no-op.
+ */
+ public void discardDownstreamTo(long absolutePosition) {
+ if (absolutePosition == C.POSITION_UNSET) {
+ return;
+ }
+ while (absolutePosition >= firstAllocationNode.endPosition) {
+ // Advance firstAllocationNode to the specified absolute position. Also clear nodes that are
+ // advanced past, and return their underlying allocations to the allocator.
+ allocator.release(firstAllocationNode.allocation);
+ firstAllocationNode = firstAllocationNode.clear();
+ }
+ if (readAllocationNode.startPosition < firstAllocationNode.startPosition) {
+ // We discarded the node referenced by readAllocationNode. We need to advance it to the first
+ // remaining node.
+ readAllocationNode = firstAllocationNode;
+ }
+ }
+
+ // Called by the loading thread.
+
+ public long getTotalBytesWritten() {
+ return totalBytesWritten;
+ }
+
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ length = preAppend(length);
+ int bytesAppended =
+ input.read(
+ writeAllocationNode.allocation.data,
+ writeAllocationNode.translateOffset(totalBytesWritten),
+ length);
+ if (bytesAppended == C.RESULT_END_OF_INPUT) {
+ if (allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ postAppend(bytesAppended);
+ return bytesAppended;
+ }
+
+ public void sampleData(ParsableByteArray buffer, int length) {
+ while (length > 0) {
+ int bytesAppended = preAppend(length);
+ buffer.readBytes(
+ writeAllocationNode.allocation.data,
+ writeAllocationNode.translateOffset(totalBytesWritten),
+ bytesAppended);
+ length -= bytesAppended;
+ postAppend(bytesAppended);
+ }
+ }
+
+ // Private methods.
+
+ /**
+ * Reads encryption data for the current sample.
+ *
+ * <p>The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link
+ * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same
+ * value is added to {@link SampleExtrasHolder#offset}.
+ *
+ * @param buffer The buffer into which the encryption data should be written.
+ * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
+ */
+ private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
+ long offset = extrasHolder.offset;
+
+ // Read the signal byte.
+ scratch.reset(1);
+ readData(offset, scratch.data, 1);
+ offset++;
+ byte signalByte = scratch.data[0];
+ boolean subsampleEncryption = (signalByte & 0x80) != 0;
+ int ivSize = signalByte & 0x7F;
+
+ // Read the initialization vector.
+ CryptoInfo cryptoInfo = buffer.cryptoInfo;
+ if (cryptoInfo.iv == null) {
+ cryptoInfo.iv = new byte[16];
+ } else {
+ // Zero out cryptoInfo.iv so that if ivSize < 16, the remaining bytes are correctly set to 0.
+ Arrays.fill(cryptoInfo.iv, (byte) 0);
+ }
+ readData(offset, cryptoInfo.iv, ivSize);
+ offset += ivSize;
+
+ // Read the subsample count, if present.
+ int subsampleCount;
+ if (subsampleEncryption) {
+ scratch.reset(2);
+ readData(offset, scratch.data, 2);
+ offset += 2;
+ subsampleCount = scratch.readUnsignedShort();
+ } else {
+ subsampleCount = 1;
+ }
+
+ // Write the clear and encrypted subsample sizes.
+ @Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData;
+ if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
+ clearDataSizes = new int[subsampleCount];
+ }
+ @Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData;
+ if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
+ encryptedDataSizes = new int[subsampleCount];
+ }
+ if (subsampleEncryption) {
+ int subsampleDataLength = 6 * subsampleCount;
+ scratch.reset(subsampleDataLength);
+ readData(offset, scratch.data, subsampleDataLength);
+ offset += subsampleDataLength;
+ scratch.setPosition(0);
+ for (int i = 0; i < subsampleCount; i++) {
+ clearDataSizes[i] = scratch.readUnsignedShort();
+ encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
+ }
+ } else {
+ clearDataSizes[0] = 0;
+ encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
+ }
+
+ // Populate the cryptoInfo.
+ CryptoData cryptoData = extrasHolder.cryptoData;
+ cryptoInfo.set(
+ subsampleCount,
+ clearDataSizes,
+ encryptedDataSizes,
+ cryptoData.encryptionKey,
+ cryptoInfo.iv,
+ cryptoData.cryptoMode,
+ cryptoData.encryptedBlocks,
+ cryptoData.clearBlocks);
+
+ // Adjust the offset and size to take into account the bytes read.
+ int bytesRead = (int) (offset - extrasHolder.offset);
+ extrasHolder.offset += bytesRead;
+ extrasHolder.size -= bytesRead;
+ }
+
+ /**
+ * Reads data from the front of the rolling buffer.
+ *
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The buffer into which data should be written.
+ * @param length The number of bytes to read.
+ */
+ private void readData(long absolutePosition, ByteBuffer target, int length) {
+ advanceReadTo(absolutePosition);
+ int remaining = length;
+ while (remaining > 0) {
+ int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
+ Allocation allocation = readAllocationNode.allocation;
+ target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy);
+ remaining -= toCopy;
+ absolutePosition += toCopy;
+ if (absolutePosition == readAllocationNode.endPosition) {
+ readAllocationNode = readAllocationNode.next;
+ }
+ }
+ }
+
+ /**
+ * Reads data from the front of the rolling buffer.
+ *
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The array into which data should be written.
+ * @param length The number of bytes to read.
+ */
+ private void readData(long absolutePosition, byte[] target, int length) {
+ advanceReadTo(absolutePosition);
+ int remaining = length;
+ while (remaining > 0) {
+ int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
+ Allocation allocation = readAllocationNode.allocation;
+ System.arraycopy(
+ allocation.data,
+ readAllocationNode.translateOffset(absolutePosition),
+ target,
+ length - remaining,
+ toCopy);
+ remaining -= toCopy;
+ absolutePosition += toCopy;
+ if (absolutePosition == readAllocationNode.endPosition) {
+ readAllocationNode = readAllocationNode.next;
+ }
+ }
+ }
+
+ /**
+ * Advances the read position to the specified absolute position.
+ *
+ * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced.
+ */
+ private void advanceReadTo(long absolutePosition) {
+ while (absolutePosition >= readAllocationNode.endPosition) {
+ readAllocationNode = readAllocationNode.next;
+ }
+ }
+
+ /**
+ * Clears allocation nodes starting from {@code fromNode}.
+ *
+ * @param fromNode The node from which to clear.
+ */
+ private void clearAllocationNodes(AllocationNode fromNode) {
+ if (!fromNode.wasInitialized) {
+ return;
+ }
+ // Bulk release allocations for performance (it's significantly faster when using
+ // DefaultAllocator because the allocator's lock only needs to be acquired and released once)
+ // [Internal: See b/29542039].
+ int allocationCount =
+ (writeAllocationNode.wasInitialized ? 1 : 0)
+ + ((int) (writeAllocationNode.startPosition - fromNode.startPosition)
+ / allocationLength);
+ Allocation[] allocationsToRelease = new Allocation[allocationCount];
+ AllocationNode currentNode = fromNode;
+ for (int i = 0; i < allocationsToRelease.length; i++) {
+ allocationsToRelease[i] = currentNode.allocation;
+ currentNode = currentNode.clear();
+ }
+ allocator.release(allocationsToRelease);
+ }
+
+ /**
+ * Called before writing sample data to {@link #writeAllocationNode}. May cause {@link
+ * #writeAllocationNode} to be initialized.
+ *
+ * @param length The number of bytes that the caller wishes to write.
+ * @return The number of bytes that the caller is permitted to write, which may be less than
+ * {@code length}.
+ */
+ private int preAppend(int length) {
+ if (!writeAllocationNode.wasInitialized) {
+ writeAllocationNode.initialize(
+ allocator.allocate(),
+ new AllocationNode(writeAllocationNode.endPosition, allocationLength));
+ }
+ return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten));
+ }
+
+ /**
+ * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced.
+ *
+ * @param length The number of bytes that were written.
+ */
+ private void postAppend(int length) {
+ totalBytesWritten += length;
+ if (totalBytesWritten == writeAllocationNode.endPosition) {
+ writeAllocationNode = writeAllocationNode.next;
+ }
+ }
+
+ /** A node in a linked list of {@link Allocation}s held by the output. */
+ private static final class AllocationNode {
+
+ /** The absolute position of the start of the data (inclusive). */
+ public final long startPosition;
+ /** The absolute position of the end of the data (exclusive). */
+ public final long endPosition;
+ /** Whether the node has been initialized. Remains true after {@link #clear()}. */
+ public boolean wasInitialized;
+ /** The {@link Allocation}, or {@code null} if the node is not initialized. */
+ @Nullable public Allocation allocation;
+ /**
+ * The next {@link AllocationNode} in the list, or {@code null} if the node has not been
+ * initialized. Remains set after {@link #clear()}.
+ */
+ @Nullable public AllocationNode next;
+
+ /**
+ * @param startPosition See {@link #startPosition}.
+ * @param allocationLength The length of the {@link Allocation} with which this node will be
+ * initialized.
+ */
+ public AllocationNode(long startPosition, int allocationLength) {
+ this.startPosition = startPosition;
+ this.endPosition = startPosition + allocationLength;
+ }
+
+ /**
+ * Initializes the node.
+ *
+ * @param allocation The node's {@link Allocation}.
+ * @param next The next {@link AllocationNode}.
+ */
+ public void initialize(Allocation allocation, AllocationNode next) {
+ this.allocation = allocation;
+ this.next = next;
+ wasInitialized = true;
+ }
+
+ /**
+ * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to
+ * the specified absolute position.
+ *
+ * @param absolutePosition The absolute position.
+ * @return The corresponding offset into the allocation's data.
+ */
+ public int translateOffset(long absolutePosition) {
+ return (int) (absolutePosition - startPosition) + allocation.offset;
+ }
+
+ /**
+ * Clears {@link #allocation} and {@link #next}.
+ *
+ * @return The cleared next {@link AllocationNode}.
+ */
+ public AllocationNode clear() {
+ allocation = null;
+ AllocationNode temp = next;
+ next = null;
+ return temp;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java
new file mode 100644
index 0000000000..639cccee00
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java
@@ -0,0 +1,926 @@
+/*
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.source;
+
+import android.os.Looper;
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+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.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+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.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/** A queue of media samples. */
+public class SampleQueue implements TrackOutput {
+
+ /** A listener for changes to the upstream format. */
+ public interface UpstreamFormatChangedListener {
+
+ /**
+ * Called on the loading thread when an upstream format change occurs.
+ *
+ * @param format The new upstream format.
+ */
+ void onUpstreamFormatChanged(Format format);
+ }
+
+ @VisibleForTesting /* package */ static final int SAMPLE_CAPACITY_INCREMENT = 1000;
+
+ private final SampleDataQueue sampleDataQueue;
+ private final SampleExtrasHolder extrasHolder;
+ private final DrmSessionManager<?> drmSessionManager;
+ private UpstreamFormatChangedListener upstreamFormatChangeListener;
+
+ @Nullable private Format downstreamFormat;
+ @Nullable private DrmSession<?> currentDrmSession;
+
+ private int capacity;
+ private int[] sourceIds;
+ private long[] offsets;
+ private int[] sizes;
+ private int[] flags;
+ private long[] timesUs;
+ private CryptoData[] cryptoDatas;
+ private Format[] formats;
+
+ private int length;
+ private int absoluteFirstIndex;
+ private int relativeFirstIndex;
+ private int readPosition;
+
+ private long largestDiscardedTimestampUs;
+ private long largestQueuedTimestampUs;
+ private boolean isLastSampleQueued;
+ private boolean upstreamKeyframeRequired;
+ private boolean upstreamFormatRequired;
+ private Format upstreamFormat;
+ private Format upstreamCommittedFormat;
+ private int upstreamSourceId;
+
+ private boolean pendingUpstreamFormatAdjustment;
+ private Format unadjustedUpstreamFormat;
+ private long sampleOffsetUs;
+ private boolean pendingSplice;
+
+ /**
+ * Creates a sample queue.
+ *
+ * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
+ * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions}
+ * from. The created instance does not take ownership of this {@link DrmSessionManager}.
+ */
+ public SampleQueue(Allocator allocator, DrmSessionManager<?> drmSessionManager) {
+ sampleDataQueue = new SampleDataQueue(allocator);
+ this.drmSessionManager = drmSessionManager;
+ extrasHolder = new SampleExtrasHolder();
+ capacity = SAMPLE_CAPACITY_INCREMENT;
+ sourceIds = new int[capacity];
+ offsets = new long[capacity];
+ timesUs = new long[capacity];
+ flags = new int[capacity];
+ sizes = new int[capacity];
+ cryptoDatas = new CryptoData[capacity];
+ formats = new Format[capacity];
+ largestDiscardedTimestampUs = Long.MIN_VALUE;
+ largestQueuedTimestampUs = Long.MIN_VALUE;
+ upstreamFormatRequired = true;
+ upstreamKeyframeRequired = true;
+ }
+
+ // Called by the consuming thread when there is no loading thread.
+
+ /** Calls {@link #reset(boolean) reset(true)} and releases any resources owned by the queue. */
+ @CallSuper
+ public void release() {
+ reset(/* resetUpstreamFormat= */ true);
+ releaseDrmSessionReferences();
+ }
+
+ /** Convenience method for {@code reset(false)}. */
+ public final void reset() {
+ reset(/* resetUpstreamFormat= */ false);
+ }
+
+ /**
+ * Clears all samples from the queue.
+ *
+ * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false,
+ * samples queued after the reset (and before a subsequent call to {@link #format(Format)})
+ * are assumed to have the current upstream format. If set to true, {@link #format(Format)}
+ * must be called after the reset before any more samples can be queued.
+ */
+ @CallSuper
+ public void reset(boolean resetUpstreamFormat) {
+ sampleDataQueue.reset();
+ length = 0;
+ absoluteFirstIndex = 0;
+ relativeFirstIndex = 0;
+ readPosition = 0;
+ upstreamKeyframeRequired = true;
+ largestDiscardedTimestampUs = Long.MIN_VALUE;
+ largestQueuedTimestampUs = Long.MIN_VALUE;
+ isLastSampleQueued = false;
+ upstreamCommittedFormat = null;
+ if (resetUpstreamFormat) {
+ unadjustedUpstreamFormat = null;
+ upstreamFormat = null;
+ upstreamFormatRequired = true;
+ }
+ }
+
+ /**
+ * Sets a source identifier for subsequent samples.
+ *
+ * @param sourceId The source identifier.
+ */
+ public final void sourceId(int sourceId) {
+ upstreamSourceId = sourceId;
+ }
+
+ /** Indicates samples that are subsequently queued should be spliced into those already queued. */
+ public final void splice() {
+ pendingSplice = true;
+ }
+
+ /** Returns the current absolute write index. */
+ public final int getWriteIndex() {
+ return absoluteFirstIndex + length;
+ }
+
+ /**
+ * Discards samples from the write side of the queue.
+ *
+ * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the
+ * range [{@link #getReadIndex()}, {@link #getWriteIndex()}].
+ */
+ public final void discardUpstreamSamples(int discardFromIndex) {
+ sampleDataQueue.discardUpstreamSampleBytes(discardUpstreamSampleMetadata(discardFromIndex));
+ }
+
+ // Called by the consuming thread.
+
+ /** Calls {@link #discardToEnd()} and releases any resources owned by the queue. */
+ @CallSuper
+ public void preRelease() {
+ discardToEnd();
+ releaseDrmSessionReferences();
+ }
+
+ /**
+ * Throws an error that's preventing data from being read. Does nothing if no such error exists.
+ *
+ * @throws IOException The underlying error.
+ */
+ @CallSuper
+ public void maybeThrowError() throws IOException {
+ // TODO: Avoid throwing if the DRM error is not preventing a read operation.
+ if (currentDrmSession != null && currentDrmSession.getState() == DrmSession.STATE_ERROR) {
+ throw Assertions.checkNotNull(currentDrmSession.getError());
+ }
+ }
+
+ /** Returns the current absolute start index. */
+ public final int getFirstIndex() {
+ return absoluteFirstIndex;
+ }
+
+ /** Returns the current absolute read index. */
+ public final int getReadIndex() {
+ return absoluteFirstIndex + readPosition;
+ }
+
+ /**
+ * Peeks the source id of the next sample to be read, or the current upstream source id if the
+ * queue is empty or if the read position is at the end of the queue.
+ *
+ * @return The source id.
+ */
+ public final synchronized int peekSourceId() {
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId;
+ }
+
+ /** Returns the upstream {@link Format} in which samples are being queued. */
+ public final synchronized Format getUpstreamFormat() {
+ return upstreamFormatRequired ? null : upstreamFormat;
+ }
+
+ /**
+ * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
+ *
+ * <p>Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
+ * considered as having been queued. Samples that were dequeued from the front of the queue are
+ * considered as having been queued.
+ *
+ * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
+ * samples have been queued.
+ */
+ public final synchronized long getLargestQueuedTimestampUs() {
+ return largestQueuedTimestampUs;
+ }
+
+ /**
+ * Returns whether the last sample of the stream has knowingly been queued. A return value of
+ * {@code false} means that the last sample had not been queued or that it's unknown whether the
+ * last sample has been queued.
+ *
+ * <p>Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
+ * considered as having been queued. Samples that were dequeued from the front of the queue are
+ * considered as having been queued.
+ */
+ public final synchronized boolean isLastSampleQueued() {
+ return isLastSampleQueued;
+ }
+
+ /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
+ public final synchronized long getFirstTimestampUs() {
+ return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex];
+ }
+
+ /**
+ * Returns whether there is data available for reading.
+ *
+ * <p>Note: If the stream has ended then a buffer with the end of stream flag can always be read
+ * from {@link #read}. Hence an ended stream is always ready.
+ *
+ * @param loadingFinished Whether no more samples will be written to the sample queue. When true,
+ * this method returns true if the sample queue is empty, because an empty sample queue means
+ * the end of stream has been reached. When false, this method returns false if the sample
+ * queue is empty.
+ */
+ @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat
+ @CallSuper
+ public synchronized boolean isReady(boolean loadingFinished) {
+ if (!hasNextSample()) {
+ return loadingFinished
+ || isLastSampleQueued
+ || (upstreamFormat != null && upstreamFormat != downstreamFormat);
+ }
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ if (formats[relativeReadIndex] != downstreamFormat) {
+ // A format can be read.
+ return true;
+ }
+ return mayReadSample(relativeReadIndex);
+ }
+
+ /**
+ * Attempts to read from the queue.
+ *
+ * <p>{@link Format Formats} read from this method may be associated to a {@link DrmSession}
+ * through {@link FormatHolder#drmSession}, which is populated in two scenarios:
+ *
+ * <ul>
+ * <li>The {@link Format} has a non-null {@link Format#drmInitData}.
+ * <li>The {@link DrmSessionManager} provides placeholder sessions for this queue's track type.
+ * See {@link DrmSessionManager#acquirePlaceholderSession(Looper, int)}.
+ * </ul>
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If the end of the stream has been reached, the {@link
+ * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. If a {@link
+ * DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, only the buffer flags may be
+ * populated by this method and the read position of the queue will not change.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even if
+ * it's not changing. A sample will never be read if set to true, however it is still possible
+ * for the end of stream or nothing to be read.
+ * @param loadingFinished True if an empty queue should be considered the end of the stream.
+ * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
+ * be set if the buffer's timestamp is less than this value.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+ * {@link C#RESULT_BUFFER_READ}.
+ */
+ @CallSuper
+ public int read(
+ FormatHolder formatHolder,
+ DecoderInputBuffer buffer,
+ boolean formatRequired,
+ boolean loadingFinished,
+ long decodeOnlyUntilUs) {
+ int result =
+ readSampleMetadata(
+ formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder);
+ if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) {
+ sampleDataQueue.readToBuffer(buffer, extrasHolder);
+ }
+ return result;
+ }
+
+ /**
+ * Attempts to seek the read position to the specified sample index.
+ *
+ * @param sampleIndex The sample index.
+ * @return Whether the seek was successful.
+ */
+ public final synchronized boolean seekTo(int sampleIndex) {
+ rewind();
+ if (sampleIndex < absoluteFirstIndex || sampleIndex > absoluteFirstIndex + length) {
+ return false;
+ }
+ readPosition = sampleIndex - absoluteFirstIndex;
+ return true;
+ }
+
+ /**
+ * Attempts to seek the read position to the keyframe before or at the specified time.
+ *
+ * @param timeUs The time to seek to.
+ * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
+ * end of the queue, by seeking to the last sample (or keyframe).
+ * @return Whether the seek was successful.
+ */
+ public final synchronized boolean seekTo(long timeUs, boolean allowTimeBeyondBuffer) {
+ rewind();
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ if (!hasNextSample()
+ || timeUs < timesUs[relativeReadIndex]
+ || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) {
+ return false;
+ }
+ int offset =
+ findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true);
+ if (offset == -1) {
+ return false;
+ }
+ readPosition += offset;
+ return true;
+ }
+
+ /**
+ * Advances the read position to the keyframe before or at the specified time.
+ *
+ * @param timeUs The time to advance to.
+ * @return The number of samples that were skipped, which may be equal to 0.
+ */
+ public final synchronized int advanceTo(long timeUs) {
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]) {
+ return 0;
+ }
+ int offset =
+ findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true);
+ if (offset == -1) {
+ return 0;
+ }
+ readPosition += offset;
+ return offset;
+ }
+
+ /**
+ * Advances the read position to the end of the queue.
+ *
+ * @return The number of samples that were skipped.
+ */
+ public final synchronized int advanceToEnd() {
+ int skipCount = length - readPosition;
+ readPosition = length;
+ return skipCount;
+ }
+
+ /**
+ * Discards up to but not including the sample immediately before or at the specified time.
+ *
+ * @param timeUs The time to discard up to.
+ * @param toKeyframe If true then discards samples up to the keyframe before or at the specified
+ * time, rather than any sample before or at that time.
+ * @param stopAtReadPosition If true then samples are only discarded if they're before the read
+ * position. If false then samples at and beyond the read position may be discarded, in which
+ * case the read position is advanced to the first remaining sample.
+ */
+ public final void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
+ sampleDataQueue.discardDownstreamTo(
+ discardSampleMetadataTo(timeUs, toKeyframe, stopAtReadPosition));
+ }
+
+ /** Discards up to but not including the read position. */
+ public final void discardToRead() {
+ sampleDataQueue.discardDownstreamTo(discardSampleMetadataToRead());
+ }
+
+ /** Discards all samples in the queue and advances the read position. */
+ public final void discardToEnd() {
+ sampleDataQueue.discardDownstreamTo(discardSampleMetadataToEnd());
+ }
+
+ // Called by the loading thread.
+
+ /**
+ * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that
+ * are subsequently queued.
+ *
+ * @param sampleOffsetUs The timestamp offset in microseconds.
+ */
+ public final void setSampleOffsetUs(long sampleOffsetUs) {
+ if (this.sampleOffsetUs != sampleOffsetUs) {
+ this.sampleOffsetUs = sampleOffsetUs;
+ invalidateUpstreamFormatAdjustment();
+ }
+ }
+
+ /**
+ * Sets a listener to be notified of changes to the upstream format.
+ *
+ * @param listener The listener.
+ */
+ public final void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) {
+ upstreamFormatChangeListener = listener;
+ }
+
+ // TrackOutput implementation. Called by the loading thread.
+
+ @Override
+ public final void format(Format unadjustedUpstreamFormat) {
+ Format adjustedUpstreamFormat = getAdjustedUpstreamFormat(unadjustedUpstreamFormat);
+ pendingUpstreamFormatAdjustment = false;
+ this.unadjustedUpstreamFormat = unadjustedUpstreamFormat;
+ boolean upstreamFormatChanged = setUpstreamFormat(adjustedUpstreamFormat);
+ if (upstreamFormatChangeListener != null && upstreamFormatChanged) {
+ upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedUpstreamFormat);
+ }
+ }
+
+ @Override
+ public final int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ return sampleDataQueue.sampleData(input, length, allowEndOfInput);
+ }
+
+ @Override
+ public final void sampleData(ParsableByteArray buffer, int length) {
+ sampleDataQueue.sampleData(buffer, length);
+ }
+
+ @Override
+ public final void sampleMetadata(
+ long timeUs,
+ @C.BufferFlags int flags,
+ int size,
+ int offset,
+ @Nullable CryptoData cryptoData) {
+ if (pendingUpstreamFormatAdjustment) {
+ format(unadjustedUpstreamFormat);
+ }
+ timeUs += sampleOffsetUs;
+ if (pendingSplice) {
+ if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !attemptSplice(timeUs)) {
+ return;
+ }
+ pendingSplice = false;
+ }
+ long absoluteOffset = sampleDataQueue.getTotalBytesWritten() - size - offset;
+ commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
+ }
+
+ /**
+ * Invalidates the last upstream format adjustment. {@link #getAdjustedUpstreamFormat(Format)}
+ * will be called to adjust the upstream {@link Format} again before the next sample is queued.
+ */
+ protected final void invalidateUpstreamFormatAdjustment() {
+ pendingUpstreamFormatAdjustment = true;
+ }
+
+ /**
+ * Adjusts the upstream {@link Format} (i.e., the {@link Format} that was most recently passed to
+ * {@link #format(Format)}).
+ *
+ * <p>The default implementation incorporates the sample offset passed to {@link
+ * #setSampleOffsetUs(long)} into {@link Format#subsampleOffsetUs}.
+ *
+ * @param format The {@link Format} to adjust.
+ * @return The adjusted {@link Format}.
+ */
+ @CallSuper
+ protected Format getAdjustedUpstreamFormat(Format format) {
+ if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
+ format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
+ }
+ return format;
+ }
+
+ // Internal methods.
+
+ /** Rewinds the read position to the first sample in the queue. */
+ private synchronized void rewind() {
+ readPosition = 0;
+ sampleDataQueue.rewind();
+ }
+
+ @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat
+ private synchronized int readSampleMetadata(
+ FormatHolder formatHolder,
+ DecoderInputBuffer buffer,
+ boolean formatRequired,
+ boolean loadingFinished,
+ long decodeOnlyUntilUs,
+ SampleExtrasHolder extrasHolder) {
+ buffer.waitingForKeys = false;
+ // This is a temporary fix for https://github.com/google/ExoPlayer/issues/6155.
+ // TODO: Remove it and replace it with a fix that discards samples when writing to the queue.
+ boolean hasNextSample;
+ int relativeReadIndex = C.INDEX_UNSET;
+ while ((hasNextSample = hasNextSample())) {
+ relativeReadIndex = getRelativeIndex(readPosition);
+ long timeUs = timesUs[relativeReadIndex];
+ if (timeUs < decodeOnlyUntilUs
+ && MimeTypes.allSamplesAreSyncSamples(formats[relativeReadIndex].sampleMimeType)) {
+ readPosition++;
+ } else {
+ break;
+ }
+ }
+
+ if (!hasNextSample) {
+ if (loadingFinished || isLastSampleQueued) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) {
+ onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder);
+ return C.RESULT_FORMAT_READ;
+ } else {
+ return C.RESULT_NOTHING_READ;
+ }
+ }
+
+ if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {
+ onFormatResult(formats[relativeReadIndex], formatHolder);
+ return C.RESULT_FORMAT_READ;
+ }
+
+ if (!mayReadSample(relativeReadIndex)) {
+ buffer.waitingForKeys = true;
+ return C.RESULT_NOTHING_READ;
+ }
+
+ buffer.setFlags(flags[relativeReadIndex]);
+ buffer.timeUs = timesUs[relativeReadIndex];
+ if (buffer.timeUs < decodeOnlyUntilUs) {
+ buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ }
+ if (buffer.isFlagsOnly()) {
+ return C.RESULT_BUFFER_READ;
+ }
+ extrasHolder.size = sizes[relativeReadIndex];
+ extrasHolder.offset = offsets[relativeReadIndex];
+ extrasHolder.cryptoData = cryptoDatas[relativeReadIndex];
+
+ readPosition++;
+ return C.RESULT_BUFFER_READ;
+ }
+
+ private synchronized boolean setUpstreamFormat(Format format) {
+ if (format == null) {
+ upstreamFormatRequired = true;
+ return false;
+ }
+ upstreamFormatRequired = false;
+ if (Util.areEqual(format, upstreamFormat)) {
+ // The format is unchanged. If format and upstreamFormat are different objects, we keep the
+ // current upstreamFormat so we can detect format changes on the read side using cheap
+ // referential quality.
+ return false;
+ } else if (Util.areEqual(format, upstreamCommittedFormat)) {
+ // The format has changed back to the format of the last committed sample. If they are
+ // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat
+ // so we can detect format changes on the read side using cheap referential equality.
+ upstreamFormat = upstreamCommittedFormat;
+ return true;
+ } else {
+ upstreamFormat = format;
+ return true;
+ }
+ }
+
+ private synchronized long discardSampleMetadataTo(
+ long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
+ if (length == 0 || timeUs < timesUs[relativeFirstIndex]) {
+ return C.POSITION_UNSET;
+ }
+ int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length;
+ int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe);
+ if (discardCount == -1) {
+ return C.POSITION_UNSET;
+ }
+ return discardSamples(discardCount);
+ }
+
+ public synchronized long discardSampleMetadataToRead() {
+ if (readPosition == 0) {
+ return C.POSITION_UNSET;
+ }
+ return discardSamples(readPosition);
+ }
+
+ private synchronized long discardSampleMetadataToEnd() {
+ if (length == 0) {
+ return C.POSITION_UNSET;
+ }
+ return discardSamples(length);
+ }
+
+ private void releaseDrmSessionReferences() {
+ if (currentDrmSession != null) {
+ currentDrmSession.release();
+ currentDrmSession = null;
+ // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData
+ // != null implies currentSession != null
+ downstreamFormat = null;
+ }
+ }
+
+ private synchronized void commitSample(
+ long timeUs, @C.BufferFlags int sampleFlags, long offset, int size, CryptoData cryptoData) {
+ if (upstreamKeyframeRequired) {
+ if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
+ return;
+ }
+ upstreamKeyframeRequired = false;
+ }
+ Assertions.checkState(!upstreamFormatRequired);
+
+ isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0;
+ largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
+
+ int relativeEndIndex = getRelativeIndex(length);
+ timesUs[relativeEndIndex] = timeUs;
+ offsets[relativeEndIndex] = offset;
+ sizes[relativeEndIndex] = size;
+ flags[relativeEndIndex] = sampleFlags;
+ cryptoDatas[relativeEndIndex] = cryptoData;
+ formats[relativeEndIndex] = upstreamFormat;
+ sourceIds[relativeEndIndex] = upstreamSourceId;
+ upstreamCommittedFormat = upstreamFormat;
+
+ length++;
+ if (length == capacity) {
+ // Increase the capacity.
+ int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
+ int[] newSourceIds = new int[newCapacity];
+ long[] newOffsets = new long[newCapacity];
+ long[] newTimesUs = new long[newCapacity];
+ int[] newFlags = new int[newCapacity];
+ int[] newSizes = new int[newCapacity];
+ CryptoData[] newCryptoDatas = new CryptoData[newCapacity];
+ Format[] newFormats = new Format[newCapacity];
+ int beforeWrap = capacity - relativeFirstIndex;
+ System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap);
+ System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap);
+ System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap);
+ System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap);
+ System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap);
+ System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap);
+ System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap);
+ int afterWrap = relativeFirstIndex;
+ System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
+ System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
+ System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
+ System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
+ System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap);
+ System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap);
+ System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
+ offsets = newOffsets;
+ timesUs = newTimesUs;
+ flags = newFlags;
+ sizes = newSizes;
+ cryptoDatas = newCryptoDatas;
+ formats = newFormats;
+ sourceIds = newSourceIds;
+ relativeFirstIndex = 0;
+ capacity = newCapacity;
+ }
+ }
+
+ /**
+ * Attempts to discard samples from the end of the queue to allow samples starting from the
+ * specified timestamp to be spliced in. Samples will not be discarded prior to the read position.
+ *
+ * @param timeUs The timestamp at which the splice occurs.
+ * @return Whether the splice was successful.
+ */
+ private synchronized boolean attemptSplice(long timeUs) {
+ if (length == 0) {
+ return timeUs > largestDiscardedTimestampUs;
+ }
+ long largestReadTimestampUs =
+ Math.max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition));
+ if (largestReadTimestampUs >= timeUs) {
+ return false;
+ }
+ int retainCount = length;
+ int relativeSampleIndex = getRelativeIndex(length - 1);
+ while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) {
+ retainCount--;
+ relativeSampleIndex--;
+ if (relativeSampleIndex == -1) {
+ relativeSampleIndex = capacity - 1;
+ }
+ }
+ discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount);
+ return true;
+ }
+
+ private long discardUpstreamSampleMetadata(int discardFromIndex) {
+ int discardCount = getWriteIndex() - discardFromIndex;
+ Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
+ length -= discardCount;
+ largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));
+ isLastSampleQueued = discardCount == 0 && isLastSampleQueued;
+ if (length != 0) {
+ int relativeLastWriteIndex = getRelativeIndex(length - 1);
+ return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex];
+ }
+ return 0;
+ }
+
+ private boolean hasNextSample() {
+ return readPosition != length;
+ }
+
+ /**
+ * Sets the downstream format, performs DRM resource management, and populates the {@code
+ * outputFormatHolder}.
+ *
+ * @param newFormat The new downstream format.
+ * @param outputFormatHolder The output {@link FormatHolder}.
+ */
+ private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) {
+ outputFormatHolder.format = newFormat;
+ boolean isFirstFormat = downstreamFormat == null;
+ DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData;
+ downstreamFormat = newFormat;
+ if (drmSessionManager == DrmSessionManager.DUMMY) {
+ // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that
+ // the media source creation has not yet been migrated and the renderer can acquire the
+ // session for the read DRM init data.
+ // TODO: Remove once renderers are migrated [Internal ref: b/122519809].
+ return;
+ }
+ DrmInitData newDrmInitData = newFormat.drmInitData;
+ outputFormatHolder.includesDrmSession = true;
+ outputFormatHolder.drmSession = currentDrmSession;
+ if (!isFirstFormat && Util.areEqual(oldDrmInitData, newDrmInitData)) {
+ // Nothing to do.
+ return;
+ }
+ // Ensure we acquire the new session before releasing the previous one in case the same session
+ // is being used for both DrmInitData.
+ DrmSession<?> previousSession = currentDrmSession;
+ Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper());
+ currentDrmSession =
+ newDrmInitData != null
+ ? drmSessionManager.acquireSession(playbackLooper, newDrmInitData)
+ : drmSessionManager.acquirePlaceholderSession(
+ playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType));
+ outputFormatHolder.drmSession = currentDrmSession;
+
+ if (previousSession != null) {
+ previousSession.release();
+ }
+ }
+
+ /**
+ * Returns whether it's possible to read the next sample.
+ *
+ * @param relativeReadIndex The relative read index of the next sample.
+ * @return Whether it's possible to read the next sample.
+ */
+ private boolean mayReadSample(int relativeReadIndex) {
+ if (drmSessionManager == DrmSessionManager.DUMMY) {
+ // TODO: Remove once renderers are migrated [Internal ref: b/122519809].
+ // For protected content it's likely that the DrmSessionManager is still being injected into
+ // the renderers. We assume that the renderers will be able to acquire a DrmSession if needed.
+ return true;
+ }
+ return currentDrmSession == null
+ || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS
+ || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0
+ && currentDrmSession.playClearSamplesWithoutKeys());
+ }
+
+ /**
+ * Finds the sample in the specified range that's before or at the specified time. If {@code
+ * keyframe} is {@code true} then the sample is additionally required to be a keyframe.
+ *
+ * @param relativeStartIndex The relative index from which to start searching.
+ * @param length The length of the range being searched.
+ * @param timeUs The specified time.
+ * @param keyframe Whether only keyframes should be considered.
+ * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching
+ * sample was found.
+ */
+ private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) {
+ // This could be optimized to use a binary search, however in practice callers to this method
+ // normally pass times near to the start of the search region. Hence it's unclear whether
+ // switching to a binary search would yield any real benefit.
+ int sampleCountToTarget = -1;
+ int searchIndex = relativeStartIndex;
+ for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) {
+ if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ // We've found a suitable sample.
+ sampleCountToTarget = i;
+ }
+ searchIndex++;
+ if (searchIndex == capacity) {
+ searchIndex = 0;
+ }
+ }
+ return sampleCountToTarget;
+ }
+
+ /**
+ * Discards the specified number of samples.
+ *
+ * @param discardCount The number of samples to discard.
+ * @return The corresponding offset up to which data should be discarded.
+ */
+ private long discardSamples(int discardCount) {
+ largestDiscardedTimestampUs =
+ Math.max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount));
+ length -= discardCount;
+ absoluteFirstIndex += discardCount;
+ relativeFirstIndex += discardCount;
+ if (relativeFirstIndex >= capacity) {
+ relativeFirstIndex -= capacity;
+ }
+ readPosition -= discardCount;
+ if (readPosition < 0) {
+ readPosition = 0;
+ }
+ if (length == 0) {
+ int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1;
+ return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex];
+ } else {
+ return offsets[relativeFirstIndex];
+ }
+ }
+
+ /**
+ * Finds the largest timestamp of any sample from the start of the queue up to the specified
+ * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of
+ * the keyframe itself, and of subsequent frames.
+ *
+ * @param length The length of the range being searched.
+ * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}.
+ */
+ private long getLargestTimestamp(int length) {
+ if (length == 0) {
+ return Long.MIN_VALUE;
+ }
+ long largestTimestampUs = Long.MIN_VALUE;
+ int relativeSampleIndex = getRelativeIndex(length - 1);
+ for (int i = 0; i < length; i++) {
+ largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]);
+ if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ break;
+ }
+ relativeSampleIndex--;
+ if (relativeSampleIndex == -1) {
+ relativeSampleIndex = capacity - 1;
+ }
+ }
+ return largestTimestampUs;
+ }
+
+ /**
+ * Returns the relative index for a given offset from the start of the queue.
+ *
+ * @param offset The offset, which must be in the range [0, length].
+ */
+ private int getRelativeIndex(int offset) {
+ int relativeIndex = relativeFirstIndex + offset;
+ return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;
+ }
+
+ /** A holder for sample metadata not held by {@link DecoderInputBuffer}. */
+ /* package */ static final class SampleExtrasHolder {
+
+ public int size;
+ public long offset;
+ public CryptoData cryptoData;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java
new file mode 100644
index 0000000000..54a7d0f895
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import java.io.IOException;
+
+/**
+ * A stream of media samples (and associated format information).
+ */
+public interface SampleStream {
+
+ /**
+ * Returns whether data is available to be read.
+ * <p>
+ * Note: If the stream has ended then a buffer with the end of stream flag can always be read from
+ * {@link #readData(FormatHolder, DecoderInputBuffer, boolean)}. Hence an ended stream is always
+ * ready.
+ *
+ * @return Whether data is available to be read.
+ */
+ boolean isReady();
+
+ /**
+ * Throws an error that's preventing data from being read. Does nothing if no such error exists.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Attempts to read from the stream.
+ *
+ * <p>If the stream has ended then {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set on {@code
+ * buffer} and {@link C#RESULT_BUFFER_READ} is returned. Else if no data is available then {@link
+ * C#RESULT_NOTHING_READ} is returned. Else if the format of the media is changing or if {@code
+ * formatRequired} is set then {@code formatHolder} is populated and {@link C#RESULT_FORMAT_READ}
+ * is returned. Else {@code buffer} is populated and {@link C#RESULT_BUFFER_READ} is returned.
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If the end of the stream has been reached, the {@link
+ * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. If a {@link
+ * DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, then no {@link
+ * DecoderInputBuffer#data} will be read and the read position of the stream will not change,
+ * but the flags of the buffer will be populated.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even if
+ * it's not changing. A sample will never be read if set to true, however it is still possible
+ * for the end of stream or nothing to be read.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+ * {@link C#RESULT_BUFFER_READ}.
+ */
+ int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired);
+
+ /**
+ * Attempts to skip to the keyframe before the specified position, or to the end of the stream if
+ * {@code positionUs} is beyond it.
+ *
+ * @param positionUs The specified time.
+ * @return The number of samples that were skipped.
+ */
+ int skipData(long positionUs);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java
new file mode 100644
index 0000000000..09cb8b663b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+
+// TODO: Clarify the requirements for implementing this interface [Internal ref: b/36250203].
+/**
+ * A loader that can proceed in approximate synchronization with other loaders.
+ */
+public interface SequenceableLoader {
+
+ /**
+ * A callback to be notified of {@link SequenceableLoader} events.
+ */
+ interface Callback<T extends SequenceableLoader> {
+
+ /**
+ * Called by the loader to indicate that it wishes for its {@link #continueLoading(long)} method
+ * to be called when it can continue to load data. Called on the playback thread.
+ */
+ void onContinueLoadingRequested(T source);
+
+ }
+
+ /**
+ * Returns an estimate of the position up to which data is buffered.
+ *
+ * @return An estimate of the absolute position in microseconds up to which data is buffered, or
+ * {@link C#TIME_END_OF_SOURCE} if the data is fully buffered.
+ */
+ long getBufferedPositionUs();
+
+ /**
+ * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
+ */
+ long getNextLoadPositionUs();
+
+ /**
+ * Attempts to continue loading.
+ *
+ * @param positionUs The current playback position in microseconds. If playback of the period to
+ * which this loader belongs has not yet started, the value will be the starting position
+ * in the period minus the duration of any media in previous periods still to be played.
+ * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return
+ * a different value than prior to the call. False otherwise.
+ */
+ boolean continueLoading(long positionUs);
+
+ /** Returns whether the loader is currently loading. */
+ boolean isLoading();
+
+ /**
+ * Re-evaluates the buffer given the playback position.
+ *
+ * <p>Re-evaluation may discard buffered media so that it can be re-buffered in a different
+ * quality.
+ *
+ * @param positionUs The current playback position in microseconds. If playback of this period has
+ * not yet started, the value will be the starting position in this period minus the duration
+ * of any media in previous periods still to be played.
+ */
+ void reevaluateBuffer(long positionUs);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java
new file mode 100644
index 0000000000..f137054145
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2017 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Shuffled order of indices.
+ *
+ * <p>The shuffle order must be immutable to ensure thread safety.
+ */
+public interface ShuffleOrder {
+
+ /**
+ * The default {@link ShuffleOrder} implementation for random shuffle order.
+ */
+ class DefaultShuffleOrder implements ShuffleOrder {
+
+ private final Random random;
+ private final int[] shuffled;
+ private final int[] indexInShuffled;
+
+ /**
+ * Creates an instance with a specified length.
+ *
+ * @param length The length of the shuffle order.
+ */
+ public DefaultShuffleOrder(int length) {
+ this(length, new Random());
+ }
+
+ /**
+ * Creates an instance with a specified length and the specified random seed. Shuffle orders of
+ * the same length initialized with the same random seed are guaranteed to be equal.
+ *
+ * @param length The length of the shuffle order.
+ * @param randomSeed A random seed.
+ */
+ public DefaultShuffleOrder(int length, long randomSeed) {
+ this(length, new Random(randomSeed));
+ }
+
+ /**
+ * Creates an instance with a specified shuffle order and the specified random seed. The random
+ * seed is used for {@link #cloneAndInsert(int, int)} invocations.
+ *
+ * @param shuffledIndices The shuffled indices to use as order.
+ * @param randomSeed A random seed.
+ */
+ public DefaultShuffleOrder(int[] shuffledIndices, long randomSeed) {
+ this(Arrays.copyOf(shuffledIndices, shuffledIndices.length), new Random(randomSeed));
+ }
+
+ private DefaultShuffleOrder(int length, Random random) {
+ this(createShuffledList(length, random), random);
+ }
+
+ private DefaultShuffleOrder(int[] shuffled, Random random) {
+ this.shuffled = shuffled;
+ this.random = random;
+ this.indexInShuffled = new int[shuffled.length];
+ for (int i = 0; i < shuffled.length; i++) {
+ indexInShuffled[shuffled[i]] = i;
+ }
+ }
+
+ @Override
+ public int getLength() {
+ return shuffled.length;
+ }
+
+ @Override
+ public int getNextIndex(int index) {
+ int shuffledIndex = indexInShuffled[index];
+ return ++shuffledIndex < shuffled.length ? shuffled[shuffledIndex] : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getPreviousIndex(int index) {
+ int shuffledIndex = indexInShuffled[index];
+ return --shuffledIndex >= 0 ? shuffled[shuffledIndex] : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getLastIndex() {
+ return shuffled.length > 0 ? shuffled[shuffled.length - 1] : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getFirstIndex() {
+ return shuffled.length > 0 ? shuffled[0] : C.INDEX_UNSET;
+ }
+
+ @Override
+ public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {
+ int[] insertionPoints = new int[insertionCount];
+ int[] insertionValues = new int[insertionCount];
+ for (int i = 0; i < insertionCount; i++) {
+ insertionPoints[i] = random.nextInt(shuffled.length + 1);
+ int swapIndex = random.nextInt(i + 1);
+ insertionValues[i] = insertionValues[swapIndex];
+ insertionValues[swapIndex] = i + insertionIndex;
+ }
+ Arrays.sort(insertionPoints);
+ int[] newShuffled = new int[shuffled.length + insertionCount];
+ int indexInOldShuffled = 0;
+ int indexInInsertionList = 0;
+ for (int i = 0; i < shuffled.length + insertionCount; i++) {
+ if (indexInInsertionList < insertionCount
+ && indexInOldShuffled == insertionPoints[indexInInsertionList]) {
+ newShuffled[i] = insertionValues[indexInInsertionList++];
+ } else {
+ newShuffled[i] = shuffled[indexInOldShuffled++];
+ if (newShuffled[i] >= insertionIndex) {
+ newShuffled[i] += insertionCount;
+ }
+ }
+ }
+ return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
+ }
+
+ @Override
+ public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {
+ int numberOfElementsToRemove = indexToExclusive - indexFrom;
+ int[] newShuffled = new int[shuffled.length - numberOfElementsToRemove];
+ int foundElementsCount = 0;
+ for (int i = 0; i < shuffled.length; i++) {
+ if (shuffled[i] >= indexFrom && shuffled[i] < indexToExclusive) {
+ foundElementsCount++;
+ } else {
+ newShuffled[i - foundElementsCount] =
+ shuffled[i] >= indexFrom ? shuffled[i] - numberOfElementsToRemove : shuffled[i];
+ }
+ }
+ return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
+ }
+
+ @Override
+ public ShuffleOrder cloneAndClear() {
+ return new DefaultShuffleOrder(/* length= */ 0, new Random(random.nextLong()));
+ }
+
+ private static int[] createShuffledList(int length, Random random) {
+ int[] shuffled = new int[length];
+ for (int i = 0; i < length; i++) {
+ int swapIndex = random.nextInt(i + 1);
+ shuffled[i] = shuffled[swapIndex];
+ shuffled[swapIndex] = i;
+ }
+ return shuffled;
+ }
+
+ }
+
+ /**
+ * A {@link ShuffleOrder} implementation which does not shuffle.
+ */
+ final class UnshuffledShuffleOrder implements ShuffleOrder {
+
+ private final int length;
+
+ /**
+ * Creates an instance with a specified length.
+ *
+ * @param length The length of the shuffle order.
+ */
+ public UnshuffledShuffleOrder(int length) {
+ this.length = length;
+ }
+
+ @Override
+ public int getLength() {
+ return length;
+ }
+
+ @Override
+ public int getNextIndex(int index) {
+ return ++index < length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getPreviousIndex(int index) {
+ return --index >= 0 ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getLastIndex() {
+ return length > 0 ? length - 1 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getFirstIndex() {
+ return length > 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {
+ return new UnshuffledShuffleOrder(length + insertionCount);
+ }
+
+ @Override
+ public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {
+ return new UnshuffledShuffleOrder(length - indexToExclusive + indexFrom);
+ }
+
+ @Override
+ public ShuffleOrder cloneAndClear() {
+ return new UnshuffledShuffleOrder(/* length= */ 0);
+ }
+ }
+
+ /**
+ * Returns length of shuffle order.
+ */
+ int getLength();
+
+ /**
+ * Returns the next index in the shuffle order.
+ *
+ * @param index An index.
+ * @return The index after {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the last
+ * element.
+ */
+ int getNextIndex(int index);
+
+ /**
+ * Returns the previous index in the shuffle order.
+ *
+ * @param index An index.
+ * @return The index before {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the first
+ * element.
+ */
+ int getPreviousIndex(int index);
+
+ /**
+ * Returns the last index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is
+ * empty.
+ */
+ int getLastIndex();
+
+ /**
+ * Returns the first index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is
+ * empty.
+ */
+ int getFirstIndex();
+
+ /**
+ * Returns a copy of the shuffle order with newly inserted elements.
+ *
+ * @param insertionIndex The index in the unshuffled order at which elements are inserted.
+ * @param insertionCount The number of elements inserted at {@code insertionIndex}.
+ * @return A copy of this {@link ShuffleOrder} with newly inserted elements.
+ */
+ ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount);
+
+ /**
+ * Returns a copy of the shuffle order with a range of elements removed.
+ *
+ * @param indexFrom The starting index in the unshuffled order of the range to remove.
+ * @param indexToExclusive The smallest index (must be greater or equal to {@code indexFrom}) that
+ * will not be removed.
+ * @return A copy of this {@link ShuffleOrder} without the elements in the removed range.
+ */
+ ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive);
+
+ /** Returns a copy of the shuffle order with all elements removed. */
+ ShuffleOrder cloneAndClear();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java
new file mode 100644
index 0000000000..096cc66622
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java
@@ -0,0 +1,253 @@
+/*
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.source;
+
+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.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+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.Util;
+import java.util.ArrayList;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/** Media source with a single period consisting of silent raw audio of a given duration. */
+public final class SilenceMediaSource extends BaseMediaSource {
+
+ private static final int SAMPLE_RATE_HZ = 44100;
+ @C.PcmEncoding private static final int ENCODING = C.ENCODING_PCM_16BIT;
+ private static final int CHANNEL_COUNT = 2;
+ private static final Format FORMAT =
+ Format.createAudioSampleFormat(
+ /* id=*/ null,
+ MimeTypes.AUDIO_RAW,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* maxInputSize= */ Format.NO_VALUE,
+ CHANNEL_COUNT,
+ SAMPLE_RATE_HZ,
+ ENCODING,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null);
+ private static final byte[] SILENCE_SAMPLE =
+ new byte[Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * 1024];
+
+ private final long durationUs;
+
+ /**
+ * Creates a new media source providing silent audio of the given duration.
+ *
+ * @param durationUs The duration of silent audio to output, in microseconds.
+ */
+ public SilenceMediaSource(long durationUs) {
+ Assertions.checkArgument(durationUs >= 0);
+ this.durationUs = durationUs;
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ refreshSourceInfo(
+ new SinglePeriodTimeline(
+ durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false));
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() {}
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ return new SilenceMediaPeriod(durationUs);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {}
+
+ @Override
+ protected void releaseSourceInternal() {}
+
+ private static final class SilenceMediaPeriod implements MediaPeriod {
+
+ private static final TrackGroupArray TRACKS = new TrackGroupArray(new TrackGroup(FORMAT));
+
+ private final long durationUs;
+ private final ArrayList<SampleStream> sampleStreams;
+
+ public SilenceMediaPeriod(long durationUs) {
+ this.durationUs = durationUs;
+ sampleStreams = new ArrayList<>();
+ }
+
+ @Override
+ public void prepare(Callback callback, long positionUs) {
+ callback.onPrepared(/* mediaPeriod= */ this);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() {}
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return TRACKS;
+ }
+
+ @Override
+ public long selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs) {
+ positionUs = constrainSeekPosition(positionUs);
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+ sampleStreams.remove(streams[i]);
+ streams[i] = null;
+ }
+ if (streams[i] == null && selections[i] != null) {
+ SilenceSampleStream stream = new SilenceSampleStream(durationUs);
+ stream.seekTo(positionUs);
+ sampleStreams.add(stream);
+ streams[i] = stream;
+ streamResetFlags[i] = true;
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs, boolean toKeyframe) {}
+
+ @Override
+ public long readDiscontinuity() {
+ return C.TIME_UNSET;
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ positionUs = constrainSeekPosition(positionUs);
+ for (int i = 0; i < sampleStreams.size(); i++) {
+ ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs);
+ }
+ return positionUs;
+ }
+
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return constrainSeekPosition(positionUs);
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return C.TIME_END_OF_SOURCE;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return C.TIME_END_OF_SOURCE;
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return false;
+ }
+
+ @Override
+ public boolean isLoading() {
+ return false;
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {}
+
+ private long constrainSeekPosition(long positionUs) {
+ return Util.constrainValue(positionUs, 0, durationUs);
+ }
+ }
+
+ private static final class SilenceSampleStream implements SampleStream {
+
+ private final long durationBytes;
+
+ private boolean sentFormat;
+ private long positionBytes;
+
+ public SilenceSampleStream(long durationUs) {
+ durationBytes = getAudioByteCount(durationUs);
+ seekTo(0);
+ }
+
+ public void seekTo(long positionUs) {
+ positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes);
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void maybeThrowError() {}
+
+ @Override
+ public int readData(
+ FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) {
+ if (!sentFormat || formatRequired) {
+ formatHolder.format = FORMAT;
+ sentFormat = true;
+ return C.RESULT_FORMAT_READ;
+ }
+
+ long bytesRemaining = durationBytes - positionBytes;
+ if (bytesRemaining == 0) {
+ buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ }
+
+ int bytesToWrite = (int) Math.min(SILENCE_SAMPLE.length, bytesRemaining);
+ buffer.ensureSpaceForWrite(bytesToWrite);
+ buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite);
+ buffer.timeUs = getAudioPositionUs(positionBytes);
+ buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
+ positionBytes += bytesToWrite;
+ return C.RESULT_BUFFER_READ;
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ long oldPositionBytes = positionBytes;
+ seekTo(positionUs);
+ return (int) ((positionBytes - oldPositionBytes) / SILENCE_SAMPLE.length);
+ }
+ }
+
+ private static long getAudioByteCount(long durationUs) {
+ long audioSampleCount = durationUs * SAMPLE_RATE_HZ / C.MICROS_PER_SECOND;
+ return Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * audioSampleCount;
+ }
+
+ private static long getAudioPositionUs(long bytes) {
+ long audioSampleCount = bytes / Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT);
+ return audioSampleCount * C.MICROS_PER_SECOND / SAMPLE_RATE_HZ;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
new file mode 100644
index 0000000000..72d805dfa3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
@@ -0,0 +1,227 @@
+/*
+ * 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;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * A {@link Timeline} consisting of a single period and static window.
+ */
+public final class SinglePeriodTimeline extends Timeline {
+
+ private static final Object UID = new Object();
+
+ private final long presentationStartTimeMs;
+ private final long windowStartTimeMs;
+ private final long periodDurationUs;
+ private final long windowDurationUs;
+ private final long windowPositionInPeriodUs;
+ private final long windowDefaultStartPositionUs;
+ private final boolean isSeekable;
+ private final boolean isDynamic;
+ private final boolean isLive;
+ @Nullable private final Object tag;
+ @Nullable private final Object manifest;
+
+ /**
+ * Creates a timeline containing a single period and a window that spans it.
+ *
+ * @param durationUs The duration of the period, in microseconds.
+ * @param isSeekable Whether seeking is supported within the period.
+ * @param isDynamic Whether the window may change when the timeline is updated.
+ * @param isLive Whether the window is live.
+ */
+ public SinglePeriodTimeline(
+ long durationUs, boolean isSeekable, boolean isDynamic, boolean isLive) {
+ this(durationUs, isSeekable, isDynamic, isLive, /* manifest= */ null, /* tag= */ null);
+ }
+
+ /**
+ * Creates a timeline containing a single period and a window that spans it.
+ *
+ * @param durationUs The duration of the period, in microseconds.
+ * @param isSeekable Whether seeking is supported within the period.
+ * @param isDynamic Whether the window may change when the timeline is updated.
+ * @param isLive Whether the window is live.
+ * @param manifest The manifest. May be {@code null}.
+ * @param tag A tag used for {@link Window#tag}.
+ */
+ public SinglePeriodTimeline(
+ long durationUs,
+ boolean isSeekable,
+ boolean isDynamic,
+ boolean isLive,
+ @Nullable Object manifest,
+ @Nullable Object tag) {
+ this(
+ durationUs,
+ durationUs,
+ /* windowPositionInPeriodUs= */ 0,
+ /* windowDefaultStartPositionUs= */ 0,
+ isSeekable,
+ isDynamic,
+ isLive,
+ manifest,
+ tag);
+ }
+
+ /**
+ * Creates a timeline with one period, and a window of known duration starting at a specified
+ * position in the period.
+ *
+ * @param periodDurationUs The duration of the period in microseconds.
+ * @param windowDurationUs The duration of the window in microseconds.
+ * @param windowPositionInPeriodUs The position of the start of the window in the period, in
+ * microseconds.
+ * @param windowDefaultStartPositionUs The default position relative to the start of the window at
+ * which to begin playback, in microseconds.
+ * @param isSeekable Whether seeking is supported within the window.
+ * @param isDynamic Whether the window may change when the timeline is updated.
+ * @param isLive Whether the window is live.
+ * @param manifest The manifest. May be (@code null}.
+ * @param tag A tag used for {@link Timeline.Window#tag}.
+ */
+ public SinglePeriodTimeline(
+ long periodDurationUs,
+ long windowDurationUs,
+ long windowPositionInPeriodUs,
+ long windowDefaultStartPositionUs,
+ boolean isSeekable,
+ boolean isDynamic,
+ boolean isLive,
+ @Nullable Object manifest,
+ @Nullable Object tag) {
+ this(
+ /* presentationStartTimeMs= */ C.TIME_UNSET,
+ /* windowStartTimeMs= */ C.TIME_UNSET,
+ periodDurationUs,
+ windowDurationUs,
+ windowPositionInPeriodUs,
+ windowDefaultStartPositionUs,
+ isSeekable,
+ isDynamic,
+ isLive,
+ manifest,
+ tag);
+ }
+
+ /**
+ * Creates a timeline with one period, and a window of known duration starting at a specified
+ * position in the period.
+ *
+ * @param presentationStartTimeMs The start time of the presentation in milliseconds since the
+ * epoch.
+ * @param windowStartTimeMs The window's start time in milliseconds since the epoch.
+ * @param periodDurationUs The duration of the period in microseconds.
+ * @param windowDurationUs The duration of the window in microseconds.
+ * @param windowPositionInPeriodUs The position of the start of the window in the period, in
+ * microseconds.
+ * @param windowDefaultStartPositionUs The default position relative to the start of the window at
+ * which to begin playback, in microseconds.
+ * @param isSeekable Whether seeking is supported within the window.
+ * @param isDynamic Whether the window may change when the timeline is updated.
+ * @param isLive Whether the window is live.
+ * @param manifest The manifest. May be {@code null}.
+ * @param tag A tag used for {@link Timeline.Window#tag}.
+ */
+ public SinglePeriodTimeline(
+ long presentationStartTimeMs,
+ long windowStartTimeMs,
+ long periodDurationUs,
+ long windowDurationUs,
+ long windowPositionInPeriodUs,
+ long windowDefaultStartPositionUs,
+ boolean isSeekable,
+ boolean isDynamic,
+ boolean isLive,
+ @Nullable Object manifest,
+ @Nullable Object tag) {
+ this.presentationStartTimeMs = presentationStartTimeMs;
+ this.windowStartTimeMs = windowStartTimeMs;
+ this.periodDurationUs = periodDurationUs;
+ this.windowDurationUs = windowDurationUs;
+ this.windowPositionInPeriodUs = windowPositionInPeriodUs;
+ this.windowDefaultStartPositionUs = windowDefaultStartPositionUs;
+ this.isSeekable = isSeekable;
+ this.isDynamic = isDynamic;
+ this.isLive = isLive;
+ this.manifest = manifest;
+ this.tag = tag;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return 1;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ Assertions.checkIndex(windowIndex, 0, 1);
+ long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;
+ if (isDynamic && defaultPositionProjectionUs != 0) {
+ if (windowDurationUs == C.TIME_UNSET) {
+ // Don't allow projection into a window that has an unknown duration.
+ windowDefaultStartPositionUs = C.TIME_UNSET;
+ } else {
+ windowDefaultStartPositionUs += defaultPositionProjectionUs;
+ if (windowDefaultStartPositionUs > windowDurationUs) {
+ // The projection takes us beyond the end of the window.
+ windowDefaultStartPositionUs = C.TIME_UNSET;
+ }
+ }
+ }
+ return window.set(
+ Window.SINGLE_WINDOW_UID,
+ tag,
+ manifest,
+ presentationStartTimeMs,
+ windowStartTimeMs,
+ isSeekable,
+ isDynamic,
+ isLive,
+ windowDefaultStartPositionUs,
+ windowDurationUs,
+ 0,
+ 0,
+ windowPositionInPeriodUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ Assertions.checkIndex(periodIndex, 0, 1);
+ Object uid = setIds ? UID : null;
+ return period.set(/* id= */ null, uid, 0, periodDurationUs, -windowPositionInPeriodUs);
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return UID.equals(uid) ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public Object getUidOfPeriod(int periodIndex) {
+ Assertions.checkIndex(periodIndex, 0, 1);
+ return UID;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
new file mode 100644
index 0000000000..6c7d92dac9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -0,0 +1,423 @@
+/*
+ * 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;
+
+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.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+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.Loader.Loadable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.StatsDataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+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.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * A {@link MediaPeriod} with a single sample.
+ */
+/* package */ final class SingleSampleMediaPeriod implements MediaPeriod,
+ Loader.Callback<SingleSampleMediaPeriod.SourceLoadable> {
+
+ /**
+ * The initial size of the allocation used to hold the sample data.
+ */
+ private static final int INITIAL_SAMPLE_SIZE = 1024;
+
+ private final DataSpec dataSpec;
+ private final DataSource.Factory dataSourceFactory;
+ @Nullable private final TransferListener transferListener;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final EventDispatcher eventDispatcher;
+ private final TrackGroupArray tracks;
+ private final ArrayList<SampleStreamImpl> sampleStreams;
+ private final long durationUs;
+
+ // Package private to avoid thunk methods.
+ /* package */ final Loader loader;
+ /* package */ final Format format;
+ /* package */ final boolean treatLoadErrorsAsEndOfStream;
+
+ /* package */ boolean notifiedReadingStarted;
+ /* package */ boolean loadingFinished;
+ /* package */ byte @MonotonicNonNull [] sampleData;
+ /* package */ int sampleSize;
+
+ public SingleSampleMediaPeriod(
+ DataSpec dataSpec,
+ DataSource.Factory dataSourceFactory,
+ @Nullable TransferListener transferListener,
+ Format format,
+ long durationUs,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ EventDispatcher eventDispatcher,
+ boolean treatLoadErrorsAsEndOfStream) {
+ this.dataSpec = dataSpec;
+ this.dataSourceFactory = dataSourceFactory;
+ this.transferListener = transferListener;
+ this.format = format;
+ this.durationUs = durationUs;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ this.eventDispatcher = eventDispatcher;
+ this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
+ tracks = new TrackGroupArray(new TrackGroup(format));
+ sampleStreams = new ArrayList<>();
+ loader = new Loader("Loader:SingleSampleMediaPeriod");
+ eventDispatcher.mediaPeriodCreated();
+ }
+
+ public void release() {
+ loader.release();
+ eventDispatcher.mediaPeriodReleased();
+ }
+
+ @Override
+ public void prepare(Callback callback, long positionUs) {
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return tracks;
+ }
+
+ @Override
+ public long selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs) {
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+ sampleStreams.remove(streams[i]);
+ streams[i] = null;
+ }
+ if (streams[i] == null && selections[i] != null) {
+ SampleStreamImpl stream = new SampleStreamImpl();
+ sampleStreams.add(stream);
+ streams[i] = stream;
+ streamResetFlags[i] = true;
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ // Do nothing.
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ // Do nothing.
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (loadingFinished || loader.isLoading() || loader.hasFatalError()) {
+ return false;
+ }
+ DataSource dataSource = dataSourceFactory.createDataSource();
+ if (transferListener != null) {
+ dataSource.addTransferListener(transferListener);
+ }
+ long elapsedRealtimeMs =
+ loader.startLoading(
+ new SourceLoadable(dataSpec, dataSource),
+ /* callback= */ this,
+ loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA));
+ eventDispatcher.loadStarted(
+ dataSpec,
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ format,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ 0,
+ durationUs,
+ elapsedRealtimeMs);
+ return true;
+ }
+
+ @Override
+ public boolean isLoading() {
+ return loader.isLoading();
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ if (!notifiedReadingStarted) {
+ eventDispatcher.readingStarted();
+ notifiedReadingStarted = true;
+ }
+ return C.TIME_UNSET;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return loadingFinished || loader.isLoading() ? C.TIME_END_OF_SOURCE : 0;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return loadingFinished ? C.TIME_END_OF_SOURCE : 0;
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ for (int i = 0; i < sampleStreams.size(); i++) {
+ sampleStreams.get(i).reset();
+ }
+ return positionUs;
+ }
+
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return positionUs;
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs,
+ long loadDurationMs) {
+ sampleSize = (int) loadable.dataSource.getBytesRead();
+ sampleData = Assertions.checkNotNull(loadable.sampleData);
+ loadingFinished = true;
+ eventDispatcher.loadCompleted(
+ loadable.dataSpec,
+ loadable.dataSource.getLastOpenedUri(),
+ loadable.dataSource.getLastResponseHeaders(),
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ format,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ 0,
+ durationUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ sampleSize);
+ }
+
+ @Override
+ public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
+ boolean released) {
+ eventDispatcher.loadCanceled(
+ loadable.dataSpec,
+ loadable.dataSource.getLastOpenedUri(),
+ loadable.dataSource.getLastResponseHeaders(),
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ 0,
+ durationUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.dataSource.getBytesRead());
+ }
+
+ @Override
+ public LoadErrorAction onLoadError(
+ SourceLoadable loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ IOException error,
+ int errorCount) {
+ long retryDelay =
+ loadErrorHandlingPolicy.getRetryDelayMsFor(
+ C.DATA_TYPE_MEDIA, loadDurationMs, error, errorCount);
+ boolean errorCanBePropagated =
+ retryDelay == C.TIME_UNSET
+ || errorCount
+ >= loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA);
+
+ LoadErrorAction action;
+ if (treatLoadErrorsAsEndOfStream && errorCanBePropagated) {
+ loadingFinished = true;
+ action = Loader.DONT_RETRY;
+ } else {
+ action =
+ retryDelay != C.TIME_UNSET
+ ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelay)
+ : Loader.DONT_RETRY_FATAL;
+ }
+ eventDispatcher.loadError(
+ loadable.dataSpec,
+ loadable.dataSource.getLastOpenedUri(),
+ loadable.dataSource.getLastResponseHeaders(),
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ format,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ 0,
+ durationUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.dataSource.getBytesRead(),
+ error,
+ /* wasCanceled= */ !action.isRetry());
+ return action;
+ }
+
+ private final class SampleStreamImpl implements SampleStream {
+
+ private static final int STREAM_STATE_SEND_FORMAT = 0;
+ private static final int STREAM_STATE_SEND_SAMPLE = 1;
+ private static final int STREAM_STATE_END_OF_STREAM = 2;
+
+ private int streamState;
+ private boolean notifiedDownstreamFormat;
+
+ public void reset() {
+ if (streamState == STREAM_STATE_END_OF_STREAM) {
+ streamState = STREAM_STATE_SEND_SAMPLE;
+ }
+ }
+
+ @Override
+ public boolean isReady() {
+ return loadingFinished;
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ if (!treatLoadErrorsAsEndOfStream) {
+ loader.maybeThrowError();
+ }
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean requireFormat) {
+ maybeNotifyDownstreamFormat();
+ if (streamState == STREAM_STATE_END_OF_STREAM) {
+ buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ } else if (requireFormat || streamState == STREAM_STATE_SEND_FORMAT) {
+ formatHolder.format = format;
+ streamState = STREAM_STATE_SEND_SAMPLE;
+ return C.RESULT_FORMAT_READ;
+ } else if (loadingFinished) {
+ if (sampleData != null) {
+ buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
+ buffer.timeUs = 0;
+ if (buffer.isFlagsOnly()) {
+ return C.RESULT_BUFFER_READ;
+ }
+ buffer.ensureSpaceForWrite(sampleSize);
+ buffer.data.put(sampleData, 0, sampleSize);
+ } else {
+ buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ }
+ streamState = STREAM_STATE_END_OF_STREAM;
+ return C.RESULT_BUFFER_READ;
+ }
+ return C.RESULT_NOTHING_READ;
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ maybeNotifyDownstreamFormat();
+ if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) {
+ streamState = STREAM_STATE_END_OF_STREAM;
+ return 1;
+ }
+ return 0;
+ }
+
+ private void maybeNotifyDownstreamFormat() {
+ if (!notifiedDownstreamFormat) {
+ eventDispatcher.downstreamFormatChanged(
+ MimeTypes.getTrackType(format.sampleMimeType),
+ format,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaTimeUs= */ 0);
+ notifiedDownstreamFormat = true;
+ }
+ }
+ }
+
+ /* package */ static final class SourceLoadable implements Loadable {
+
+ public final DataSpec dataSpec;
+
+ private final StatsDataSource dataSource;
+
+ @Nullable private byte[] sampleData;
+
+ // the constructor does not initialize fields: sampleData
+ @SuppressWarnings("nullness:initialization.fields.uninitialized")
+ public SourceLoadable(DataSpec dataSpec, DataSource dataSource) {
+ this.dataSpec = dataSpec;
+ this.dataSource = new StatsDataSource(dataSource);
+ }
+
+ @Override
+ public void cancelLoad() {
+ // Never happens.
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ // We always load from the beginning, so reset bytesRead to 0.
+ dataSource.resetBytesRead();
+ try {
+ // Create and open the input.
+ dataSource.open(dataSpec);
+ // Load the sample data.
+ int result = 0;
+ while (result != C.RESULT_END_OF_INPUT) {
+ int sampleSize = (int) dataSource.getBytesRead();
+ if (sampleData == null) {
+ sampleData = new byte[INITIAL_SAMPLE_SIZE];
+ } else if (sampleSize == sampleData.length) {
+ sampleData = Arrays.copyOf(sampleData, sampleData.length * 2);
+ }
+ result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
new file mode 100644
index 0000000000..01f35ef775
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
@@ -0,0 +1,371 @@
+/*
+ * 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;
+
+import android.net.Uri;
+import android.os.Handler;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}.
+ */
+public final class SingleSampleMediaSource extends BaseMediaSource {
+
+ /**
+ * Listener of {@link SingleSampleMediaSource} events.
+ *
+ * @deprecated Use {@link MediaSourceEventListener}.
+ */
+ @Deprecated
+ public interface EventListener {
+
+ /**
+ * Called when an error occurs loading media data.
+ *
+ * @param sourceId The id of the reporting {@link SingleSampleMediaSource}.
+ * @param e The cause of the failure.
+ */
+ void onLoadError(int sourceId, IOException e);
+
+ }
+
+ /** Factory for {@link SingleSampleMediaSource}. */
+ public static final class Factory {
+
+ private final DataSource.Factory dataSourceFactory;
+
+ private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private boolean treatLoadErrorsAsEndOfStream;
+ private boolean isCreateCalled;
+ @Nullable private Object tag;
+
+ /**
+ * Creates a factory for {@link SingleSampleMediaSource}s.
+ *
+ * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will
+ * be obtained.
+ */
+ public Factory(DataSource.Factory dataSourceFactory) {
+ this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory);
+ loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
+ }
+
+ /**
+ * Sets a tag for the media source which will be published in the {@link Timeline} of the source
+ * as {@link Timeline.Window#tag}.
+ *
+ * @param tag A tag for the media source.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setTag(Object tag) {
+ Assertions.checkState(!isCreateCalled);
+ this.tag = tag;
+ return this;
+ }
+
+ /**
+ * Sets the minimum number of times to retry if a loading error occurs. See {@link
+ * #setLoadErrorHandlingPolicy} for the default value.
+ *
+ * <p>Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with
+ * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int)
+ * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)}
+ *
+ * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead.
+ */
+ @Deprecated
+ public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {
+ return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount));
+ }
+
+ /**
+ * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link
+ * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.
+ *
+ * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}.
+ *
+ * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
+ Assertions.checkState(!isCreateCalled);
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ return this;
+ }
+
+ /**
+ * Sets whether load errors will be treated as end-of-stream signal (load errors will not be
+ * propagated). The default value is false.
+ *
+ * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample
+ * streams, treating them as ended instead. If false, load errors will be propagated
+ * normally by {@link SampleStream#maybeThrowError()}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) {
+ Assertions.checkState(!isCreateCalled);
+ this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link SingleSampleMediaSource} using the current parameters.
+ *
+ * @param uri The {@link Uri}.
+ * @param format The {@link Format} of the media stream.
+ * @param durationUs The duration of the media stream in microseconds.
+ * @return The new {@link SingleSampleMediaSource}.
+ */
+ public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) {
+ isCreateCalled = true;
+ return new SingleSampleMediaSource(
+ uri,
+ dataSourceFactory,
+ format,
+ durationUs,
+ loadErrorHandlingPolicy,
+ treatLoadErrorsAsEndOfStream,
+ tag);
+ }
+
+ /**
+ * @deprecated Use {@link #createMediaSource(Uri, Format, long)} and {@link
+ * #addEventListener(Handler, MediaSourceEventListener)} instead.
+ */
+ @Deprecated
+ public SingleSampleMediaSource createMediaSource(
+ Uri uri,
+ Format format,
+ long durationUs,
+ @Nullable Handler eventHandler,
+ @Nullable MediaSourceEventListener eventListener) {
+ SingleSampleMediaSource mediaSource = createMediaSource(uri, format, durationUs);
+ if (eventHandler != null && eventListener != null) {
+ mediaSource.addEventListener(eventHandler, eventListener);
+ }
+ return mediaSource;
+ }
+
+ }
+
+ private final DataSpec dataSpec;
+ private final DataSource.Factory dataSourceFactory;
+ private final Format format;
+ private final long durationUs;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final boolean treatLoadErrorsAsEndOfStream;
+ private final Timeline timeline;
+ @Nullable private final Object tag;
+
+ @Nullable private TransferListener transferListener;
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will
+ * be obtained.
+ * @param format The {@link Format} associated with the output track.
+ * @param durationUs The duration of the media stream in microseconds.
+ * @deprecated Use {@link Factory} instead.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public SingleSampleMediaSource(
+ Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) {
+ this(
+ uri,
+ dataSourceFactory,
+ format,
+ durationUs,
+ DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT);
+ }
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will
+ * be obtained.
+ * @param format The {@link Format} associated with the output track.
+ * @param durationUs The duration of the media stream in microseconds.
+ * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+ * @deprecated Use {@link Factory} instead.
+ */
+ @Deprecated
+ public SingleSampleMediaSource(
+ Uri uri,
+ DataSource.Factory dataSourceFactory,
+ Format format,
+ long durationUs,
+ int minLoadableRetryCount) {
+ this(
+ uri,
+ dataSourceFactory,
+ format,
+ durationUs,
+ new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount),
+ /* treatLoadErrorsAsEndOfStream= */ false,
+ /* tag= */ null);
+ }
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will
+ * be obtained.
+ * @param format The {@link Format} associated with the output track.
+ * @param durationUs The duration of the media stream in microseconds.
+ * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param eventSourceId An identifier that gets passed to {@code eventListener} methods.
+ * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample
+ * streams, treating them as ended instead. If false, load errors will be propagated normally
+ * by {@link SampleStream#maybeThrowError()}.
+ * @deprecated Use {@link Factory} instead.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public SingleSampleMediaSource(
+ Uri uri,
+ DataSource.Factory dataSourceFactory,
+ Format format,
+ long durationUs,
+ int minLoadableRetryCount,
+ Handler eventHandler,
+ EventListener eventListener,
+ int eventSourceId,
+ boolean treatLoadErrorsAsEndOfStream) {
+ this(
+ uri,
+ dataSourceFactory,
+ format,
+ durationUs,
+ new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount),
+ treatLoadErrorsAsEndOfStream,
+ /* tag= */ null);
+ if (eventHandler != null && eventListener != null) {
+ addEventListener(eventHandler, new EventListenerWrapper(eventListener, eventSourceId));
+ }
+ }
+
+ private SingleSampleMediaSource(
+ Uri uri,
+ DataSource.Factory dataSourceFactory,
+ Format format,
+ long durationUs,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ boolean treatLoadErrorsAsEndOfStream,
+ @Nullable Object tag) {
+ this.dataSourceFactory = dataSourceFactory;
+ this.format = format;
+ this.durationUs = durationUs;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
+ this.tag = tag;
+ dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP);
+ timeline =
+ new SinglePeriodTimeline(
+ durationUs,
+ /* isSeekable= */ true,
+ /* isDynamic= */ false,
+ /* isLive= */ false,
+ /* manifest= */ null,
+ tag);
+ }
+
+ // MediaSource implementation.
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return tag;
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ transferListener = mediaTransferListener;
+ refreshSourceInfo(timeline);
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ return new SingleSampleMediaPeriod(
+ dataSpec,
+ dataSourceFactory,
+ transferListener,
+ format,
+ durationUs,
+ loadErrorHandlingPolicy,
+ createEventDispatcher(id),
+ treatLoadErrorsAsEndOfStream);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ ((SingleSampleMediaPeriod) mediaPeriod).release();
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ // Do nothing.
+ }
+
+ /**
+ * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in
+ * {@link MediaSourceEventListener}.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ private static final class EventListenerWrapper implements MediaSourceEventListener {
+
+ private final EventListener eventListener;
+ private final int eventSourceId;
+
+ public EventListenerWrapper(EventListener eventListener, int eventSourceId) {
+ this.eventListener = Assertions.checkNotNull(eventListener);
+ this.eventSourceId = eventSourceId;
+ }
+
+ @Override
+ public void onLoadError(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {
+ eventListener.onLoadError(eventSourceId, error);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java
new file mode 100644
index 0000000000..566238dbdb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+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.util.Assertions;
+import java.util.Arrays;
+
+// TODO: Add an allowMultipleStreams boolean to indicate where the one stream per group restriction
+// does not apply.
+/**
+ * Defines a group of tracks exposed by a {@link MediaPeriod}.
+ *
+ * <p>A {@link MediaPeriod} is only able to provide one {@link SampleStream} corresponding to a
+ * group at any given time, however this {@link SampleStream} may adapt between multiple tracks
+ * within the group.
+ */
+public final class TrackGroup implements Parcelable {
+
+ /**
+ * The number of tracks in the group.
+ */
+ public final int length;
+
+ private final Format[] formats;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @param formats The track formats. Must not be null, contain null elements or be of length 0.
+ */
+ public TrackGroup(Format... formats) {
+ Assertions.checkState(formats.length > 0);
+ this.formats = formats;
+ this.length = formats.length;
+ }
+
+ /* package */ TrackGroup(Parcel in) {
+ length = in.readInt();
+ formats = new Format[length];
+ for (int i = 0; i < length; i++) {
+ formats[i] = in.readParcelable(Format.class.getClassLoader());
+ }
+ }
+
+ /**
+ * Returns the format of the track at a given index.
+ *
+ * @param index The index of the track.
+ * @return The track's format.
+ */
+ public Format getFormat(int index) {
+ return formats[index];
+ }
+
+ /**
+ * Returns the index of the track with the given format in the group. The format is located by
+ * identity so, for example, {@code group.indexOf(group.getFormat(index)) == index} even if
+ * multiple tracks have formats that contain the same values.
+ *
+ * @param format The format.
+ * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ public int indexOf(Format format) {
+ for (int i = 0; i < formats.length; i++) {
+ if (format == formats[i]) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + Arrays.hashCode(formats);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackGroup other = (TrackGroup) obj;
+ return length == other.length && Arrays.equals(formats, other.formats);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(length);
+ for (int i = 0; i < length; i++) {
+ dest.writeParcelable(formats[i], 0);
+ }
+ }
+
+ public static final Parcelable.Creator<TrackGroup> CREATOR =
+ new Parcelable.Creator<TrackGroup>() {
+
+ @Override
+ public TrackGroup createFromParcel(Parcel in) {
+ return new TrackGroup(in);
+ }
+
+ @Override
+ public TrackGroup[] newArray(int size) {
+ return new TrackGroup[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java
new file mode 100644
index 0000000000..103a45080e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java
@@ -0,0 +1,141 @@
+/*
+ * 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;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.util.Arrays;
+
+/** An array of {@link TrackGroup}s exposed by a {@link MediaPeriod}. */
+public final class TrackGroupArray implements Parcelable {
+
+ /**
+ * The empty array.
+ */
+ public static final TrackGroupArray EMPTY = new TrackGroupArray();
+
+ /**
+ * The number of groups in the array. Greater than or equal to zero.
+ */
+ public final int length;
+
+ private final TrackGroup[] trackGroups;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @param trackGroups The groups. Must not be null or contain null elements, but may be empty.
+ */
+ public TrackGroupArray(TrackGroup... trackGroups) {
+ this.trackGroups = trackGroups;
+ this.length = trackGroups.length;
+ }
+
+ /* package */ TrackGroupArray(Parcel in) {
+ length = in.readInt();
+ trackGroups = new TrackGroup[length];
+ for (int i = 0; i < length; i++) {
+ trackGroups[i] = in.readParcelable(TrackGroup.class.getClassLoader());
+ }
+ }
+
+ /**
+ * Returns the group at a given index.
+ *
+ * @param index The index of the group.
+ * @return The group.
+ */
+ public TrackGroup get(int index) {
+ return trackGroups[index];
+ }
+
+ /**
+ * Returns the index of a group within the array.
+ *
+ * @param group The group.
+ * @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ public int indexOf(TrackGroup group) {
+ for (int i = 0; i < length; i++) {
+ // Suppressed reference equality warning because this is looking for the index of a specific
+ // TrackGroup object, not the index of a potential equal TrackGroup.
+ if (trackGroups[i] == group) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns whether this track group array is empty.
+ */
+ public boolean isEmpty() {
+ return length == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ hashCode = Arrays.hashCode(trackGroups);
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackGroupArray other = (TrackGroupArray) obj;
+ return length == other.length && Arrays.equals(trackGroups, other.trackGroups);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(length);
+ for (int i = 0; i < length; i++) {
+ dest.writeParcelable(trackGroups[i], 0);
+ }
+ }
+
+ public static final Parcelable.Creator<TrackGroupArray> CREATOR =
+ new Parcelable.Creator<TrackGroupArray>() {
+
+ @Override
+ public TrackGroupArray createFromParcel(Parcel in) {
+ return new TrackGroupArray(in);
+ }
+
+ @Override
+ public TrackGroupArray[] newArray(int size) {
+ return new TrackGroupArray[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java
new file mode 100644
index 0000000000..ccb9d350fc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.net.Uri;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+
+/**
+ * Thrown if the input format was not recognized.
+ */
+public class UnrecognizedInputFormatException extends ParserException {
+
+ /**
+ * The {@link Uri} from which the unrecognized data was read.
+ */
+ public final Uri uri;
+
+ /**
+ * @param message The detail message for the exception.
+ * @param uri The {@link Uri} from which the unrecognized data was read.
+ */
+ public UnrecognizedInputFormatException(String message, Uri uri) {
+ super(message);
+ this.uri = uri;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
new file mode 100644
index 0000000000..83b5b1bc40
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2017 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.ads;
+
+import android.net.Uri;
+import androidx.annotation.CheckResult;
+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.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * Represents ad group times relative to the start of the media and information on the state and
+ * URIs of ads within each ad group.
+ *
+ * <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the
+ * required changes.
+ */
+public final class AdPlaybackState {
+
+ /**
+ * Represents a group of ads, with information about their states.
+ *
+ * <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the
+ * required changes.
+ */
+ public static final class AdGroup {
+
+ /** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */
+ public final int count;
+ /** The URI of each ad in the ad group. */
+ public final @NullableType Uri[] uris;
+ /** The state of each ad in the ad group. */
+ @AdState public final int[] states;
+ /** The durations of each ad in the ad group, in microseconds. */
+ public final long[] durationsUs;
+
+ /** Creates a new ad group with an unspecified number of ads. */
+ public AdGroup() {
+ this(
+ /* count= */ C.LENGTH_UNSET,
+ /* states= */ new int[0],
+ /* uris= */ new Uri[0],
+ /* durationsUs= */ new long[0]);
+ }
+
+ private AdGroup(
+ int count, @AdState int[] states, @NullableType Uri[] uris, long[] durationsUs) {
+ Assertions.checkArgument(states.length == uris.length);
+ this.count = count;
+ this.states = states;
+ this.uris = uris;
+ this.durationsUs = durationsUs;
+ }
+
+ /**
+ * Returns the index of the first ad in the ad group that should be played, or {@link #count} if
+ * no ads should be played.
+ */
+ public int getFirstAdIndexToPlay() {
+ return getNextAdIndexToPlay(-1);
+ }
+
+ /**
+ * Returns the index of the next ad in the ad group that should be played after playing {@code
+ * lastPlayedAdIndex}, or {@link #count} if no later ads should be played.
+ */
+ public int getNextAdIndexToPlay(int lastPlayedAdIndex) {
+ int nextAdIndexToPlay = lastPlayedAdIndex + 1;
+ while (nextAdIndexToPlay < states.length) {
+ if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE
+ || states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) {
+ break;
+ }
+ nextAdIndexToPlay++;
+ }
+ return nextAdIndexToPlay;
+ }
+
+ /** Returns whether the ad group has at least one ad that still needs to be played. */
+ public boolean hasUnplayedAds() {
+ return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ AdGroup adGroup = (AdGroup) o;
+ return count == adGroup.count
+ && Arrays.equals(uris, adGroup.uris)
+ && Arrays.equals(states, adGroup.states)
+ && Arrays.equals(durationsUs, adGroup.durationsUs);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = count;
+ result = 31 * result + Arrays.hashCode(uris);
+ result = 31 * result + Arrays.hashCode(states);
+ result = 31 * result + Arrays.hashCode(durationsUs);
+ return result;
+ }
+
+ /**
+ * Returns a new instance with the ad count set to {@code count}. This method may only be called
+ * if this instance's ad count has not yet been specified.
+ */
+ @CheckResult
+ public AdGroup withAdCount(int count) {
+ Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count);
+ @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count);
+ long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
+ @NullableType Uri[] uris = Arrays.copyOf(this.uris, count);
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ /**
+ * Returns a new instance with the specified {@code uri} set for the specified ad, and the ad
+ * marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link
+ * #AD_STATE_UNAVAILABLE}, which is the default state.
+ *
+ * <p>This instance's ad count may be unknown, in which case {@code index} must be less than the
+ * ad count specified later. Otherwise, {@code index} must be less than the current ad count.
+ */
+ @CheckResult
+ public AdGroup withAdUri(Uri uri, int index) {
+ Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
+ @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
+ Assertions.checkArgument(states[index] == AD_STATE_UNAVAILABLE);
+ long[] durationsUs =
+ this.durationsUs.length == states.length
+ ? this.durationsUs
+ : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
+ @NullableType Uri[] uris = Arrays.copyOf(this.uris, states.length);
+ uris[index] = uri;
+ states[index] = AD_STATE_AVAILABLE;
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ /**
+ * Returns a new instance with the specified ad set to the specified {@code state}. The ad
+ * specified must currently either be in {@link #AD_STATE_UNAVAILABLE} or {@link
+ * #AD_STATE_AVAILABLE}.
+ *
+ * <p>This instance's ad count may be unknown, in which case {@code index} must be less than the
+ * ad count specified later. Otherwise, {@code index} must be less than the current ad count.
+ */
+ @CheckResult
+ public AdGroup withAdState(@AdState int state, int index) {
+ Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
+ @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
+ Assertions.checkArgument(
+ states[index] == AD_STATE_UNAVAILABLE
+ || states[index] == AD_STATE_AVAILABLE
+ || states[index] == state);
+ long[] durationsUs =
+ this.durationsUs.length == states.length
+ ? this.durationsUs
+ : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
+ @NullableType
+ Uri[] uris =
+ this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length);
+ states[index] = state;
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ /** Returns a new instance with the specified ad durations, in microseconds. */
+ @CheckResult
+ public AdGroup withAdDurationsUs(long[] durationsUs) {
+ Assertions.checkArgument(count == C.LENGTH_UNSET || durationsUs.length <= this.uris.length);
+ if (durationsUs.length < this.uris.length) {
+ durationsUs = copyDurationsUsWithSpaceForAdCount(durationsUs, uris.length);
+ }
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ /**
+ * Returns an instance with all unavailable and available ads marked as skipped. If the ad count
+ * hasn't been set, it will be set to zero.
+ */
+ @CheckResult
+ public AdGroup withAllAdsSkipped() {
+ if (count == C.LENGTH_UNSET) {
+ return new AdGroup(
+ /* count= */ 0,
+ /* states= */ new int[0],
+ /* uris= */ new Uri[0],
+ /* durationsUs= */ new long[0]);
+ }
+ int count = this.states.length;
+ @AdState int[] states = Arrays.copyOf(this.states, count);
+ for (int i = 0; i < count; i++) {
+ if (states[i] == AD_STATE_AVAILABLE || states[i] == AD_STATE_UNAVAILABLE) {
+ states[i] = AD_STATE_SKIPPED;
+ }
+ }
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ @CheckResult
+ private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) {
+ int oldStateCount = states.length;
+ int newStateCount = Math.max(count, oldStateCount);
+ states = Arrays.copyOf(states, newStateCount);
+ Arrays.fill(states, oldStateCount, newStateCount, AD_STATE_UNAVAILABLE);
+ return states;
+ }
+
+ @CheckResult
+ private static long[] copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int count) {
+ int oldDurationsUsCount = durationsUs.length;
+ int newDurationsUsCount = Math.max(count, oldDurationsUsCount);
+ durationsUs = Arrays.copyOf(durationsUs, newDurationsUsCount);
+ Arrays.fill(durationsUs, oldDurationsUsCount, newDurationsUsCount, C.TIME_UNSET);
+ return durationsUs;
+ }
+ }
+
+ /**
+ * Represents the state of an ad in an ad group. One of {@link #AD_STATE_UNAVAILABLE}, {@link
+ * #AD_STATE_AVAILABLE}, {@link #AD_STATE_SKIPPED}, {@link #AD_STATE_PLAYED} or {@link
+ * #AD_STATE_ERROR}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ AD_STATE_UNAVAILABLE,
+ AD_STATE_AVAILABLE,
+ AD_STATE_SKIPPED,
+ AD_STATE_PLAYED,
+ AD_STATE_ERROR,
+ })
+ public @interface AdState {}
+ /** State for an ad that does not yet have a URL. */
+ public static final int AD_STATE_UNAVAILABLE = 0;
+ /** State for an ad that has a URL but has not yet been played. */
+ public static final int AD_STATE_AVAILABLE = 1;
+ /** State for an ad that was skipped. */
+ public static final int AD_STATE_SKIPPED = 2;
+ /** State for an ad that was played in full. */
+ public static final int AD_STATE_PLAYED = 3;
+ /** State for an ad that could not be loaded. */
+ public static final int AD_STATE_ERROR = 4;
+
+ /** Ad playback state with no ads. */
+ public static final AdPlaybackState NONE = new AdPlaybackState();
+
+ /** The number of ad groups. */
+ public final int adGroupCount;
+ /**
+ * The times of ad groups, in microseconds. A final element with the value {@link
+ * C#TIME_END_OF_SOURCE} indicates a postroll ad.
+ */
+ public final long[] adGroupTimesUs;
+ /** The ad groups. */
+ public final AdGroup[] adGroups;
+ /** The position offset in the first unplayed ad at which to begin playback, in microseconds. */
+ public final long adResumePositionUs;
+ /** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */
+ public final long contentDurationUs;
+
+ /**
+ * Creates a new ad playback state with the specified ad group times.
+ *
+ * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value
+ * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad.
+ */
+ public AdPlaybackState(long... adGroupTimesUs) {
+ int count = adGroupTimesUs.length;
+ adGroupCount = count;
+ this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count);
+ this.adGroups = new AdGroup[count];
+ for (int i = 0; i < count; i++) {
+ adGroups[i] = new AdGroup();
+ }
+ adResumePositionUs = 0;
+ contentDurationUs = C.TIME_UNSET;
+ }
+
+ private AdPlaybackState(
+ long[] adGroupTimesUs, AdGroup[] adGroups, long adResumePositionUs, long contentDurationUs) {
+ adGroupCount = adGroups.length;
+ this.adGroupTimesUs = adGroupTimesUs;
+ this.adGroups = adGroups;
+ this.adResumePositionUs = adResumePositionUs;
+ this.contentDurationUs = contentDurationUs;
+ }
+
+ /**
+ * Returns the index of the ad group at or before {@code positionUs}, if that ad group is
+ * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no
+ * ads remaining to be played, or if there is no such ad group.
+ *
+ * @param positionUs The position at or before which to find an ad group, in microseconds, or
+ * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any
+ * unplayed postroll ad group will be returned).
+ * @return The index of the ad group, or {@link C#INDEX_UNSET}.
+ */
+ public int getAdGroupIndexForPositionUs(long positionUs) {
+ // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
+ // In practice we expect there to be few ad groups so the search shouldn't be expensive.
+ int index = adGroupTimesUs.length - 1;
+ while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) {
+ index--;
+ }
+ return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be
+ * played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
+ *
+ * @param positionUs The position after which to find an ad group, in microseconds, or {@link
+ * C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad group
+ * after the position).
+ * @param periodDurationUs The duration of the containing period in microseconds, or {@link
+ * C#TIME_UNSET} if not known.
+ * @return The index of the ad group, or {@link C#INDEX_UNSET}.
+ */
+ public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) {
+ if (positionUs == C.TIME_END_OF_SOURCE
+ || (periodDurationUs != C.TIME_UNSET && positionUs >= periodDurationUs)) {
+ return C.INDEX_UNSET;
+ }
+ // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
+ // In practice we expect there to be few ad groups so the search shouldn't be expensive.
+ int index = 0;
+ while (index < adGroupTimesUs.length
+ && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE
+ && (positionUs >= adGroupTimesUs[index] || !adGroups[index].hasUnplayedAds())) {
+ index++;
+ }
+ return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}.
+ * The ad count must be greater than zero.
+ */
+ @CheckResult
+ public AdPlaybackState withAdCount(int adGroupIndex, int adCount) {
+ Assertions.checkArgument(adCount > 0);
+ if (adGroups[adGroupIndex].count == adCount) {
+ return this;
+ }
+ AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad URI. */
+ @CheckResult
+ public AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri) {
+ AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdUri(uri, adIndexInAdGroup);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad marked as played. */
+ @CheckResult
+ public AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) {
+ AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_PLAYED, adIndexInAdGroup);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad marked as skipped. */
+ @CheckResult
+ public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) {
+ AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad marked as having a load error. */
+ @CheckResult
+ public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {
+ AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /**
+ * Returns an instance with all ads in the specified ad group skipped (except for those already
+ * marked as played or in the error state).
+ */
+ @CheckResult
+ public AdPlaybackState withSkippedAdGroup(int adGroupIndex) {
+ AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAllAdsSkipped();
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad durations, in microseconds. */
+ @CheckResult
+ public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) {
+ AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
+ for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) {
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]);
+ }
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad resume position, in microseconds. */
+ @CheckResult
+ public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) {
+ if (this.adResumePositionUs == adResumePositionUs) {
+ return this;
+ } else {
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+ }
+
+ /** Returns an instance with the specified content duration, in microseconds. */
+ @CheckResult
+ public AdPlaybackState withContentDurationUs(long contentDurationUs) {
+ if (this.contentDurationUs == contentDurationUs) {
+ return this;
+ } else {
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ AdPlaybackState that = (AdPlaybackState) o;
+ return adGroupCount == that.adGroupCount
+ && adResumePositionUs == that.adResumePositionUs
+ && contentDurationUs == that.contentDurationUs
+ && Arrays.equals(adGroupTimesUs, that.adGroupTimesUs)
+ && Arrays.equals(adGroups, that.adGroups);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = adGroupCount;
+ result = 31 * result + (int) adResumePositionUs;
+ result = 31 * result + (int) contentDurationUs;
+ result = 31 * result + Arrays.hashCode(adGroupTimesUs);
+ result = 31 * result + Arrays.hashCode(adGroups);
+ return result;
+ }
+
+ private boolean isPositionBeforeAdGroup(long positionUs, int adGroupIndex) {
+ if (positionUs == C.TIME_END_OF_SOURCE) {
+ // The end of the content is at (but not before) any postroll ad, and after any other ads.
+ return false;
+ }
+ long adGroupPositionUs = adGroupTimesUs[adGroupIndex];
+ if (adGroupPositionUs == C.TIME_END_OF_SOURCE) {
+ return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs;
+ } else {
+ return positionUs < adGroupPositionUs;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java
new file mode 100644
index 0000000000..12ffb8ec0d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2017 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.ads;
+
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import java.io.IOException;
+
+/**
+ * Interface for loaders of ads, which can be used with {@link AdsMediaSource}.
+ *
+ * <p>Ads loaders notify the {@link AdsMediaSource} about events via {@link EventListener}. In
+ * particular, implementations must call {@link EventListener#onAdPlaybackState(AdPlaybackState)}
+ * with a new copy of the current {@link AdPlaybackState} whenever further information about ads
+ * becomes known (for example, when an ad media URI is available, or an ad has played to the end).
+ *
+ * <p>{@link #start(EventListener, AdViewProvider)} will be called when the ads media source first
+ * initializes, at which point the loader can request ads. If the player enters the background,
+ * {@link #stop()} will be called. Loaders should maintain any ad playback state in preparation for
+ * a later call to {@link #start(EventListener, AdViewProvider)}. If an ad is playing when the
+ * player is detached, update the ad playback state with the current playback position using {@link
+ * AdPlaybackState#withAdResumePositionUs(long)}.
+ *
+ * <p>If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the
+ * implementation of {@link #start(EventListener, AdViewProvider)} should invoke the same listener
+ * to provide the existing playback state to the new player.
+ */
+public interface AdsLoader {
+
+ /** Listener for ads loader events. All methods are called on the main thread. */
+ interface EventListener {
+
+ /**
+ * Called when the ad playback state has been updated.
+ *
+ * @param adPlaybackState The new ad playback state.
+ */
+ default void onAdPlaybackState(AdPlaybackState adPlaybackState) {}
+
+ /**
+ * Called when there was an error loading ads.
+ *
+ * @param error The error.
+ * @param dataSpec The data spec associated with the load error.
+ */
+ default void onAdLoadError(AdLoadException error, DataSpec dataSpec) {}
+
+ /** Called when the user clicks through an ad (for example, following a 'learn more' link). */
+ default void onAdClicked() {}
+
+ /** Called when the user taps a non-clickthrough part of an ad. */
+ default void onAdTapped() {}
+ }
+
+ /** Provides views for the ad UI. */
+ interface AdViewProvider {
+
+ /** Returns the {@link ViewGroup} on top of the player that will show any ad UI. */
+ ViewGroup getAdViewGroup();
+
+ /**
+ * Returns an array of views that are shown on top of the ad view group, but that are essential
+ * for controlling playback and should be excluded from ad viewability measurements by the
+ * {@link AdsLoader} (if it supports this).
+ *
+ * <p>Each view must be either a fully transparent overlay (for capturing touch events), or a
+ * small piece of transient UI that is essential to the user experience of playback (such as a
+ * button to pause/resume playback or a transient full-screen or cast button). For more
+ * information see the documentation for your ads loader.
+ */
+ View[] getAdOverlayViews();
+ }
+
+ // Methods called by the application.
+
+ /**
+ * Sets the player that will play the loaded ads.
+ *
+ * <p>This method must be called before the player is prepared with media using this ads loader.
+ *
+ * <p>This method must also be called on the main thread and only players which are accessed on
+ * the main thread are supported ({@code player.getApplicationLooper() ==
+ * Looper.getMainLooper()}).
+ *
+ * @param player The player instance that will play the loaded ads. May be null to delete the
+ * reference to a previously set player.
+ */
+ void setPlayer(@Nullable Player player);
+
+ /**
+ * Releases the loader. Must be called by the application on the main thread when the instance is
+ * no longer needed.
+ */
+ void release();
+
+ // Methods called by AdsMediaSource.
+
+ /**
+ * Sets the supported content types for ad media. Must be called before the first call to {@link
+ * #start(EventListener, AdViewProvider)}. Subsequent calls may be ignored. Called on the main
+ * thread by {@link AdsMediaSource}.
+ *
+ * @param contentTypes The supported content types for ad media. Each element must be one of
+ * {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}.
+ */
+ void setSupportedContentTypes(@C.ContentType int... contentTypes);
+
+ /**
+ * Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}.
+ *
+ * @param eventListener Listener for ads loader events.
+ * @param adViewProvider Provider of views for the ad UI.
+ */
+ void start(EventListener eventListener, AdViewProvider adViewProvider);
+
+ /**
+ * Stops using the ads loader for playback and deregisters the event listener. Called on the main
+ * thread by {@link AdsMediaSource}.
+ */
+ void stop();
+
+ /**
+ * Notifies the ads loader that the player was not able to prepare media for a given ad.
+ * Implementations should update the ad playback state as the specified ad has failed to load.
+ * Called on the main thread by {@link AdsMediaSource}.
+ *
+ * @param adGroupIndex The index of the ad group.
+ * @param adIndexInAdGroup The index of the ad in the ad group.
+ * @param exception The preparation error.
+ */
+ void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
new file mode 100644
index 0000000000..02c33a3d34
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2017 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.ads;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+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.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.CompositeMediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MaskingMediaPeriod;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source
+ * cannot be used as a child source in a composition. It must be the top-level source used to
+ * prepare the player.
+ */
+public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
+
+ /**
+ * Wrapper for exceptions that occur while loading ads, which are notified via {@link
+ * MediaSourceEventListener#onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData,
+ * IOException, boolean)}.
+ */
+ public static final class AdLoadException extends IOException {
+
+ /**
+ * Types of ad load exceptions. One of {@link #TYPE_AD}, {@link #TYPE_AD_GROUP}, {@link
+ * #TYPE_ALL_ADS} or {@link #TYPE_UNEXPECTED}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_AD, TYPE_AD_GROUP, TYPE_ALL_ADS, TYPE_UNEXPECTED})
+ public @interface Type {}
+ /** Type for when an ad failed to load. The ad will be skipped. */
+ public static final int TYPE_AD = 0;
+ /** Type for when an ad group failed to load. The ad group will be skipped. */
+ public static final int TYPE_AD_GROUP = 1;
+ /** Type for when all ad groups failed to load. All ads will be skipped. */
+ public static final int TYPE_ALL_ADS = 2;
+ /** Type for when an unexpected error occurred while loading ads. All ads will be skipped. */
+ public static final int TYPE_UNEXPECTED = 3;
+
+ /** Returns a new ad load exception of {@link #TYPE_AD}. */
+ public static AdLoadException createForAd(Exception error) {
+ return new AdLoadException(TYPE_AD, error);
+ }
+
+ /** Returns a new ad load exception of {@link #TYPE_AD_GROUP}. */
+ public static AdLoadException createForAdGroup(Exception error, int adGroupIndex) {
+ return new AdLoadException(
+ TYPE_AD_GROUP, new IOException("Failed to load ad group " + adGroupIndex, error));
+ }
+
+ /** Returns a new ad load exception of {@link #TYPE_ALL_ADS}. */
+ public static AdLoadException createForAllAds(Exception error) {
+ return new AdLoadException(TYPE_ALL_ADS, error);
+ }
+
+ /** Returns a new ad load exception of {@link #TYPE_UNEXPECTED}. */
+ public static AdLoadException createForUnexpected(RuntimeException error) {
+ return new AdLoadException(TYPE_UNEXPECTED, error);
+ }
+
+ /** The {@link Type} of the ad load exception. */
+ public final @Type int type;
+
+ private AdLoadException(@Type int type, Exception cause) {
+ super(cause);
+ this.type = type;
+ }
+
+ /**
+ * Returns the {@link RuntimeException} that caused the exception if its type is {@link
+ * #TYPE_UNEXPECTED}.
+ */
+ public RuntimeException getRuntimeExceptionForUnexpected() {
+ Assertions.checkState(type == TYPE_UNEXPECTED);
+ return (RuntimeException) Assertions.checkNotNull(getCause());
+ }
+ }
+
+ // Used to identify the content "child" source for CompositeMediaSource.
+ private static final MediaPeriodId DUMMY_CONTENT_MEDIA_PERIOD_ID =
+ new MediaPeriodId(/* periodUid= */ new Object());
+
+ private final MediaSource contentMediaSource;
+ private final MediaSourceFactory adMediaSourceFactory;
+ private final AdsLoader adsLoader;
+ private final AdsLoader.AdViewProvider adViewProvider;
+ private final Handler mainHandler;
+ private final Map<MediaSource, List<MaskingMediaPeriod>> maskingMediaPeriodByAdMediaSource;
+ private final Timeline.Period period;
+
+ // Accessed on the player thread.
+ @Nullable private ComponentListener componentListener;
+ @Nullable private Timeline contentTimeline;
+ @Nullable private AdPlaybackState adPlaybackState;
+ private @NullableType MediaSource[][] adGroupMediaSources;
+ private @NullableType Timeline[][] adGroupTimelines;
+
+ /**
+ * Constructs a new source that inserts ads linearly with the content specified by {@code
+ * contentMediaSource}. Ad media is loaded using {@link ProgressiveMediaSource}.
+ *
+ * @param contentMediaSource The {@link MediaSource} providing the content to play.
+ * @param dataSourceFactory Factory for data sources used to load ad media.
+ * @param adsLoader The loader for ads.
+ * @param adViewProvider Provider of views for the ad UI.
+ */
+ public AdsMediaSource(
+ MediaSource contentMediaSource,
+ DataSource.Factory dataSourceFactory,
+ AdsLoader adsLoader,
+ AdsLoader.AdViewProvider adViewProvider) {
+ this(
+ contentMediaSource,
+ new ProgressiveMediaSource.Factory(dataSourceFactory),
+ adsLoader,
+ adViewProvider);
+ }
+
+ /**
+ * Constructs a new source that inserts ads linearly with the content specified by {@code
+ * contentMediaSource}.
+ *
+ * @param contentMediaSource The {@link MediaSource} providing the content to play.
+ * @param adMediaSourceFactory Factory for media sources used to load ad media.
+ * @param adsLoader The loader for ads.
+ * @param adViewProvider Provider of views for the ad UI.
+ */
+ public AdsMediaSource(
+ MediaSource contentMediaSource,
+ MediaSourceFactory adMediaSourceFactory,
+ AdsLoader adsLoader,
+ AdsLoader.AdViewProvider adViewProvider) {
+ this.contentMediaSource = contentMediaSource;
+ this.adMediaSourceFactory = adMediaSourceFactory;
+ this.adsLoader = adsLoader;
+ this.adViewProvider = adViewProvider;
+ mainHandler = new Handler(Looper.getMainLooper());
+ maskingMediaPeriodByAdMediaSource = new HashMap<>();
+ period = new Timeline.Period();
+ adGroupMediaSources = new MediaSource[0][];
+ adGroupTimelines = new Timeline[0][];
+ adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes());
+ }
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return contentMediaSource.getTag();
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ ComponentListener componentListener = new ComponentListener();
+ this.componentListener = componentListener;
+ prepareChildSource(DUMMY_CONTENT_MEDIA_PERIOD_ID, contentMediaSource);
+ mainHandler.post(() -> adsLoader.start(componentListener, adViewProvider));
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ AdPlaybackState adPlaybackState = Assertions.checkNotNull(this.adPlaybackState);
+ if (adPlaybackState.adGroupCount > 0 && id.isAd()) {
+ int adGroupIndex = id.adGroupIndex;
+ int adIndexInAdGroup = id.adIndexInAdGroup;
+ Uri adUri =
+ Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]);
+ if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {
+ int adCount = adIndexInAdGroup + 1;
+ adGroupMediaSources[adGroupIndex] =
+ Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);
+ adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount);
+ }
+ MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];
+ if (mediaSource == null) {
+ mediaSource = adMediaSourceFactory.createMediaSource(adUri);
+ adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource;
+ maskingMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>());
+ prepareChildSource(id, mediaSource);
+ }
+ MaskingMediaPeriod maskingMediaPeriod =
+ new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs);
+ maskingMediaPeriod.setPrepareErrorListener(
+ new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup));
+ List<MaskingMediaPeriod> mediaPeriods = maskingMediaPeriodByAdMediaSource.get(mediaSource);
+ if (mediaPeriods == null) {
+ Object periodUid =
+ Assertions.checkNotNull(adGroupTimelines[adGroupIndex][adIndexInAdGroup])
+ .getUidOfPeriod(/* periodIndex= */ 0);
+ MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber);
+ maskingMediaPeriod.createPeriod(adSourceMediaPeriodId);
+ } else {
+ // Keep track of the masking media period so it can be populated with the real media period
+ // when the source's info becomes available.
+ mediaPeriods.add(maskingMediaPeriod);
+ }
+ return maskingMediaPeriod;
+ } else {
+ MaskingMediaPeriod mediaPeriod =
+ new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs);
+ mediaPeriod.createPeriod(id);
+ return mediaPeriod;
+ }
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod;
+ List<MaskingMediaPeriod> mediaPeriods =
+ maskingMediaPeriodByAdMediaSource.get(maskingMediaPeriod.mediaSource);
+ if (mediaPeriods != null) {
+ mediaPeriods.remove(maskingMediaPeriod);
+ }
+ maskingMediaPeriod.releasePeriod();
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ super.releaseSourceInternal();
+ Assertions.checkNotNull(componentListener).release();
+ componentListener = null;
+ maskingMediaPeriodByAdMediaSource.clear();
+ contentTimeline = null;
+ adPlaybackState = null;
+ adGroupMediaSources = new MediaSource[0][];
+ adGroupTimelines = new Timeline[0][];
+ mainHandler.post(adsLoader::stop);
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ MediaPeriodId mediaPeriodId, MediaSource mediaSource, Timeline timeline) {
+ if (mediaPeriodId.isAd()) {
+ int adGroupIndex = mediaPeriodId.adGroupIndex;
+ int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup;
+ onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline);
+ } else {
+ onContentSourceInfoRefreshed(timeline);
+ }
+ }
+
+ @Override
+ protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
+ MediaPeriodId childId, MediaPeriodId mediaPeriodId) {
+ // The child id for the content period is just DUMMY_CONTENT_MEDIA_PERIOD_ID. That's why we need
+ // to forward the reported mediaPeriodId in this case.
+ return childId.isAd() ? childId : mediaPeriodId;
+ }
+
+ // Internal methods.
+
+ private void onAdPlaybackState(AdPlaybackState adPlaybackState) {
+ if (this.adPlaybackState == null) {
+ adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][];
+ Arrays.fill(adGroupMediaSources, new MediaSource[0]);
+ adGroupTimelines = new Timeline[adPlaybackState.adGroupCount][];
+ Arrays.fill(adGroupTimelines, new Timeline[0]);
+ }
+ this.adPlaybackState = adPlaybackState;
+ maybeUpdateSourceInfo();
+ }
+
+ private void onContentSourceInfoRefreshed(Timeline timeline) {
+ Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ contentTimeline = timeline;
+ maybeUpdateSourceInfo();
+ }
+
+ private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex,
+ int adIndexInAdGroup, Timeline timeline) {
+ Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline;
+ List<MaskingMediaPeriod> mediaPeriods = maskingMediaPeriodByAdMediaSource.remove(mediaSource);
+ if (mediaPeriods != null) {
+ Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
+ for (int i = 0; i < mediaPeriods.size(); i++) {
+ MaskingMediaPeriod mediaPeriod = mediaPeriods.get(i);
+ MediaPeriodId adSourceMediaPeriodId =
+ new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber);
+ mediaPeriod.createPeriod(adSourceMediaPeriodId);
+ }
+ }
+ maybeUpdateSourceInfo();
+ }
+
+ private void maybeUpdateSourceInfo() {
+ Timeline contentTimeline = this.contentTimeline;
+ if (adPlaybackState != null && contentTimeline != null) {
+ adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period));
+ Timeline timeline =
+ adPlaybackState.adGroupCount == 0
+ ? contentTimeline
+ : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState);
+ refreshSourceInfo(timeline);
+ }
+ }
+
+ private static long[][] getAdDurations(
+ @NullableType Timeline[][] adTimelines, Timeline.Period period) {
+ long[][] adDurations = new long[adTimelines.length][];
+ for (int i = 0; i < adTimelines.length; i++) {
+ adDurations[i] = new long[adTimelines[i].length];
+ for (int j = 0; j < adTimelines[i].length; j++) {
+ adDurations[i][j] =
+ adTimelines[i][j] == null
+ ? C.TIME_UNSET
+ : adTimelines[i][j].getPeriod(/* periodIndex= */ 0, period).getDurationUs();
+ }
+ }
+ return adDurations;
+ }
+
+ /** Listener for component events. All methods are called on the main thread. */
+ private final class ComponentListener implements AdsLoader.EventListener {
+
+ private final Handler playerHandler;
+
+ private volatile boolean released;
+
+ /**
+ * Creates new listener which forwards ad playback states on the creating thread and all other
+ * events on the external event listener thread.
+ */
+ public ComponentListener() {
+ playerHandler = new Handler();
+ }
+
+ /** Releases the component listener. */
+ public void release() {
+ released = true;
+ playerHandler.removeCallbacksAndMessages(null);
+ }
+
+ @Override
+ public void onAdPlaybackState(final AdPlaybackState adPlaybackState) {
+ if (released) {
+ return;
+ }
+ playerHandler.post(
+ () -> {
+ if (released) {
+ return;
+ }
+ AdsMediaSource.this.onAdPlaybackState(adPlaybackState);
+ });
+ }
+
+ @Override
+ public void onAdLoadError(final AdLoadException error, DataSpec dataSpec) {
+ if (released) {
+ return;
+ }
+ createEventDispatcher(/* mediaPeriodId= */ null)
+ .loadError(
+ dataSpec,
+ dataSpec.uri,
+ /* responseHeaders= */ Collections.emptyMap(),
+ C.DATA_TYPE_AD,
+ C.TRACK_TYPE_UNKNOWN,
+ /* loadDurationMs= */ 0,
+ /* bytesLoaded= */ 0,
+ error,
+ /* wasCanceled= */ true);
+ }
+ }
+
+ private final class AdPrepareErrorListener implements MaskingMediaPeriod.PrepareErrorListener {
+
+ private final Uri adUri;
+ private final int adGroupIndex;
+ private final int adIndexInAdGroup;
+
+ public AdPrepareErrorListener(Uri adUri, int adGroupIndex, int adIndexInAdGroup) {
+ this.adUri = adUri;
+ this.adGroupIndex = adGroupIndex;
+ this.adIndexInAdGroup = adIndexInAdGroup;
+ }
+
+ @Override
+ public void onPrepareError(MediaPeriodId mediaPeriodId, final IOException exception) {
+ createEventDispatcher(mediaPeriodId)
+ .loadError(
+ new DataSpec(adUri),
+ adUri,
+ /* responseHeaders= */ Collections.emptyMap(),
+ C.DATA_TYPE_AD,
+ C.TRACK_TYPE_UNKNOWN,
+ /* loadDurationMs= */ 0,
+ /* bytesLoaded= */ 0,
+ AdLoadException.createForAd(exception),
+ /* wasCanceled= */ true);
+ mainHandler.post(
+ () -> adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception));
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java
new file mode 100644
index 0000000000..44f6d0bc66
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 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.ads;
+
+import androidx.annotation.VisibleForTesting;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ForwardingTimeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/** A {@link Timeline} for sources that have ads. */
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+public final class SinglePeriodAdTimeline extends ForwardingTimeline {
+
+ private final AdPlaybackState adPlaybackState;
+
+ /**
+ * Creates a new timeline with a single period containing ads.
+ *
+ * @param contentTimeline The timeline of the content alongside which ads will be played. It must
+ * have one window and one period.
+ * @param adPlaybackState The state of the period's ads.
+ */
+ public SinglePeriodAdTimeline(Timeline contentTimeline, AdPlaybackState adPlaybackState) {
+ super(contentTimeline);
+ Assertions.checkState(contentTimeline.getPeriodCount() == 1);
+ Assertions.checkState(contentTimeline.getWindowCount() == 1);
+ this.adPlaybackState = adPlaybackState;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ timeline.getPeriod(periodIndex, period, setIds);
+ period.set(
+ period.id,
+ period.uid,
+ period.windowIndex,
+ period.durationUs,
+ period.getPositionInWindowUs(),
+ adPlaybackState);
+ return period;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ window = super.getWindow(windowIndex, window, defaultPositionProjectionUs);
+ if (window.durationUs == C.TIME_UNSET) {
+ window.durationUs = adPlaybackState.contentDurationUs;
+ }
+ return window;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
new file mode 100644
index 0000000000..406cd1617a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
@@ -0,0 +1,100 @@
+/*
+ * 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.chunk;
+
+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.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+
+/**
+ * A base implementation of {@link MediaChunk} that outputs to a {@link BaseMediaChunkOutput}.
+ */
+public abstract class BaseMediaChunk extends MediaChunk {
+
+ /**
+ * The time from which output will begin, or {@link C#TIME_UNSET} if output will begin from the
+ * start of the chunk.
+ */
+ public final long clippedStartTimeUs;
+ /**
+ * The time from which output will end, or {@link C#TIME_UNSET} if output will end at the end of
+ * the chunk.
+ */
+ public final long clippedEndTimeUs;
+
+ private BaseMediaChunkOutput output;
+ private int[] firstSampleIndices;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link
+ * C#TIME_UNSET} to output from the start of the chunk.
+ * @param clippedEndTimeUs The time in the chunk from which output will end, or {@link
+ * C#TIME_UNSET} to output to the end of the chunk.
+ * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
+ */
+ public BaseMediaChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs,
+ long clippedStartTimeUs,
+ long clippedEndTimeUs,
+ long chunkIndex) {
+ super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
+ endTimeUs, chunkIndex);
+ this.clippedStartTimeUs = clippedStartTimeUs;
+ this.clippedEndTimeUs = clippedEndTimeUs;
+ }
+
+ /**
+ * Initializes the chunk for loading, setting the {@link BaseMediaChunkOutput} that will receive
+ * samples as they are loaded.
+ *
+ * @param output The output that will receive the loaded media samples.
+ */
+ public void init(BaseMediaChunkOutput output) {
+ this.output = output;
+ firstSampleIndices = output.getWriteIndices();
+ }
+
+ /**
+ * Returns the index of the first sample in the specified track of the output that will originate
+ * from this chunk.
+ */
+ public final int getFirstSampleIndex(int trackIndex) {
+ return firstSampleIndices[trackIndex];
+ }
+
+ /**
+ * Returns the output most recently passed to {@link #init(BaseMediaChunkOutput)}.
+ */
+ protected final BaseMediaChunkOutput getOutput() {
+ return output;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java
new file mode 100644
index 0000000000..3987260578
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java
@@ -0,0 +1,75 @@
+/*
+ * 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.chunk;
+
+import java.util.NoSuchElementException;
+
+/**
+ * Base class for {@link MediaChunkIterator}s. Handles {@link #next()} and {@link #isEnded()}, and
+ * provides a bounds check for child classes.
+ */
+public abstract class BaseMediaChunkIterator implements MediaChunkIterator {
+
+ private final long fromIndex;
+ private final long toIndex;
+
+ private long currentIndex;
+
+ /**
+ * Creates base iterator.
+ *
+ * @param fromIndex The first available index.
+ * @param toIndex The last available index.
+ */
+ @SuppressWarnings("method.invocation.invalid")
+ public BaseMediaChunkIterator(long fromIndex, long toIndex) {
+ this.fromIndex = fromIndex;
+ this.toIndex = toIndex;
+ reset();
+ }
+
+ @Override
+ public boolean isEnded() {
+ return currentIndex > toIndex;
+ }
+
+ @Override
+ public boolean next() {
+ currentIndex++;
+ return !isEnded();
+ }
+
+ @Override
+ public void reset() {
+ currentIndex = fromIndex - 1;
+ }
+
+ /**
+ * Verifies that the iterator points to a valid element.
+ *
+ * @throws NoSuchElementException If the iterator does not point to a valid element.
+ */
+ protected final void checkInBounds() {
+ if (currentIndex < fromIndex || currentIndex > toIndex) {
+ throw new NoSuchElementException();
+ }
+ }
+
+ /** Returns the current index this iterator is pointing to. */
+ protected final long getCurrentIndex() {
+ return currentIndex;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
new file mode 100644
index 0000000000..5d1f93bf01
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 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.chunk;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+
+/**
+ * A {@link TrackOutputProvider} that provides {@link TrackOutput TrackOutputs} based on a
+ * predefined mapping from track type to output.
+ */
+public final class BaseMediaChunkOutput implements TrackOutputProvider {
+
+ private static final String TAG = "BaseMediaChunkOutput";
+
+ private final int[] trackTypes;
+ private final SampleQueue[] sampleQueues;
+
+ /**
+ * @param trackTypes The track types of the individual track outputs.
+ * @param sampleQueues The individual sample queues.
+ */
+ public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) {
+ this.trackTypes = trackTypes;
+ this.sampleQueues = sampleQueues;
+ }
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ for (int i = 0; i < trackTypes.length; i++) {
+ if (type == trackTypes[i]) {
+ return sampleQueues[i];
+ }
+ }
+ Log.e(TAG, "Unmatched track of type: " + type);
+ return new DummyTrackOutput();
+ }
+
+ /**
+ * Returns the current absolute write indices of the individual sample queues.
+ */
+ public int[] getWriteIndices() {
+ int[] writeIndices = new int[sampleQueues.length];
+ for (int i = 0; i < sampleQueues.length; i++) {
+ if (sampleQueues[i] != null) {
+ writeIndices[i] = sampleQueues[i].getWriteIndex();
+ }
+ }
+ return writeIndices;
+ }
+
+ /**
+ * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
+ * subsequently written to the sample queues.
+ */
+ public void setSampleOffsetUs(long sampleOffsetUs) {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ if (sampleQueue != null) {
+ sampleQueue.setSampleOffsetUs(sampleOffsetUs);
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java
new file mode 100644
index 0000000000..3f4450eddd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java
@@ -0,0 +1,137 @@
+/*
+ * 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.chunk;
+
+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.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.StatsDataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An abstract base class for {@link Loadable} implementations that load chunks of data required
+ * for the playback of streams.
+ */
+public abstract class Chunk implements Loadable {
+
+ /**
+ * The {@link DataSpec} that defines the data to be loaded.
+ */
+ public final DataSpec dataSpec;
+ /**
+ * The type of the chunk. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For
+ * reporting only.
+ */
+ public final int type;
+ /**
+ * The format of the track to which this chunk belongs, or null if the chunk does not belong to
+ * a track.
+ */
+ public final Format trackFormat;
+ /**
+ * One of the {@link C} {@code SELECTION_REASON_*} constants if the chunk belongs to a track.
+ * {@link C#SELECTION_REASON_UNKNOWN} if the chunk does not belong to a track.
+ */
+ public final int trackSelectionReason;
+ /**
+ * Optional data associated with the selection of the track to which this chunk belongs. Null if
+ * the chunk does not belong to a track.
+ */
+ @Nullable public final Object trackSelectionData;
+ /**
+ * The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data
+ * being loaded does not contain media samples.
+ */
+ public final long startTimeUs;
+ /**
+ * The end time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data being
+ * loaded does not contain media samples.
+ */
+ public final long endTimeUs;
+
+ protected final StatsDataSource dataSource;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param type See {@link #type}.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs See {@link #startTimeUs}.
+ * @param endTimeUs See {@link #endTimeUs}.
+ */
+ public Chunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ int type,
+ Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs) {
+ this.dataSource = new StatsDataSource(dataSource);
+ this.dataSpec = Assertions.checkNotNull(dataSpec);
+ this.type = type;
+ this.trackFormat = trackFormat;
+ this.trackSelectionReason = trackSelectionReason;
+ this.trackSelectionData = trackSelectionData;
+ this.startTimeUs = startTimeUs;
+ this.endTimeUs = endTimeUs;
+ }
+
+ /**
+ * Returns the duration of the chunk in microseconds.
+ */
+ public final long getDurationUs() {
+ return endTimeUs - startTimeUs;
+ }
+
+ /**
+ * Returns the number of bytes that have been loaded. Must only be called after the load
+ * completed, failed, or was canceled.
+ */
+ public final long bytesLoaded() {
+ return dataSource.getBytesRead();
+ }
+
+ /**
+ * Returns the {@link Uri} associated with the last {@link DataSource#open} call. If redirection
+ * occurred, this is the redirected uri. Must only be called after the load completed, failed, or
+ * was canceled.
+ *
+ * @see DataSource#getUri()
+ */
+ public final Uri getUri() {
+ return dataSource.getLastOpenedUri();
+ }
+
+ /**
+ * Returns the response headers associated with the last {@link DataSource#open} call. Must only
+ * be called after the load completed, failed, or was canceled.
+ *
+ * @see DataSource#getResponseHeaders()
+ */
+ public final Map<String, List<String>> getResponseHeaders() {
+ return dataSource.getLastResponseHeaders();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
new file mode 100644
index 0000000000..04cef9198c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
@@ -0,0 +1,220 @@
+/*
+ * 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.chunk;
+
+import android.util.SparseArray;
+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.extractor.DummyTrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * An {@link Extractor} wrapper for loading chunks that contain a single primary track, and possibly
+ * additional embedded tracks.
+ * <p>
+ * The wrapper allows switching of the {@link TrackOutput}s that receive parsed data.
+ */
+public final class ChunkExtractorWrapper implements ExtractorOutput {
+
+ /**
+ * Provides {@link TrackOutput} instances to be written to by the wrapper.
+ */
+ public interface TrackOutputProvider {
+
+ /**
+ * Called to get the {@link TrackOutput} for a specific track.
+ * <p>
+ * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
+ *
+ * @param id A track identifier.
+ * @param type The type of the track. Typically one of the
+ * {@link org.mozilla.thirdparty.com.google.android.exoplayer2C} {@code TRACK_TYPE_*} constants.
+ * @return The {@link TrackOutput} for the given track identifier.
+ */
+ TrackOutput track(int id, int type);
+
+ }
+
+ public final Extractor extractor;
+
+ private final int primaryTrackType;
+ private final Format primaryTrackManifestFormat;
+ private final SparseArray<BindingTrackOutput> bindingTrackOutputs;
+
+ private boolean extractorInitialized;
+ private TrackOutputProvider trackOutputProvider;
+ private long endTimeUs;
+ private SeekMap seekMap;
+ private Format[] sampleFormats;
+
+ /**
+ * @param extractor The extractor to wrap.
+ * @param primaryTrackType The type of the primary track. Typically one of the
+ * {@link org.mozilla.thirdparty.com.google.android.exoplayer2C} {@code TRACK_TYPE_*} constants.
+ * @param primaryTrackManifestFormat A manifest defined {@link Format} whose data should be merged
+ * into any sample {@link Format} output from the {@link Extractor} for the primary track.
+ */
+ public ChunkExtractorWrapper(Extractor extractor, int primaryTrackType,
+ Format primaryTrackManifestFormat) {
+ this.extractor = extractor;
+ this.primaryTrackType = primaryTrackType;
+ this.primaryTrackManifestFormat = primaryTrackManifestFormat;
+ bindingTrackOutputs = new SparseArray<>();
+ }
+
+ /**
+ * Returns the {@link SeekMap} most recently output by the extractor, or null.
+ */
+ public SeekMap getSeekMap() {
+ return seekMap;
+ }
+
+ /**
+ * Returns the sample {@link Format}s most recently output by the extractor, or null.
+ */
+ public Format[] getSampleFormats() {
+ return sampleFormats;
+ }
+
+ /**
+ * Initializes the wrapper to output to {@link TrackOutput}s provided by the specified {@link
+ * TrackOutputProvider}, and configures the extractor to receive data from a new chunk.
+ *
+ * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data.
+ * @param startTimeUs The start position in the new chunk, or {@link C#TIME_UNSET} to output
+ * samples from the start of the chunk.
+ * @param endTimeUs The end position in the new chunk, or {@link C#TIME_UNSET} to output samples
+ * to the end of the chunk.
+ */
+ public void init(
+ @Nullable TrackOutputProvider trackOutputProvider, long startTimeUs, long endTimeUs) {
+ this.trackOutputProvider = trackOutputProvider;
+ this.endTimeUs = endTimeUs;
+ if (!extractorInitialized) {
+ extractor.init(this);
+ if (startTimeUs != C.TIME_UNSET) {
+ extractor.seek(/* position= */ 0, startTimeUs);
+ }
+ extractorInitialized = true;
+ } else {
+ extractor.seek(/* position= */ 0, startTimeUs == C.TIME_UNSET ? 0 : startTimeUs);
+ for (int i = 0; i < bindingTrackOutputs.size(); i++) {
+ bindingTrackOutputs.valueAt(i).bind(trackOutputProvider, endTimeUs);
+ }
+ }
+ }
+
+ // ExtractorOutput implementation.
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ BindingTrackOutput bindingTrackOutput = bindingTrackOutputs.get(id);
+ if (bindingTrackOutput == null) {
+ // Assert that if we're seeing a new track we have not seen endTracks.
+ Assertions.checkState(sampleFormats == null);
+ // TODO: Manifest formats for embedded tracks should also be passed here.
+ bindingTrackOutput = new BindingTrackOutput(id, type,
+ type == primaryTrackType ? primaryTrackManifestFormat : null);
+ bindingTrackOutput.bind(trackOutputProvider, endTimeUs);
+ bindingTrackOutputs.put(id, bindingTrackOutput);
+ }
+ return bindingTrackOutput;
+ }
+
+ @Override
+ public void endTracks() {
+ Format[] sampleFormats = new Format[bindingTrackOutputs.size()];
+ for (int i = 0; i < bindingTrackOutputs.size(); i++) {
+ sampleFormats[i] = bindingTrackOutputs.valueAt(i).sampleFormat;
+ }
+ this.sampleFormats = sampleFormats;
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ this.seekMap = seekMap;
+ }
+
+ // Internal logic.
+
+ private static final class BindingTrackOutput implements TrackOutput {
+
+ private final int id;
+ private final int type;
+ private final Format manifestFormat;
+ private final DummyTrackOutput dummyTrackOutput;
+
+ public Format sampleFormat;
+ private TrackOutput trackOutput;
+ private long endTimeUs;
+
+ public BindingTrackOutput(int id, int type, Format manifestFormat) {
+ this.id = id;
+ this.type = type;
+ this.manifestFormat = manifestFormat;
+ dummyTrackOutput = new DummyTrackOutput();
+ }
+
+ public void bind(TrackOutputProvider trackOutputProvider, long endTimeUs) {
+ if (trackOutputProvider == null) {
+ trackOutput = dummyTrackOutput;
+ return;
+ }
+ this.endTimeUs = endTimeUs;
+ trackOutput = trackOutputProvider.track(id, type);
+ if (sampleFormat != null) {
+ trackOutput.format(sampleFormat);
+ }
+ }
+
+ @Override
+ public void format(Format format) {
+ sampleFormat = manifestFormat != null ? format.copyWithManifestFormatInfo(manifestFormat)
+ : format;
+ trackOutput.format(sampleFormat);
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ return trackOutput.sampleData(input, length, allowEndOfInput);
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray data, int length) {
+ trackOutput.sampleData(data, length);
+ }
+
+ @Override
+ public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+ CryptoData cryptoData) {
+ if (endTimeUs != C.TIME_UNSET && timeUs >= endTimeUs) {
+ trackOutput = dummyTrackOutput;
+ }
+ trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java
new file mode 100644
index 0000000000..ef9daddd2c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java
@@ -0,0 +1,41 @@
+/*
+ * 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.chunk;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Holds a chunk or an indication that the end of the stream has been reached.
+ */
+public final class ChunkHolder {
+
+ /** The chunk. */
+ @Nullable public Chunk chunk;
+
+ /**
+ * Indicates that the end of the stream has been reached.
+ */
+ public boolean endOfStream;
+
+ /**
+ * Clears the holder.
+ */
+ public void clear() {
+ chunk = null;
+ endOfStream = false;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
new file mode 100644
index 0000000000..a789805cd7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -0,0 +1,791 @@
+/*
+ * 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.chunk;
+
+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.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SequenceableLoader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+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.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}.
+ * May also be configured to expose additional embedded {@link SampleStream}s.
+ */
+public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader,
+ Loader.Callback<Chunk>, Loader.ReleaseCallback {
+
+ /** A callback to be notified when a sample stream has finished being released. */
+ public interface ReleaseCallback<T extends ChunkSource> {
+
+ /**
+ * Called when the {@link ChunkSampleStream} has finished being released.
+ *
+ * @param chunkSampleStream The released sample stream.
+ */
+ void onSampleStreamReleased(ChunkSampleStream<T> chunkSampleStream);
+ }
+
+ private static final String TAG = "ChunkSampleStream";
+
+ public final int primaryTrackType;
+
+ @Nullable private final int[] embeddedTrackTypes;
+ @Nullable private final Format[] embeddedTrackFormats;
+ private final boolean[] embeddedTracksSelected;
+ private final T chunkSource;
+ private final SequenceableLoader.Callback<ChunkSampleStream<T>> callback;
+ private final EventDispatcher eventDispatcher;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final Loader loader;
+ private final ChunkHolder nextChunkHolder;
+ private final ArrayList<BaseMediaChunk> mediaChunks;
+ private final List<BaseMediaChunk> readOnlyMediaChunks;
+ private final SampleQueue primarySampleQueue;
+ private final SampleQueue[] embeddedSampleQueues;
+ private final BaseMediaChunkOutput chunkOutput;
+
+ private Format primaryDownstreamTrackFormat;
+ @Nullable private ReleaseCallback<T> releaseCallback;
+ private long pendingResetPositionUs;
+ private long lastSeekPositionUs;
+ private int nextNotifyPrimaryFormatMediaChunkIndex;
+
+ /* package */ long decodeOnlyUntilPositionUs;
+ /* package */ boolean loadingFinished;
+
+ /**
+ * Constructs an instance.
+ *
+ * @param primaryTrackType The type of the primary track. One of the {@link C} {@code
+ * TRACK_TYPE_*} constants.
+ * @param embeddedTrackTypes The types of any embedded tracks, or null.
+ * @param embeddedTrackFormats The formats of the embedded tracks, or null.
+ * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.
+ * @param callback An {@link Callback} for the stream.
+ * @param allocator An {@link Allocator} from which allocations can be obtained.
+ * @param positionUs The position from which to start loading media.
+ * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions}
+ * from.
+ * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
+ * @param eventDispatcher A dispatcher to notify of events.
+ */
+ public ChunkSampleStream(
+ int primaryTrackType,
+ @Nullable int[] embeddedTrackTypes,
+ @Nullable Format[] embeddedTrackFormats,
+ T chunkSource,
+ Callback<ChunkSampleStream<T>> callback,
+ Allocator allocator,
+ long positionUs,
+ DrmSessionManager<?> drmSessionManager,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ EventDispatcher eventDispatcher) {
+ this.primaryTrackType = primaryTrackType;
+ this.embeddedTrackTypes = embeddedTrackTypes;
+ this.embeddedTrackFormats = embeddedTrackFormats;
+ this.chunkSource = chunkSource;
+ this.callback = callback;
+ this.eventDispatcher = eventDispatcher;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ loader = new Loader("Loader:ChunkSampleStream");
+ nextChunkHolder = new ChunkHolder();
+ mediaChunks = new ArrayList<>();
+ readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
+
+ int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length;
+ embeddedSampleQueues = new SampleQueue[embeddedTrackCount];
+ embeddedTracksSelected = new boolean[embeddedTrackCount];
+ int[] trackTypes = new int[1 + embeddedTrackCount];
+ SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount];
+
+ primarySampleQueue = new SampleQueue(allocator, drmSessionManager);
+ trackTypes[0] = primaryTrackType;
+ sampleQueues[0] = primarySampleQueue;
+
+ for (int i = 0; i < embeddedTrackCount; i++) {
+ SampleQueue sampleQueue =
+ new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager());
+ embeddedSampleQueues[i] = sampleQueue;
+ sampleQueues[i + 1] = sampleQueue;
+ trackTypes[i + 1] = embeddedTrackTypes[i];
+ }
+
+ chunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues);
+ pendingResetPositionUs = positionUs;
+ lastSeekPositionUs = positionUs;
+ }
+
+ /**
+ * Discards buffered media up to the specified position.
+ *
+ * @param positionUs The position to discard up to, in microseconds.
+ * @param toKeyframe If true then for each track discards samples up to the keyframe before or at
+ * the specified position, rather than any sample before or at that position.
+ */
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ if (isPendingReset()) {
+ return;
+ }
+ int oldFirstSampleIndex = primarySampleQueue.getFirstIndex();
+ primarySampleQueue.discardTo(positionUs, toKeyframe, true);
+ int newFirstSampleIndex = primarySampleQueue.getFirstIndex();
+ if (newFirstSampleIndex > oldFirstSampleIndex) {
+ long discardToUs = primarySampleQueue.getFirstTimestampUs();
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ embeddedSampleQueues[i].discardTo(discardToUs, toKeyframe, embeddedTracksSelected[i]);
+ }
+ }
+ discardDownstreamMediaChunks(newFirstSampleIndex);
+ }
+
+ /**
+ * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's
+ * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned
+ * stream when the track is no longer required, and before calling this method again to obtain
+ * another stream for the same track.
+ *
+ * @param positionUs The current playback position in microseconds.
+ * @param trackType The type of the embedded track to enable.
+ * @return The {@link EmbeddedSampleStream} for the embedded track.
+ */
+ public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) {
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ if (embeddedTrackTypes[i] == trackType) {
+ Assertions.checkState(!embeddedTracksSelected[i]);
+ embeddedTracksSelected[i] = true;
+ embeddedSampleQueues[i].seekTo(positionUs, /* allowTimeBeyondBuffer= */ true);
+ return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i);
+ }
+ }
+ // Should never happen.
+ throw new IllegalStateException();
+ }
+
+ /**
+ * Returns the {@link ChunkSource} used by this stream.
+ */
+ public T getChunkSource() {
+ return chunkSource;
+ }
+
+ /**
+ * Returns an estimate of the position up to which data is buffered.
+ *
+ * @return An estimate of the absolute position in microseconds up to which data is buffered, or
+ * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
+ */
+ @Override
+ public long getBufferedPositionUs() {
+ if (loadingFinished) {
+ return C.TIME_END_OF_SOURCE;
+ } else if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ long bufferedPositionUs = lastSeekPositionUs;
+ BaseMediaChunk lastMediaChunk = getLastMediaChunk();
+ BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk
+ : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null;
+ if (lastCompletedMediaChunk != null) {
+ bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);
+ }
+ return Math.max(bufferedPositionUs, primarySampleQueue.getLargestQueuedTimestampUs());
+ }
+ }
+
+ /**
+ * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used
+ * as sync points.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @param seekParameters Parameters that control how the seek is performed.
+ * @return The adjusted seek position, in microseconds.
+ */
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters);
+ }
+
+ /**
+ * Seeks to the specified position in microseconds.
+ *
+ * @param positionUs The seek position in microseconds.
+ */
+ public void seekToUs(long positionUs) {
+ lastSeekPositionUs = positionUs;
+ if (isPendingReset()) {
+ // A reset is already pending. We only need to update its position.
+ pendingResetPositionUs = positionUs;
+ return;
+ }
+
+ // Detect whether the seek is to the start of a chunk that's at least partially buffered.
+ BaseMediaChunk seekToMediaChunk = null;
+ for (int i = 0; i < mediaChunks.size(); i++) {
+ BaseMediaChunk mediaChunk = mediaChunks.get(i);
+ long mediaChunkStartTimeUs = mediaChunk.startTimeUs;
+ if (mediaChunkStartTimeUs == positionUs && mediaChunk.clippedStartTimeUs == C.TIME_UNSET) {
+ seekToMediaChunk = mediaChunk;
+ break;
+ } else if (mediaChunkStartTimeUs > positionUs) {
+ // We're not going to find a chunk with a matching start time.
+ break;
+ }
+ }
+
+ // See if we can seek inside the primary sample queue.
+ boolean seekInsideBuffer;
+ if (seekToMediaChunk != null) {
+ // When seeking to the start of a chunk we use the index of the first sample in the chunk
+ // rather than the seek position. This ensures we seek to the keyframe at the start of the
+ // chunk even if the sample timestamps are slightly offset from the chunk start times.
+ seekInsideBuffer = primarySampleQueue.seekTo(seekToMediaChunk.getFirstSampleIndex(0));
+ decodeOnlyUntilPositionUs = 0;
+ } else {
+ seekInsideBuffer =
+ primarySampleQueue.seekTo(
+ positionUs, /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs());
+ decodeOnlyUntilPositionUs = lastSeekPositionUs;
+ }
+
+ if (seekInsideBuffer) {
+ // We can seek inside the buffer.
+ nextNotifyPrimaryFormatMediaChunkIndex =
+ primarySampleIndexToMediaChunkIndex(
+ primarySampleQueue.getReadIndex(), /* minChunkIndex= */ 0);
+ // Seek the embedded sample queues.
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true);
+ }
+ } else {
+ // We can't seek inside the buffer, and so need to reset.
+ pendingResetPositionUs = positionUs;
+ loadingFinished = false;
+ mediaChunks.clear();
+ nextNotifyPrimaryFormatMediaChunkIndex = 0;
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ } else {
+ loader.clearFatalError();
+ primarySampleQueue.reset();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
+ }
+ }
+ }
+ }
+
+ /**
+ * Releases the stream.
+ *
+ * <p>This method should be called when the stream is no longer required. Either this method or
+ * {@link #release(ReleaseCallback)} can be used to release this stream.
+ */
+ public void release() {
+ release(null);
+ }
+
+ /**
+ * Releases the stream.
+ *
+ * <p>This method should be called when the stream is no longer required. Either this method or
+ * {@link #release()} can be used to release this stream.
+ *
+ * @param callback An optional callback to be called on the loading thread once the loader has
+ * been released.
+ */
+ public void release(@Nullable ReleaseCallback<T> callback) {
+ this.releaseCallback = callback;
+ // Discard as much as we can synchronously.
+ primarySampleQueue.preRelease();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.preRelease();
+ }
+ loader.release(this);
+ }
+
+ @Override
+ public void onLoaderReleased() {
+ primarySampleQueue.release();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.release();
+ }
+ if (releaseCallback != null) {
+ releaseCallback.onSampleStreamReleased(this);
+ }
+ }
+
+ // SampleStream implementation.
+
+ @Override
+ public boolean isReady() {
+ return !isPendingReset() && primarySampleQueue.isReady(loadingFinished);
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ loader.maybeThrowError();
+ primarySampleQueue.maybeThrowError();
+ if (!loader.isLoading()) {
+ chunkSource.maybeThrowError();
+ }
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ if (isPendingReset()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ maybeNotifyPrimaryTrackFormatChanged();
+
+ return primarySampleQueue.read(
+ formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs);
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ if (isPendingReset()) {
+ return 0;
+ }
+ int skipCount;
+ if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) {
+ skipCount = primarySampleQueue.advanceToEnd();
+ } else {
+ skipCount = primarySampleQueue.advanceTo(positionUs);
+ }
+ maybeNotifyPrimaryTrackFormatChanged();
+ return skipCount;
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
+ chunkSource.onChunkLoadCompleted(loadable);
+ eventDispatcher.loadCompleted(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ loadable.type,
+ primaryTrackType,
+ loadable.trackFormat,
+ loadable.trackSelectionReason,
+ loadable.trackSelectionData,
+ loadable.startTimeUs,
+ loadable.endTimeUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded());
+ callback.onContinueLoadingRequested(this);
+ }
+
+ @Override
+ public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+ boolean released) {
+ eventDispatcher.loadCanceled(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ loadable.type,
+ primaryTrackType,
+ loadable.trackFormat,
+ loadable.trackSelectionReason,
+ loadable.trackSelectionData,
+ loadable.startTimeUs,
+ loadable.endTimeUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded());
+ if (!released) {
+ primarySampleQueue.reset();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
+ }
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+
+ @Override
+ public LoadErrorAction onLoadError(
+ Chunk loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ IOException error,
+ int errorCount) {
+ long bytesLoaded = loadable.bytesLoaded();
+ boolean isMediaChunk = isMediaChunk(loadable);
+ int lastChunkIndex = mediaChunks.size() - 1;
+ boolean cancelable =
+ bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex);
+ long blacklistDurationMs =
+ cancelable
+ ? loadErrorHandlingPolicy.getBlacklistDurationMsFor(
+ loadable.type, loadDurationMs, error, errorCount)
+ : C.TIME_UNSET;
+ LoadErrorAction loadErrorAction = null;
+ if (chunkSource.onChunkLoadError(loadable, cancelable, error, blacklistDurationMs)) {
+ if (cancelable) {
+ loadErrorAction = Loader.DONT_RETRY;
+ if (isMediaChunk) {
+ BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex);
+ Assertions.checkState(removed == loadable);
+ if (mediaChunks.isEmpty()) {
+ pendingResetPositionUs = lastSeekPositionUs;
+ }
+ }
+ } else {
+ Log.w(TAG, "Ignoring attempt to cancel non-cancelable load.");
+ }
+ }
+
+ if (loadErrorAction == null) {
+ // The load was not cancelled. Either the load must be retried or the error propagated.
+ long retryDelayMs =
+ loadErrorHandlingPolicy.getRetryDelayMsFor(
+ loadable.type, loadDurationMs, error, errorCount);
+ loadErrorAction =
+ retryDelayMs != C.TIME_UNSET
+ ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs)
+ : Loader.DONT_RETRY_FATAL;
+ }
+
+ boolean canceled = !loadErrorAction.isRetry();
+ eventDispatcher.loadError(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ loadable.type,
+ primaryTrackType,
+ loadable.trackFormat,
+ loadable.trackSelectionReason,
+ loadable.trackSelectionData,
+ loadable.startTimeUs,
+ loadable.endTimeUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ bytesLoaded,
+ error,
+ canceled);
+ if (canceled) {
+ callback.onContinueLoadingRequested(this);
+ }
+ return loadErrorAction;
+ }
+
+ // SequenceableLoader implementation
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (loadingFinished || loader.isLoading() || loader.hasFatalError()) {
+ return false;
+ }
+
+ boolean pendingReset = isPendingReset();
+ List<BaseMediaChunk> chunkQueue;
+ long loadPositionUs;
+ if (pendingReset) {
+ chunkQueue = Collections.emptyList();
+ loadPositionUs = pendingResetPositionUs;
+ } else {
+ chunkQueue = readOnlyMediaChunks;
+ loadPositionUs = getLastMediaChunk().endTimeUs;
+ }
+ chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder);
+ boolean endOfStream = nextChunkHolder.endOfStream;
+ Chunk loadable = nextChunkHolder.chunk;
+ nextChunkHolder.clear();
+
+ if (endOfStream) {
+ pendingResetPositionUs = C.TIME_UNSET;
+ loadingFinished = true;
+ return true;
+ }
+
+ if (loadable == null) {
+ return false;
+ }
+
+ if (isMediaChunk(loadable)) {
+ BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable;
+ if (pendingReset) {
+ boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs;
+ // Only enable setting of the decode only flag if we're not resetting to a chunk boundary.
+ decodeOnlyUntilPositionUs = resetToMediaChunk ? 0 : pendingResetPositionUs;
+ pendingResetPositionUs = C.TIME_UNSET;
+ }
+ mediaChunk.init(chunkOutput);
+ mediaChunks.add(mediaChunk);
+ } else if (loadable instanceof InitializationChunk) {
+ ((InitializationChunk) loadable).init(chunkOutput);
+ }
+ long elapsedRealtimeMs =
+ loader.startLoading(
+ loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));
+ eventDispatcher.loadStarted(
+ loadable.dataSpec,
+ loadable.type,
+ primaryTrackType,
+ loadable.trackFormat,
+ loadable.trackSelectionReason,
+ loadable.trackSelectionData,
+ loadable.startTimeUs,
+ loadable.endTimeUs,
+ elapsedRealtimeMs);
+ return true;
+ }
+
+ @Override
+ public boolean isLoading() {
+ return loader.isLoading();
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs;
+ }
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ if (loader.isLoading() || loader.hasFatalError() || isPendingReset()) {
+ return;
+ }
+
+ int currentQueueSize = mediaChunks.size();
+ int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
+ if (currentQueueSize <= preferredQueueSize) {
+ return;
+ }
+
+ int newQueueSize = currentQueueSize;
+ for (int i = preferredQueueSize; i < currentQueueSize; i++) {
+ if (!haveReadFromMediaChunk(i)) {
+ newQueueSize = i;
+ break;
+ }
+ }
+ if (newQueueSize == currentQueueSize) {
+ return;
+ }
+
+ long endTimeUs = getLastMediaChunk().endTimeUs;
+ BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize);
+ if (mediaChunks.isEmpty()) {
+ pendingResetPositionUs = lastSeekPositionUs;
+ }
+ loadingFinished = false;
+ eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs);
+ }
+
+ // Internal methods
+
+ private boolean isMediaChunk(Chunk chunk) {
+ return chunk instanceof BaseMediaChunk;
+ }
+
+ /** Returns whether samples have been read from media chunk at given index. */
+ private boolean haveReadFromMediaChunk(int mediaChunkIndex) {
+ BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex);
+ if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) {
+ return true;
+ }
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /* package */ boolean isPendingReset() {
+ return pendingResetPositionUs != C.TIME_UNSET;
+ }
+
+ private void discardDownstreamMediaChunks(int discardToSampleIndex) {
+ int discardToMediaChunkIndex =
+ primarySampleIndexToMediaChunkIndex(discardToSampleIndex, /* minChunkIndex= */ 0);
+ // Don't discard any chunks that we haven't reported the primary format change for yet.
+ discardToMediaChunkIndex =
+ Math.min(discardToMediaChunkIndex, nextNotifyPrimaryFormatMediaChunkIndex);
+ if (discardToMediaChunkIndex > 0) {
+ Util.removeRange(mediaChunks, /* fromIndex= */ 0, /* toIndex= */ discardToMediaChunkIndex);
+ nextNotifyPrimaryFormatMediaChunkIndex -= discardToMediaChunkIndex;
+ }
+ }
+
+ private void maybeNotifyPrimaryTrackFormatChanged() {
+ int readSampleIndex = primarySampleQueue.getReadIndex();
+ int notifyToMediaChunkIndex =
+ primarySampleIndexToMediaChunkIndex(
+ readSampleIndex, /* minChunkIndex= */ nextNotifyPrimaryFormatMediaChunkIndex - 1);
+ while (nextNotifyPrimaryFormatMediaChunkIndex <= notifyToMediaChunkIndex) {
+ maybeNotifyPrimaryTrackFormatChanged(nextNotifyPrimaryFormatMediaChunkIndex++);
+ }
+ }
+
+ private void maybeNotifyPrimaryTrackFormatChanged(int mediaChunkReadIndex) {
+ BaseMediaChunk currentChunk = mediaChunks.get(mediaChunkReadIndex);
+ Format trackFormat = currentChunk.trackFormat;
+ if (!trackFormat.equals(primaryDownstreamTrackFormat)) {
+ eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat,
+ currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+ currentChunk.startTimeUs);
+ }
+ primaryDownstreamTrackFormat = trackFormat;
+ }
+
+ /**
+ * Returns the media chunk index corresponding to a given primary sample index.
+ *
+ * @param primarySampleIndex The primary sample index for which the corresponding media chunk
+ * index is required.
+ * @param minChunkIndex A minimum chunk index from which to start searching, or -1 if no hint can
+ * be provided.
+ * @return The index of the media chunk corresponding to the sample index, or -1 if the list of
+ * media chunks is empty, or {@code minChunkIndex} if the sample precedes the first chunk in
+ * the search (i.e. the chunk at {@code minChunkIndex}, or at index 0 if {@code minChunkIndex}
+ * is -1.
+ */
+ private int primarySampleIndexToMediaChunkIndex(int primarySampleIndex, int minChunkIndex) {
+ for (int i = minChunkIndex + 1; i < mediaChunks.size(); i++) {
+ if (mediaChunks.get(i).getFirstSampleIndex(0) > primarySampleIndex) {
+ return i - 1;
+ }
+ }
+ return mediaChunks.size() - 1;
+ }
+
+ private BaseMediaChunk getLastMediaChunk() {
+ return mediaChunks.get(mediaChunks.size() - 1);
+ }
+
+ /**
+ * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample
+ * queues.
+ *
+ * @param chunkIndex The index of the first chunk to discard.
+ * @return The chunk at given index.
+ */
+ private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) {
+ BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex);
+ Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size());
+ nextNotifyPrimaryFormatMediaChunkIndex =
+ Math.max(nextNotifyPrimaryFormatMediaChunkIndex, mediaChunks.size());
+ primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0));
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1));
+ }
+ return firstRemovedChunk;
+ }
+
+ /**
+ * A {@link SampleStream} embedded in a {@link ChunkSampleStream}.
+ */
+ public final class EmbeddedSampleStream implements SampleStream {
+
+ public final ChunkSampleStream<T> parent;
+
+ private final SampleQueue sampleQueue;
+ private final int index;
+
+ private boolean notifiedDownstreamFormat;
+
+ public EmbeddedSampleStream(ChunkSampleStream<T> parent, SampleQueue sampleQueue, int index) {
+ this.parent = parent;
+ this.sampleQueue = sampleQueue;
+ this.index = index;
+ }
+
+ @Override
+ public boolean isReady() {
+ return !isPendingReset() && sampleQueue.isReady(loadingFinished);
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ if (isPendingReset()) {
+ return 0;
+ }
+ maybeNotifyDownstreamFormat();
+ int skipCount;
+ if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
+ skipCount = sampleQueue.advanceToEnd();
+ } else {
+ skipCount = sampleQueue.advanceTo(positionUs);
+ }
+ return skipCount;
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing. Errors will be thrown from the primary stream.
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ if (isPendingReset()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ maybeNotifyDownstreamFormat();
+ return sampleQueue.read(
+ formatHolder,
+ buffer,
+ formatRequired,
+ loadingFinished,
+ decodeOnlyUntilPositionUs);
+ }
+
+ public void release() {
+ Assertions.checkState(embeddedTracksSelected[index]);
+ embeddedTracksSelected[index] = false;
+ }
+
+ private void maybeNotifyDownstreamFormat() {
+ if (!notifiedDownstreamFormat) {
+ eventDispatcher.downstreamFormatChanged(
+ embeddedTrackTypes[index],
+ embeddedTrackFormats[index],
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ lastSeekPositionUs);
+ notifiedDownstreamFormat = true;
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java
new file mode 100644
index 0000000000..33cee8e20e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java
@@ -0,0 +1,111 @@
+/*
+ * 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.chunk;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A provider of {@link Chunk}s for a {@link ChunkSampleStream} to load.
+ */
+public interface ChunkSource {
+
+ /**
+ * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used
+ * as sync points.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @param seekParameters Parameters that control how the seek is performed.
+ * @return The adjusted seek position, in microseconds.
+ */
+ long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters);
+
+ /**
+ * If the source is currently having difficulty providing chunks, then this method throws the
+ * underlying error. Otherwise does nothing.
+ * <p>
+ * This method should only be called after the source has been prepared.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Evaluates whether {@link MediaChunk}s should be removed from the back of the queue.
+ * <p>
+ * Removing {@link MediaChunk}s from the back of the queue can be useful if they could be replaced
+ * with chunks of a significantly higher quality (e.g. because the available bandwidth has
+ * substantially increased).
+ *
+ * @param playbackPositionUs The current playback position.
+ * @param queue The queue of buffered {@link MediaChunk}s.
+ * @return The preferred queue size.
+ */
+ int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);
+
+ /**
+ * Returns the next chunk to load.
+ *
+ * <p>If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has
+ * been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the
+ * end of the stream has not been reached, the {@link ChunkHolder} is not modified.
+ *
+ * @param playbackPositionUs The current playback position in microseconds. If playback of the
+ * period to which this chunk source belongs has not yet started, the value will be the
+ * starting position in the period minus the duration of any media in previous periods still
+ * to be played.
+ * @param loadPositionUs The current load position in microseconds. If {@code queue} is empty,
+ * this is the starting position from which chunks should be provided. Else it's equal to
+ * {@link MediaChunk#endTimeUs} of the last chunk in the {@code queue}.
+ * @param queue The queue of buffered {@link MediaChunk}s.
+ * @param out A holder to populate.
+ */
+ void getNextChunk(
+ long playbackPositionUs,
+ long loadPositionUs,
+ List<? extends MediaChunk> queue,
+ ChunkHolder out);
+
+ /**
+ * Called when the {@link ChunkSampleStream} has finished loading a chunk obtained from this
+ * source.
+ *
+ * <p>This method should only be called when the source is enabled.
+ *
+ * @param chunk The chunk whose load has been completed.
+ */
+ void onChunkLoadCompleted(Chunk chunk);
+
+ /**
+ * Called when the {@link ChunkSampleStream} encounters an error loading a chunk obtained from
+ * this source.
+ *
+ * <p>This method should only be called when the source is enabled.
+ *
+ * @param chunk The chunk whose load encountered the error.
+ * @param cancelable Whether the load can be canceled.
+ * @param e The error.
+ * @param blacklistDurationMs The duration for which the associated track may be blacklisted, or
+ * {@link C#TIME_UNSET} if the track may not be blacklisted.
+ * @return Whether the load should be canceled so that a replacement chunk can be loaded instead.
+ * Must be {@code false} if {@code cancelable} is {@code false}. If {@code true}, {@link
+ * #getNextChunk(long, long, List, ChunkHolder)} will be called to obtain the replacement
+ * chunk.
+ */
+ boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
new file mode 100644
index 0000000000..98865e8b0e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
@@ -0,0 +1,157 @@
+/*
+ * 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.chunk;
+
+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.extractor.DefaultExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data.
+ */
+public class ContainerMediaChunk extends BaseMediaChunk {
+
+ private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder();
+
+ private final int chunkCount;
+ private final long sampleOffsetUs;
+ private final ChunkExtractorWrapper extractorWrapper;
+
+ private long nextLoadPosition;
+ private volatile boolean loadCanceled;
+ private boolean loadCompleted;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link
+ * C#TIME_UNSET} to output from the start of the chunk.
+ * @param clippedEndTimeUs The time in the chunk from which output will end, or {@link
+ * C#TIME_UNSET} to output to the end of the chunk.
+ * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
+ * @param chunkCount The number of chunks in the underlying media that are spanned by this
+ * instance. Normally equal to one, but may be larger if multiple chunks as defined by the
+ * underlying media are being merged into a single load.
+ * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor.
+ * @param extractorWrapper A wrapped extractor to use for parsing the data.
+ */
+ public ContainerMediaChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs,
+ long clippedStartTimeUs,
+ long clippedEndTimeUs,
+ long chunkIndex,
+ int chunkCount,
+ long sampleOffsetUs,
+ ChunkExtractorWrapper extractorWrapper) {
+ super(
+ dataSource,
+ dataSpec,
+ trackFormat,
+ trackSelectionReason,
+ trackSelectionData,
+ startTimeUs,
+ endTimeUs,
+ clippedStartTimeUs,
+ clippedEndTimeUs,
+ chunkIndex);
+ this.chunkCount = chunkCount;
+ this.sampleOffsetUs = sampleOffsetUs;
+ this.extractorWrapper = extractorWrapper;
+ }
+
+ @Override
+ public long getNextChunkIndex() {
+ return chunkIndex + chunkCount;
+ }
+
+ @Override
+ public boolean isLoadCompleted() {
+ return loadCompleted;
+ }
+
+ // Loadable implementation.
+
+ @Override
+ public final void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @SuppressWarnings("NonAtomicVolatileUpdate")
+ @Override
+ public final void load() throws IOException, InterruptedException {
+ if (nextLoadPosition == 0) {
+ // Configure the output and set it as the target for the extractor wrapper.
+ BaseMediaChunkOutput output = getOutput();
+ output.setSampleOffsetUs(sampleOffsetUs);
+ extractorWrapper.init(
+ getTrackOutputProvider(output),
+ clippedStartTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedStartTimeUs - sampleOffsetUs),
+ clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs));
+ }
+ try {
+ // Create and open the input.
+ DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition);
+ ExtractorInput input =
+ new DefaultExtractorInput(
+ dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
+ // Load and decode the sample data.
+ try {
+ Extractor extractor = extractorWrapper.extractor;
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ result = extractor.read(input, DUMMY_POSITION_HOLDER);
+ }
+ Assertions.checkState(result != Extractor.RESULT_SEEK);
+ } finally {
+ nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition;
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ loadCompleted = true;
+ }
+
+ /**
+ * Returns the {@link TrackOutputProvider} to be used by the wrapped extractor.
+ *
+ * @param baseMediaChunkOutput The {@link BaseMediaChunkOutput} most recently passed to {@link
+ * #init(BaseMediaChunkOutput)}.
+ * @return A {@link TrackOutputProvider} to be used by the wrapped extractor.
+ */
+ protected TrackOutputProvider getTrackOutputProvider(BaseMediaChunkOutput baseMediaChunkOutput) {
+ return baseMediaChunkOutput;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java
new file mode 100644
index 0000000000..583f8ceeee
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java
@@ -0,0 +1,119 @@
+/*
+ * 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.chunk;
+
+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.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * A base class for {@link Chunk} implementations where the data should be loaded into a
+ * {@code byte[]} before being consumed.
+ */
+public abstract class DataChunk extends Chunk {
+
+ private static final int READ_GRANULARITY = 16 * 1024;
+
+ private byte[] data;
+
+ private volatile boolean loadCanceled;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param type See {@link #type}.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param data An optional recycled array that can be used as a holder for the data.
+ */
+ public DataChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ int type,
+ Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ byte[] data) {
+ super(dataSource, dataSpec, type, trackFormat, trackSelectionReason, trackSelectionData,
+ C.TIME_UNSET, C.TIME_UNSET);
+ this.data = data;
+ }
+
+ /**
+ * Returns the array in which the data is held.
+ * <p>
+ * This method should be used for recycling the holder only, and not for reading the data.
+ *
+ * @return The array in which the data is held.
+ */
+ public byte[] getDataHolder() {
+ return data;
+ }
+
+ // Loadable implementation
+
+ @Override
+ public final void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public final void load() throws IOException, InterruptedException {
+ try {
+ dataSource.open(dataSpec);
+ int limit = 0;
+ int bytesRead = 0;
+ while (bytesRead != C.RESULT_END_OF_INPUT && !loadCanceled) {
+ maybeExpandData(limit);
+ bytesRead = dataSource.read(data, limit, READ_GRANULARITY);
+ if (bytesRead != -1) {
+ limit += bytesRead;
+ }
+ }
+ if (!loadCanceled) {
+ consume(data, limit);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+ /**
+ * Called by {@link #load()}. Implementations should override this method to consume the loaded
+ * data.
+ *
+ * @param data An array containing the data.
+ * @param limit The limit of the data.
+ * @throws IOException If an error occurs consuming the loaded data.
+ */
+ protected abstract void consume(byte[] data, int limit) throws IOException;
+
+ private void maybeExpandData(int limit) {
+ if (data == null) {
+ data = new byte[READ_GRANULARITY];
+ } else if (data.length < limit + READ_GRANULARITY) {
+ // The new length is calculated as (data.length + READ_GRANULARITY) rather than
+ // (limit + READ_GRANULARITY) in order to avoid small increments in the length.
+ data = Arrays.copyOf(data, data.length + READ_GRANULARITY);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
new file mode 100644
index 0000000000..db6e82c2c7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
@@ -0,0 +1,112 @@
+/*
+ * 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.chunk;
+
+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.extractor.DefaultExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track.
+ */
+public final class InitializationChunk extends Chunk {
+
+ private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder();
+
+ private final ChunkExtractorWrapper extractorWrapper;
+
+ @MonotonicNonNull private TrackOutputProvider trackOutputProvider;
+ private long nextLoadPosition;
+ private volatile boolean loadCanceled;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param extractorWrapper A wrapped extractor to use for parsing the initialization data.
+ */
+ public InitializationChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ ChunkExtractorWrapper extractorWrapper) {
+ super(dataSource, dataSpec, C.DATA_TYPE_MEDIA_INITIALIZATION, trackFormat, trackSelectionReason,
+ trackSelectionData, C.TIME_UNSET, C.TIME_UNSET);
+ this.extractorWrapper = extractorWrapper;
+ }
+
+ /**
+ * Initializes the chunk for loading, setting a {@link TrackOutputProvider} for track outputs to
+ * which formats will be written as they are loaded.
+ *
+ * @param trackOutputProvider The {@link TrackOutputProvider} for track outputs to which formats
+ * will be written as they are loaded.
+ */
+ public void init(TrackOutputProvider trackOutputProvider) {
+ this.trackOutputProvider = trackOutputProvider;
+ }
+
+ // Loadable implementation.
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @SuppressWarnings("NonAtomicVolatileUpdate")
+ @Override
+ public void load() throws IOException, InterruptedException {
+ if (nextLoadPosition == 0) {
+ extractorWrapper.init(
+ trackOutputProvider, /* startTimeUs= */ C.TIME_UNSET, /* endTimeUs= */ C.TIME_UNSET);
+ }
+ try {
+ // Create and open the input.
+ DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition);
+ ExtractorInput input =
+ new DefaultExtractorInput(
+ dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
+ // Load and decode the initialization data.
+ try {
+ Extractor extractor = extractorWrapper.extractor;
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ result = extractor.read(input, DUMMY_POSITION_HOLDER);
+ }
+ Assertions.checkState(result != Extractor.RESULT_SEEK);
+ } finally {
+ nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition;
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java
new file mode 100644
index 0000000000..81c9d216b9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java
@@ -0,0 +1,68 @@
+/*
+ * 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.chunk;
+
+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.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * An abstract base class for {@link Chunk}s that contain media samples.
+ */
+public abstract class MediaChunk extends Chunk {
+
+ /** The chunk index, or {@link C#INDEX_UNSET} if it is not known. */
+ public final long chunkIndex;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
+ */
+ public MediaChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs,
+ long chunkIndex) {
+ super(dataSource, dataSpec, C.DATA_TYPE_MEDIA, trackFormat, trackSelectionReason,
+ trackSelectionData, startTimeUs, endTimeUs);
+ Assertions.checkNotNull(trackFormat);
+ this.chunkIndex = chunkIndex;
+ }
+
+ /** Returns the next chunk index or {@link C#INDEX_UNSET} if it is not known. */
+ public long getNextChunkIndex() {
+ return chunkIndex != C.INDEX_UNSET ? chunkIndex + 1 : C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns whether the chunk has been fully loaded.
+ */
+ public abstract boolean isLoadCompleted();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java
new file mode 100644
index 0000000000..c6f5b1d41e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java
@@ -0,0 +1,104 @@
+/*
+ * 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.chunk;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import java.util.NoSuchElementException;
+
+/**
+ * Iterator for media chunk sequences.
+ *
+ * <p>The iterator initially points in front of the first available element. The first call to
+ * {@link #next()} moves the iterator to the first element. Check the return value of {@link
+ * #next()} or {@link #isEnded()} to determine whether the iterator reached the end of the available
+ * data.
+ */
+public interface MediaChunkIterator {
+
+ /** An empty media chunk iterator without available data. */
+ MediaChunkIterator EMPTY =
+ new MediaChunkIterator() {
+ @Override
+ public boolean isEnded() {
+ return true;
+ }
+
+ @Override
+ public boolean next() {
+ return false;
+ }
+
+ @Override
+ public DataSpec getDataSpec() {
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public long getChunkStartTimeUs() {
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public long getChunkEndTimeUs() {
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public void reset() {
+ // Do nothing.
+ }
+ };
+
+ /** Returns whether the iteration has reached the end of the available data. */
+ boolean isEnded();
+
+ /**
+ * Moves the iterator to the next media chunk.
+ *
+ * <p>Check the return value or {@link #isEnded()} to determine whether the iterator reached the
+ * end of the available data.
+ *
+ * @return Whether the iterator points to a media chunk with available data.
+ */
+ boolean next();
+
+ /**
+ * Returns the {@link DataSpec} used to load the media chunk.
+ *
+ * @throws java.util.NoSuchElementException If the method is called before the first call to
+ * {@link #next()} or when {@link #isEnded()} is true.
+ */
+ DataSpec getDataSpec();
+
+ /**
+ * Returns the media start time of the chunk, in microseconds.
+ *
+ * @throws java.util.NoSuchElementException If the method is called before the first call to
+ * {@link #next()} or when {@link #isEnded()} is true.
+ */
+ long getChunkStartTimeUs();
+
+ /**
+ * Returns the media end time of the chunk, in microseconds.
+ *
+ * @throws java.util.NoSuchElementException If the method is called before the first call to
+ * {@link #next()} or when {@link #isEnded()} is true.
+ */
+ long getChunkEndTimeUs();
+
+ /** Resets the iterator to the initial position. */
+ void reset();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java
new file mode 100644
index 0000000000..1b3004418e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java
@@ -0,0 +1,61 @@
+/*
+ * 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.chunk;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import java.util.List;
+
+/** A {@link MediaChunkIterator} which iterates over a {@link List} of {@link MediaChunk}s. */
+public final class MediaChunkListIterator extends BaseMediaChunkIterator {
+
+ private final List<? extends MediaChunk> chunks;
+ private final boolean reverseOrder;
+
+ /**
+ * Creates iterator.
+ *
+ * @param chunks The list of chunks to iterate over.
+ * @param reverseOrder Whether to iterate in reverse order.
+ */
+ public MediaChunkListIterator(List<? extends MediaChunk> chunks, boolean reverseOrder) {
+ super(0, chunks.size() - 1);
+ this.chunks = chunks;
+ this.reverseOrder = reverseOrder;
+ }
+
+ @Override
+ public DataSpec getDataSpec() {
+ return getCurrentChunk().dataSpec;
+ }
+
+ @Override
+ public long getChunkStartTimeUs() {
+ return getCurrentChunk().startTimeUs;
+ }
+
+ @Override
+ public long getChunkEndTimeUs() {
+ return getCurrentChunk().endTimeUs;
+ }
+
+ private MediaChunk getCurrentChunk() {
+ int index = (int) super.getCurrentIndex();
+ if (reverseOrder) {
+ index = chunks.size() - 1 - index;
+ }
+ return chunks.get(index);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
new file mode 100644
index 0000000000..b3d30408ee
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
@@ -0,0 +1,120 @@
+/*
+ * 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.chunk;
+
+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.extractor.DefaultExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A {@link BaseMediaChunk} for chunks consisting of a single raw sample.
+ */
+public final class SingleSampleMediaChunk extends BaseMediaChunk {
+
+ private final int trackType;
+ private final Format sampleFormat;
+
+ private long nextLoadPosition;
+ private boolean loadCompleted;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
+ * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*}
+ * constants.
+ * @param sampleFormat The {@link Format} of the sample in the chunk.
+ */
+ public SingleSampleMediaChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs,
+ long chunkIndex,
+ int trackType,
+ Format sampleFormat) {
+ super(
+ dataSource,
+ dataSpec,
+ trackFormat,
+ trackSelectionReason,
+ trackSelectionData,
+ startTimeUs,
+ endTimeUs,
+ /* clippedStartTimeUs= */ C.TIME_UNSET,
+ /* clippedEndTimeUs= */ C.TIME_UNSET,
+ chunkIndex);
+ this.trackType = trackType;
+ this.sampleFormat = sampleFormat;
+ }
+
+
+ @Override
+ public boolean isLoadCompleted() {
+ return loadCompleted;
+ }
+
+ // Loadable implementation.
+
+ @Override
+ public void cancelLoad() {
+ // Do nothing.
+ }
+
+ @SuppressWarnings("NonAtomicVolatileUpdate")
+ @Override
+ public void load() throws IOException, InterruptedException {
+ BaseMediaChunkOutput output = getOutput();
+ output.setSampleOffsetUs(0);
+ TrackOutput trackOutput = output.track(0, trackType);
+ trackOutput.format(sampleFormat);
+ try {
+ // Create and open the input.
+ DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition);
+ long length = dataSource.open(loadDataSpec);
+ if (length != C.LENGTH_UNSET) {
+ length += nextLoadPosition;
+ }
+ ExtractorInput extractorInput =
+ new DefaultExtractorInput(dataSource, nextLoadPosition, length);
+ // Load the sample data.
+ int result = 0;
+ while (result != C.RESULT_END_OF_INPUT) {
+ nextLoadPosition += result;
+ result = trackOutput.sampleData(extractorInput, Integer.MAX_VALUE, true);
+ }
+ int sampleSize = (int) nextLoadPosition;
+ trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ loadCompleted = true;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java
new file mode 100644
index 0000000000..4643c0402c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java
@@ -0,0 +1,129 @@
+/*
+ * 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;
+
+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.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSourceInputStream;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.List;
+import java.util.Map;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * A {@link DataSource} that decrypts data read from an upstream source, encrypted with AES-128 with
+ * a 128-bit key and PKCS7 padding.
+ *
+ * <p>Note that this {@link DataSource} does not support being opened from arbitrary offsets. It is
+ * designed specifically for reading whole files as defined in an HLS media playlist. For this
+ * reason the implementation is private to the HLS package.
+ */
+/* package */ class Aes128DataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final byte[] encryptionKey;
+ private final byte[] encryptionIv;
+
+ @Nullable private CipherInputStream cipherInputStream;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param encryptionKey The encryption key.
+ * @param encryptionIv The encryption initialization vector.
+ */
+ public Aes128DataSource(DataSource upstream, byte[] encryptionKey, byte[] encryptionIv) {
+ this.upstream = upstream;
+ this.encryptionKey = encryptionKey;
+ this.encryptionIv = encryptionIv;
+ }
+
+ @Override
+ public final void addTransferListener(TransferListener transferListener) {
+ upstream.addTransferListener(transferListener);
+ }
+
+ @Override
+ public final long open(DataSpec dataSpec) throws IOException {
+ Cipher cipher;
+ try {
+ cipher = getCipherInstance();
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new RuntimeException(e);
+ }
+
+ Key cipherKey = new SecretKeySpec(encryptionKey, "AES");
+ AlgorithmParameterSpec cipherIV = new IvParameterSpec(encryptionIv);
+
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new RuntimeException(e);
+ }
+
+ DataSourceInputStream inputStream = new DataSourceInputStream(upstream, dataSpec);
+ cipherInputStream = new CipherInputStream(inputStream, cipher);
+ inputStream.open();
+
+ return C.LENGTH_UNSET;
+ }
+
+ @Override
+ public final int read(byte[] buffer, int offset, int readLength) throws IOException {
+ Assertions.checkNotNull(cipherInputStream);
+ int bytesRead = cipherInputStream.read(buffer, offset, readLength);
+ if (bytesRead < 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public final Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public final Map<String, List<String>> getResponseHeaders() {
+ return upstream.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (cipherInputStream != null) {
+ cipherInputStream = null;
+ upstream.close();
+ }
+ }
+
+ protected Cipher getCipherInstance() throws NoSuchPaddingException, NoSuchAlgorithmException {
+ return Cipher.getInstance("AES/CBC/PKCS7Padding");
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java
new file mode 100644
index 0000000000..cbe2f797b7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+
+/**
+ * Default implementation of {@link HlsDataSourceFactory}.
+ */
+public final class DefaultHlsDataSourceFactory implements HlsDataSourceFactory {
+
+ private final DataSource.Factory dataSourceFactory;
+
+ /**
+ * @param dataSourceFactory The {@link DataSource.Factory} to use for all data types.
+ */
+ public DefaultHlsDataSourceFactory(DataSource.Factory dataSourceFactory) {
+ this.dataSourceFactory = dataSourceFactory;
+ }
+
+ @Override
+ public DataSource createDataSource(int dataType) {
+ return dataSourceFactory.createDataSource();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java
new file mode 100644
index 0000000000..6f39e1bff8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Default {@link HlsExtractorFactory} implementation.
+ */
+public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
+
+ public static final String AAC_FILE_EXTENSION = ".aac";
+ public static final String AC3_FILE_EXTENSION = ".ac3";
+ public static final String EC3_FILE_EXTENSION = ".ec3";
+ public static final String AC4_FILE_EXTENSION = ".ac4";
+ public static final String MP3_FILE_EXTENSION = ".mp3";
+ public static final String MP4_FILE_EXTENSION = ".mp4";
+ public static final String M4_FILE_EXTENSION_PREFIX = ".m4";
+ public static final String MP4_FILE_EXTENSION_PREFIX = ".mp4";
+ public static final String CMF_FILE_EXTENSION_PREFIX = ".cmf";
+ public static final String VTT_FILE_EXTENSION = ".vtt";
+ public static final String WEBVTT_FILE_EXTENSION = ".webvtt";
+
+ @DefaultTsPayloadReaderFactory.Flags private final int payloadReaderFactoryFlags;
+ private final boolean exposeCea608WhenMissingDeclarations;
+
+ /**
+ * Equivalent to {@link #DefaultHlsExtractorFactory(int, boolean) new
+ * DefaultHlsExtractorFactory(payloadReaderFactoryFlags = 0, exposeCea608WhenMissingDeclarations =
+ * true)}
+ */
+ public DefaultHlsExtractorFactory() {
+ this(/* payloadReaderFactoryFlags= */ 0, /* exposeCea608WhenMissingDeclarations */ true);
+ }
+
+ /**
+ * Creates a factory for HLS segment extractors.
+ *
+ * @param payloadReaderFactoryFlags Flags to add when constructing any {@link
+ * DefaultTsPayloadReaderFactory} instances. Other flags may be added on top of {@code
+ * payloadReaderFactoryFlags} when creating {@link DefaultTsPayloadReaderFactory}.
+ * @param exposeCea608WhenMissingDeclarations Whether created {@link TsExtractor} instances should
+ * expose a CEA-608 track should the master playlist contain no Closed Captions declarations.
+ * If the master playlist contains any Closed Captions declarations, this flag is ignored.
+ */
+ public DefaultHlsExtractorFactory(
+ int payloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations) {
+ this.payloadReaderFactoryFlags = payloadReaderFactoryFlags;
+ this.exposeCea608WhenMissingDeclarations = exposeCea608WhenMissingDeclarations;
+ }
+
+ @Override
+ public Result createExtractor(
+ @Nullable Extractor previousExtractor,
+ Uri uri,
+ Format format,
+ @Nullable List<Format> muxedCaptionFormats,
+ TimestampAdjuster timestampAdjuster,
+ Map<String, List<String>> responseHeaders,
+ ExtractorInput extractorInput)
+ throws InterruptedException, IOException {
+
+ if (previousExtractor != null) {
+ // A extractor has already been successfully used. Return one of the same type.
+ if (isReusable(previousExtractor)) {
+ return buildResult(previousExtractor);
+ } else {
+ Result result =
+ buildResultForSameExtractorType(previousExtractor, format, timestampAdjuster);
+ if (result == null) {
+ throw new IllegalArgumentException(
+ "Unexpected previousExtractor type: " + previousExtractor.getClass().getSimpleName());
+ }
+ }
+ }
+
+ // Try selecting the extractor by the file extension.
+ Extractor extractorByFileExtension =
+ createExtractorByFileExtension(uri, format, muxedCaptionFormats, timestampAdjuster);
+ extractorInput.resetPeekPosition();
+ if (sniffQuietly(extractorByFileExtension, extractorInput)) {
+ return buildResult(extractorByFileExtension);
+ }
+
+ // We need to manually sniff each known type, without retrying the one selected by file
+ // extension.
+
+ if (!(extractorByFileExtension instanceof WebvttExtractor)) {
+ WebvttExtractor webvttExtractor = new WebvttExtractor(format.language, timestampAdjuster);
+ if (sniffQuietly(webvttExtractor, extractorInput)) {
+ return buildResult(webvttExtractor);
+ }
+ }
+
+ if (!(extractorByFileExtension instanceof AdtsExtractor)) {
+ AdtsExtractor adtsExtractor = new AdtsExtractor();
+ if (sniffQuietly(adtsExtractor, extractorInput)) {
+ return buildResult(adtsExtractor);
+ }
+ }
+
+ if (!(extractorByFileExtension instanceof Ac3Extractor)) {
+ Ac3Extractor ac3Extractor = new Ac3Extractor();
+ if (sniffQuietly(ac3Extractor, extractorInput)) {
+ return buildResult(ac3Extractor);
+ }
+ }
+
+ if (!(extractorByFileExtension instanceof Ac4Extractor)) {
+ Ac4Extractor ac4Extractor = new Ac4Extractor();
+ if (sniffQuietly(ac4Extractor, extractorInput)) {
+ return buildResult(ac4Extractor);
+ }
+ }
+
+ if (!(extractorByFileExtension instanceof Mp3Extractor)) {
+ Mp3Extractor mp3Extractor =
+ new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0);
+ if (sniffQuietly(mp3Extractor, extractorInput)) {
+ return buildResult(mp3Extractor);
+ }
+ }
+
+ if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) {
+ FragmentedMp4Extractor fragmentedMp4Extractor =
+ createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats);
+ if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) {
+ return buildResult(fragmentedMp4Extractor);
+ }
+ }
+
+ if (!(extractorByFileExtension instanceof TsExtractor)) {
+ TsExtractor tsExtractor =
+ createTsExtractor(
+ payloadReaderFactoryFlags,
+ exposeCea608WhenMissingDeclarations,
+ format,
+ muxedCaptionFormats,
+ timestampAdjuster);
+ if (sniffQuietly(tsExtractor, extractorInput)) {
+ return buildResult(tsExtractor);
+ }
+ }
+
+ // Fall back on the extractor created by file extension.
+ return buildResult(extractorByFileExtension);
+ }
+
+ private Extractor createExtractorByFileExtension(
+ Uri uri,
+ Format format,
+ @Nullable List<Format> muxedCaptionFormats,
+ TimestampAdjuster timestampAdjuster) {
+ String lastPathSegment = uri.getLastPathSegment();
+ if (lastPathSegment == null) {
+ lastPathSegment = "";
+ }
+ if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType)
+ || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
+ || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
+ return new WebvttExtractor(format.language, timestampAdjuster);
+ } else if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
+ return new AdtsExtractor();
+ } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION)
+ || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) {
+ return new Ac3Extractor();
+ } else if (lastPathSegment.endsWith(AC4_FILE_EXTENSION)) {
+ return new Ac4Extractor();
+ } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
+ return new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0);
+ } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)
+ || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)
+ || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)
+ || lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) {
+ return createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats);
+ } else {
+ // For any other file extension, we assume TS format.
+ return createTsExtractor(
+ payloadReaderFactoryFlags,
+ exposeCea608WhenMissingDeclarations,
+ format,
+ muxedCaptionFormats,
+ timestampAdjuster);
+ }
+ }
+
+ private static TsExtractor createTsExtractor(
+ @DefaultTsPayloadReaderFactory.Flags int userProvidedPayloadReaderFactoryFlags,
+ boolean exposeCea608WhenMissingDeclarations,
+ Format format,
+ @Nullable List<Format> muxedCaptionFormats,
+ TimestampAdjuster timestampAdjuster) {
+ @DefaultTsPayloadReaderFactory.Flags
+ int payloadReaderFactoryFlags =
+ DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM
+ | userProvidedPayloadReaderFactoryFlags;
+ if (muxedCaptionFormats != null) {
+ // The playlist declares closed caption renditions, we should ignore descriptors.
+ payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS;
+ } else if (exposeCea608WhenMissingDeclarations) {
+ // The playlist does not provide any closed caption information. We preemptively declare a
+ // closed caption track on channel 0.
+ muxedCaptionFormats =
+ Collections.singletonList(
+ Format.createTextSampleFormat(
+ /* id= */ null,
+ MimeTypes.APPLICATION_CEA608,
+ /* selectionFlags= */ 0,
+ /* language= */ null));
+ } else {
+ muxedCaptionFormats = Collections.emptyList();
+ }
+ String codecs = format.codecs;
+ if (!TextUtils.isEmpty(codecs)) {
+ // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
+ // exist. If we know from the codec attribute that they don't exist, then we can
+ // explicitly ignore them even if they're declared.
+ if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
+ payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
+ }
+ if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
+ payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
+ }
+ }
+
+ return new TsExtractor(
+ TsExtractor.MODE_HLS,
+ timestampAdjuster,
+ new DefaultTsPayloadReaderFactory(payloadReaderFactoryFlags, muxedCaptionFormats));
+ }
+
+ private static FragmentedMp4Extractor createFragmentedMp4Extractor(
+ TimestampAdjuster timestampAdjuster,
+ Format format,
+ @Nullable List<Format> muxedCaptionFormats) {
+ // Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid
+ // creating a separate EMSG track for every audio track in a video stream.
+ return new FragmentedMp4Extractor(
+ /* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0,
+ timestampAdjuster,
+ /* sideloadedTrack= */ null,
+ muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList());
+ }
+
+ /** Returns true if this {@code format} represents a 'variant' track (i.e. the main one). */
+ private static boolean isFmp4Variant(Format format) {
+ Metadata metadata = format.metadata;
+ if (metadata == null) {
+ return false;
+ }
+ for (int i = 0; i < metadata.length(); i++) {
+ Metadata.Entry entry = metadata.get(i);
+ if (entry instanceof HlsTrackMetadataEntry) {
+ return !((HlsTrackMetadataEntry) entry).variantInfos.isEmpty();
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ private static Result buildResultForSameExtractorType(
+ Extractor previousExtractor, Format format, TimestampAdjuster timestampAdjuster) {
+ if (previousExtractor instanceof WebvttExtractor) {
+ return buildResult(new WebvttExtractor(format.language, timestampAdjuster));
+ } else if (previousExtractor instanceof AdtsExtractor) {
+ return buildResult(new AdtsExtractor());
+ } else if (previousExtractor instanceof Ac3Extractor) {
+ return buildResult(new Ac3Extractor());
+ } else if (previousExtractor instanceof Ac4Extractor) {
+ return buildResult(new Ac4Extractor());
+ } else if (previousExtractor instanceof Mp3Extractor) {
+ return buildResult(new Mp3Extractor());
+ } else {
+ return null;
+ }
+ }
+
+ private static Result buildResult(Extractor extractor) {
+ return new Result(
+ extractor,
+ extractor instanceof AdtsExtractor
+ || extractor instanceof Ac3Extractor
+ || extractor instanceof Ac4Extractor
+ || extractor instanceof Mp3Extractor,
+ isReusable(extractor));
+ }
+
+ private static boolean sniffQuietly(Extractor extractor, ExtractorInput input)
+ throws InterruptedException, IOException {
+ boolean result = false;
+ try {
+ result = extractor.sniff(input);
+ } catch (EOFException e) {
+ // Do nothing.
+ } finally {
+ input.resetPeekPosition();
+ }
+ return result;
+ }
+
+ private static boolean isReusable(Extractor previousExtractor) {
+ return previousExtractor instanceof TsExtractor
+ || previousExtractor instanceof FragmentedMp4Extractor;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java
new file mode 100644
index 0000000000..eab538582d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * LRU cache that holds up to {@code maxSize} full-segment-encryption keys. Which each addition,
+ * once the cache's size exceeds {@code maxSize}, the oldest item (according to insertion order) is
+ * removed.
+ */
+/* package */ final class FullSegmentEncryptionKeyCache {
+
+ private final LinkedHashMap<Uri, byte[]> backingMap;
+
+ public FullSegmentEncryptionKeyCache(int maxSize) {
+ backingMap =
+ new LinkedHashMap<Uri, byte[]>(
+ /* initialCapacity= */ maxSize + 1, /* loadFactor= */ 1, /* accessOrder= */ false) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<Uri, byte[]> eldest) {
+ return size() > maxSize;
+ }
+ };
+ }
+
+ /**
+ * Returns the {@code encryptionKey} cached against this {@code uri}, or null if {@code uri} is
+ * null or not present in the cache.
+ */
+ @Nullable
+ public byte[] get(@Nullable Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+ return backingMap.get(uri);
+ }
+
+ /**
+ * Inserts an entry into the cache.
+ *
+ * @throws NullPointerException if {@code uri} or {@code encryptionKey} are null.
+ */
+ @Nullable
+ public byte[] put(Uri uri, byte[] encryptionKey) {
+ return backingMap.put(Assertions.checkNotNull(uri), Assertions.checkNotNull(encryptionKey));
+ }
+
+ /**
+ * Returns true if {@code uri} is present in the cache.
+ *
+ * @throws NullPointerException if {@code uri} is null.
+ */
+ public boolean containsUri(Uri uri) {
+ return backingMap.containsKey(Assertions.checkNotNull(uri));
+ }
+
+ /**
+ * Removes {@code uri} from the cache. If {@code uri} was present in the cahce, this returns the
+ * corresponding {@code encryptionKey}, otherwise null.
+ *
+ * @throws NullPointerException if {@code uri} is null.
+ */
+ @Nullable
+ public byte[] remove(Uri uri) {
+ return backingMap.remove(Assertions.checkNotNull(uri));
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
new file mode 100644
index 0000000000..da935389d8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
@@ -0,0 +1,668 @@
+/*
+ * 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;
+
+import android.net.Uri;
+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.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.BehindLiveWindowException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.Chunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.DataChunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.BaseTrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.UriUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/** Source of Hls (possibly adaptive) chunks. */
+/* package */ class HlsChunkSource {
+
+ /**
+ * Chunk holder that allows the scheduling of retries.
+ */
+ public static final class HlsChunkHolder {
+
+ public HlsChunkHolder() {
+ clear();
+ }
+
+ /** The chunk to be loaded next. */
+ @Nullable public Chunk chunk;
+
+ /**
+ * Indicates that the end of the stream has been reached.
+ */
+ public boolean endOfStream;
+
+ /** Indicates that the chunk source is waiting for the referred playlist to be refreshed. */
+ @Nullable public Uri playlistUrl;
+
+ /**
+ * Clears the holder.
+ */
+ public void clear() {
+ chunk = null;
+ endOfStream = false;
+ playlistUrl = null;
+ }
+
+ }
+
+ /**
+ * The maximum number of keys that the key cache can hold. This value must be 2 or greater in
+ * order to hold initialization segment and media segment keys simultaneously.
+ */
+ private static final int KEY_CACHE_SIZE = 4;
+
+ private final HlsExtractorFactory extractorFactory;
+ private final DataSource mediaDataSource;
+ private final DataSource encryptionDataSource;
+ private final TimestampAdjusterProvider timestampAdjusterProvider;
+ private final Uri[] playlistUrls;
+ private final Format[] playlistFormats;
+ private final HlsPlaylistTracker playlistTracker;
+ private final TrackGroup trackGroup;
+ @Nullable private final List<Format> muxedCaptionFormats;
+ private final FullSegmentEncryptionKeyCache keyCache;
+
+ private boolean isTimestampMaster;
+ private byte[] scratchSpace;
+ @Nullable private IOException fatalError;
+ @Nullable private Uri expectedPlaylistUrl;
+ private boolean independentSegments;
+
+ // Note: The track group in the selection is typically *not* equal to trackGroup. This is due to
+ // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods
+ // in TrackSelection to avoid unexpected behavior.
+ private TrackSelection trackSelection;
+ private long liveEdgeInPeriodTimeUs;
+ private boolean seenExpectedPlaylistError;
+
+ /**
+ * @param extractorFactory An {@link HlsExtractorFactory} from which to obtain the extractors for
+ * media chunks.
+ * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists.
+ * @param playlistUrls The {@link Uri}s of the media playlists that can be adapted between by this
+ * chunk source.
+ * @param playlistFormats The {@link Format Formats} corresponding to the media playlists.
+ * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the
+ * chunks.
+ * @param mediaTransferListener The transfer listener which should be informed of any media data
+ * transfers. May be null if no listener is available.
+ * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If multiple
+ * {@link HlsChunkSource}s are used for a single playback, they should all share the same
+ * provider.
+ * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
+ * information is available in the master playlist.
+ */
+ public HlsChunkSource(
+ HlsExtractorFactory extractorFactory,
+ HlsPlaylistTracker playlistTracker,
+ Uri[] playlistUrls,
+ Format[] playlistFormats,
+ HlsDataSourceFactory dataSourceFactory,
+ @Nullable TransferListener mediaTransferListener,
+ TimestampAdjusterProvider timestampAdjusterProvider,
+ @Nullable List<Format> muxedCaptionFormats) {
+ this.extractorFactory = extractorFactory;
+ this.playlistTracker = playlistTracker;
+ this.playlistUrls = playlistUrls;
+ this.playlistFormats = playlistFormats;
+ this.timestampAdjusterProvider = timestampAdjusterProvider;
+ this.muxedCaptionFormats = muxedCaptionFormats;
+ keyCache = new FullSegmentEncryptionKeyCache(KEY_CACHE_SIZE);
+ scratchSpace = Util.EMPTY_BYTE_ARRAY;
+ liveEdgeInPeriodTimeUs = C.TIME_UNSET;
+ mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA);
+ if (mediaTransferListener != null) {
+ mediaDataSource.addTransferListener(mediaTransferListener);
+ }
+ encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM);
+ trackGroup = new TrackGroup(playlistFormats);
+ int[] initialTrackSelection = new int[playlistUrls.length];
+ for (int i = 0; i < playlistUrls.length; i++) {
+ initialTrackSelection[i] = i;
+ }
+ trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection);
+ }
+
+ /**
+ * If the source is currently having difficulty providing chunks, then this method throws the
+ * underlying error. Otherwise does nothing.
+ *
+ * @throws IOException The underlying error.
+ */
+ public void maybeThrowError() throws IOException {
+ if (fatalError != null) {
+ throw fatalError;
+ }
+ if (expectedPlaylistUrl != null && seenExpectedPlaylistError) {
+ playlistTracker.maybeThrowPlaylistRefreshError(expectedPlaylistUrl);
+ }
+ }
+
+ /**
+ * Returns the track group exposed by the source.
+ */
+ public TrackGroup getTrackGroup() {
+ return trackGroup;
+ }
+
+ /**
+ * Sets the current track selection.
+ *
+ * @param trackSelection The {@link TrackSelection}.
+ */
+ public void setTrackSelection(TrackSelection trackSelection) {
+ this.trackSelection = trackSelection;
+ }
+
+ /** Returns the current {@link TrackSelection}. */
+ public TrackSelection getTrackSelection() {
+ return trackSelection;
+ }
+
+ /**
+ * Resets the source.
+ */
+ public void reset() {
+ fatalError = null;
+ }
+
+ /**
+ * Sets whether this chunk source is responsible for initializing timestamp adjusters.
+ *
+ * @param isTimestampMaster True if this chunk source is responsible for initializing timestamp
+ * adjusters.
+ */
+ public void setIsTimestampMaster(boolean isTimestampMaster) {
+ this.isTimestampMaster = isTimestampMaster;
+ }
+
+ /**
+ * Returns the next chunk to load.
+ *
+ * <p>If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream
+ * has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available
+ * but the end of the stream has not been reached, {@link HlsChunkHolder#playlistUrl} is set to
+ * contain the {@link Uri} that refers to the playlist that needs refreshing.
+ *
+ * @param playbackPositionUs The current playback position relative to the period start in
+ * microseconds. If playback of the period to which this chunk source belongs has not yet
+ * started, the value will be the starting position in the period minus the duration of any
+ * media in previous periods still to be played.
+ * @param loadPositionUs The current load position relative to the period start in microseconds.
+ * @param queue The queue of buffered {@link HlsMediaChunk}s.
+ * @param allowEndOfStream Whether {@link HlsChunkHolder#endOfStream} is allowed to be set for
+ * non-empty media playlists. If {@code false}, the last available chunk is returned instead.
+ * If the media playlist is empty, {@link HlsChunkHolder#endOfStream} is always set.
+ * @param out A holder to populate.
+ */
+ public void getNextChunk(
+ long playbackPositionUs,
+ long loadPositionUs,
+ List<HlsMediaChunk> queue,
+ boolean allowEndOfStream,
+ HlsChunkHolder out) {
+ HlsMediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1);
+ int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat);
+ long bufferedDurationUs = loadPositionUs - playbackPositionUs;
+ long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);
+ if (previous != null && !independentSegments) {
+ // Unless segments are known to be independent, switching tracks requires downloading
+ // overlapping segments. Hence we subtract the previous segment's duration from the buffered
+ // duration.
+ // This may affect the live-streaming adaptive track selection logic, when we compare the
+ // buffered duration to time-to-live-edge to decide whether to switch. Therefore, we subtract
+ // the duration of the last loaded segment from timeToLiveEdgeUs as well.
+ long subtractedDurationUs = previous.getDurationUs();
+ bufferedDurationUs = Math.max(0, bufferedDurationUs - subtractedDurationUs);
+ if (timeToLiveEdgeUs != C.TIME_UNSET) {
+ timeToLiveEdgeUs = Math.max(0, timeToLiveEdgeUs - subtractedDurationUs);
+ }
+ }
+
+ // Select the track.
+ MediaChunkIterator[] mediaChunkIterators = createMediaChunkIterators(previous, loadPositionUs);
+ trackSelection.updateSelectedTrack(
+ playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators);
+ int selectedTrackIndex = trackSelection.getSelectedIndexInTrackGroup();
+
+ boolean switchingTrack = oldTrackIndex != selectedTrackIndex;
+ Uri selectedPlaylistUrl = playlistUrls[selectedTrackIndex];
+ if (!playlistTracker.isSnapshotValid(selectedPlaylistUrl)) {
+ out.playlistUrl = selectedPlaylistUrl;
+ seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);
+ expectedPlaylistUrl = selectedPlaylistUrl;
+ // Retry when playlist is refreshed.
+ return;
+ }
+ HlsMediaPlaylist mediaPlaylist =
+ playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
+ // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null.
+ Assertions.checkNotNull(mediaPlaylist);
+ independentSegments = mediaPlaylist.hasIndependentSegments;
+
+ updateLiveEdgeTimeUs(mediaPlaylist);
+
+ // Select the chunk.
+ long startOfPlaylistInPeriodUs =
+ mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
+ long chunkMediaSequence =
+ getChunkMediaSequence(
+ previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs);
+ if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null && switchingTrack) {
+ // We try getting the next chunk without adapting in case that's the reason for falling
+ // behind the live window.
+ selectedTrackIndex = oldTrackIndex;
+ selectedPlaylistUrl = playlistUrls[selectedTrackIndex];
+ mediaPlaylist =
+ playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
+ // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be
+ // non-null.
+ Assertions.checkNotNull(mediaPlaylist);
+ startOfPlaylistInPeriodUs =
+ mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
+ chunkMediaSequence = previous.getNextChunkIndex();
+ }
+
+ if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
+ fatalError = new BehindLiveWindowException();
+ return;
+ }
+
+ int segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence);
+ int availableSegmentCount = mediaPlaylist.segments.size();
+ if (segmentIndexInPlaylist >= availableSegmentCount) {
+ if (mediaPlaylist.hasEndTag) {
+ if (allowEndOfStream || availableSegmentCount == 0) {
+ out.endOfStream = true;
+ return;
+ }
+ segmentIndexInPlaylist = availableSegmentCount - 1;
+ } else /* Live */ {
+ out.playlistUrl = selectedPlaylistUrl;
+ seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);
+ expectedPlaylistUrl = selectedPlaylistUrl;
+ return;
+ }
+ }
+ // We have a valid playlist snapshot, we can discard any playlist errors at this point.
+ seenExpectedPlaylistError = false;
+ expectedPlaylistUrl = null;
+
+ // Handle encryption.
+ HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
+
+ // Check if the segment or its initialization segment are fully encrypted.
+ Uri initSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment.initializationSegment);
+ out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex);
+ if (out.chunk != null) {
+ return;
+ }
+ Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment);
+ out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex);
+ if (out.chunk != null) {
+ return;
+ }
+
+ out.chunk =
+ HlsMediaChunk.createInstance(
+ extractorFactory,
+ mediaDataSource,
+ playlistFormats[selectedTrackIndex],
+ startOfPlaylistInPeriodUs,
+ mediaPlaylist,
+ segmentIndexInPlaylist,
+ selectedPlaylistUrl,
+ muxedCaptionFormats,
+ trackSelection.getSelectionReason(),
+ trackSelection.getSelectionData(),
+ isTimestampMaster,
+ timestampAdjusterProvider,
+ previous,
+ /* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),
+ /* initSegmentKey= */ keyCache.get(initSegmentKeyUri));
+ }
+
+ /**
+ * Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
+ * source.
+ *
+ * @param chunk The chunk whose load has been completed.
+ */
+ public void onChunkLoadCompleted(Chunk chunk) {
+ if (chunk instanceof EncryptionKeyChunk) {
+ EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk;
+ scratchSpace = encryptionKeyChunk.getDataHolder();
+ keyCache.put(
+ encryptionKeyChunk.dataSpec.uri, Assertions.checkNotNull(encryptionKeyChunk.getResult()));
+ }
+ }
+
+ /**
+ * Attempts to blacklist the track associated with the given chunk. Blacklisting will fail if the
+ * track is the only non-blacklisted track in the selection.
+ *
+ * @param chunk The chunk whose load caused the blacklisting attempt.
+ * @param blacklistDurationMs The number of milliseconds for which the track selection should be
+ * blacklisted.
+ * @return Whether the blacklisting succeeded.
+ */
+ public boolean maybeBlacklistTrack(Chunk chunk, long blacklistDurationMs) {
+ return trackSelection.blacklist(
+ trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), blacklistDurationMs);
+ }
+
+ /**
+ * Called when a playlist load encounters an error.
+ *
+ * @param playlistUrl The {@link Uri} of the playlist whose load encountered an 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.
+ */
+ public boolean onPlaylistError(Uri playlistUrl, long blacklistDurationMs) {
+ int trackGroupIndex = C.INDEX_UNSET;
+ for (int i = 0; i < playlistUrls.length; i++) {
+ if (playlistUrls[i].equals(playlistUrl)) {
+ trackGroupIndex = i;
+ break;
+ }
+ }
+ if (trackGroupIndex == C.INDEX_UNSET) {
+ return true;
+ }
+ int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex);
+ if (trackSelectionIndex == C.INDEX_UNSET) {
+ return true;
+ }
+ seenExpectedPlaylistError |= playlistUrl.equals(expectedPlaylistUrl);
+ return blacklistDurationMs == C.TIME_UNSET
+ || trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs);
+ }
+
+ /**
+ * Returns an array of {@link MediaChunkIterator}s for upcoming media chunks.
+ *
+ * @param previous The previous media chunk. May be null.
+ * @param loadPositionUs The position at which the iterators will start.
+ * @return Array of {@link MediaChunkIterator}s for each track.
+ */
+ public MediaChunkIterator[] createMediaChunkIterators(
+ @Nullable HlsMediaChunk previous, long loadPositionUs) {
+ int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat);
+ MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];
+ for (int i = 0; i < chunkIterators.length; i++) {
+ int trackIndex = trackSelection.getIndexInTrackGroup(i);
+ Uri playlistUrl = playlistUrls[trackIndex];
+ if (!playlistTracker.isSnapshotValid(playlistUrl)) {
+ chunkIterators[i] = MediaChunkIterator.EMPTY;
+ continue;
+ }
+ HlsMediaPlaylist playlist =
+ playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false);
+ // Playlist snapshot is valid (checked by if() above) so playlist must be non-null.
+ Assertions.checkNotNull(playlist);
+ long startOfPlaylistInPeriodUs =
+ playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
+ boolean switchingTrack = trackIndex != oldTrackIndex;
+ long chunkMediaSequence =
+ getChunkMediaSequence(
+ previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
+ if (chunkMediaSequence < playlist.mediaSequence) {
+ chunkIterators[i] = MediaChunkIterator.EMPTY;
+ continue;
+ }
+ int chunkIndex = (int) (chunkMediaSequence - playlist.mediaSequence);
+ chunkIterators[i] =
+ new HlsMediaPlaylistSegmentIterator(playlist, startOfPlaylistInPeriodUs, chunkIndex);
+ }
+ return chunkIterators;
+ }
+
+ // Private methods.
+
+ /**
+ * Returns the media sequence number of the segment to load next in {@code mediaPlaylist}.
+ *
+ * @param previous The last (at least partially) loaded segment.
+ * @param switchingTrack Whether the segment to load is not preceded by a segment in the same
+ * track.
+ * @param mediaPlaylist The media playlist to which the segment to load belongs.
+ * @param startOfPlaylistInPeriodUs The start of {@code mediaPlaylist} relative to the period
+ * start in microseconds.
+ * @param loadPositionUs The current load position relative to the period start in microseconds.
+ * @return The media sequence of the segment to load.
+ */
+ private long getChunkMediaSequence(
+ @Nullable HlsMediaChunk previous,
+ boolean switchingTrack,
+ HlsMediaPlaylist mediaPlaylist,
+ long startOfPlaylistInPeriodUs,
+ long loadPositionUs) {
+ if (previous == null || switchingTrack) {
+ long endOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs + mediaPlaylist.durationUs;
+ long targetPositionInPeriodUs =
+ (previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs;
+ if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) {
+ // If the playlist is too old to contain the chunk, we need to refresh it.
+ return mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
+ }
+ long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs;
+ return Util.binarySearchFloor(
+ mediaPlaylist.segments,
+ /* value= */ targetPositionInPlaylistUs,
+ /* inclusive= */ true,
+ /* stayInBounds= */ !playlistTracker.isLive() || previous == null)
+ + mediaPlaylist.mediaSequence;
+ }
+ // We ignore the case of previous not having loaded completely, in which case we load the next
+ // segment.
+ return previous.getNextChunkIndex();
+ }
+
+ private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {
+ final boolean resolveTimeToLiveEdgePossible = liveEdgeInPeriodTimeUs != C.TIME_UNSET;
+ return resolveTimeToLiveEdgePossible
+ ? liveEdgeInPeriodTimeUs - playbackPositionUs
+ : C.TIME_UNSET;
+ }
+
+ private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) {
+ liveEdgeInPeriodTimeUs =
+ mediaPlaylist.hasEndTag
+ ? C.TIME_UNSET
+ : (mediaPlaylist.getEndTimeUs() - playlistTracker.getInitialStartTimeUs());
+ }
+
+ @Nullable
+ private Chunk maybeCreateEncryptionChunkFor(@Nullable Uri keyUri, int selectedTrackIndex) {
+ if (keyUri == null) {
+ return null;
+ }
+
+ byte[] encryptionKey = keyCache.remove(keyUri);
+ if (encryptionKey != null) {
+ // The key was present in the key cache. We re-insert it to prevent it from being evicted by
+ // the following key addition. Note that removal of the key is necessary to affect the
+ // eviction order.
+ keyCache.put(keyUri, encryptionKey);
+ return null;
+ }
+ DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP);
+ return new EncryptionKeyChunk(
+ encryptionDataSource,
+ dataSpec,
+ playlistFormats[selectedTrackIndex],
+ trackSelection.getSelectionReason(),
+ trackSelection.getSelectionData(),
+ scratchSpace);
+ }
+
+ @Nullable
+ private static Uri getFullEncryptionKeyUri(HlsMediaPlaylist playlist, @Nullable Segment segment) {
+ if (segment == null || segment.fullSegmentEncryptionKeyUri == null) {
+ return null;
+ }
+ return UriUtil.resolveToUri(playlist.baseUri, segment.fullSegmentEncryptionKeyUri);
+ }
+
+ // Private classes.
+
+ /**
+ * A {@link TrackSelection} to use for initialization.
+ */
+ private static final class InitializationTrackSelection extends BaseTrackSelection {
+
+ private int selectedIndex;
+
+ public InitializationTrackSelection(TrackGroup group, int[] tracks) {
+ super(group, tracks);
+ selectedIndex = indexOf(group.getFormat(0));
+ }
+
+ @Override
+ public void updateSelectedTrack(
+ long playbackPositionUs,
+ long bufferedDurationUs,
+ long availableDurationUs,
+ List<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators) {
+ long nowMs = SystemClock.elapsedRealtime();
+ if (!isBlacklisted(selectedIndex, nowMs)) {
+ return;
+ }
+ // Try from lowest bitrate to highest.
+ for (int i = length - 1; i >= 0; i--) {
+ if (!isBlacklisted(i, nowMs)) {
+ selectedIndex = i;
+ return;
+ }
+ }
+ // Should never happen.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return C.SELECTION_REASON_UNKNOWN;
+ }
+
+ @Override
+ @Nullable
+ public Object getSelectionData() {
+ return null;
+ }
+
+ }
+
+ private static final class EncryptionKeyChunk extends DataChunk {
+
+ private byte @MonotonicNonNull [] result;
+
+ public EncryptionKeyChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ byte[] scratchSpace) {
+ super(dataSource, dataSpec, C.DATA_TYPE_DRM, trackFormat, trackSelectionReason,
+ trackSelectionData, scratchSpace);
+ }
+
+ @Override
+ protected void consume(byte[] data, int limit) {
+ result = Arrays.copyOf(data, limit);
+ }
+
+ /** Return the result of this chunk, or null if loading is not complete. */
+ @Nullable
+ public byte[] getResult() {
+ return result;
+ }
+
+ }
+
+ /** {@link MediaChunkIterator} wrapping a {@link HlsMediaPlaylist}. */
+ private static final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator {
+
+ private final HlsMediaPlaylist playlist;
+ private final long startOfPlaylistInPeriodUs;
+
+ /**
+ * Creates iterator.
+ *
+ * @param playlist The {@link HlsMediaPlaylist} to wrap.
+ * @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in
+ * microseconds.
+ * @param chunkIndex The index of the first available chunk in the playlist.
+ */
+ public HlsMediaPlaylistSegmentIterator(
+ HlsMediaPlaylist playlist, long startOfPlaylistInPeriodUs, int chunkIndex) {
+ super(/* fromIndex= */ chunkIndex, /* toIndex= */ playlist.segments.size() - 1);
+ this.playlist = playlist;
+ this.startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs;
+ }
+
+ @Override
+ public DataSpec getDataSpec() {
+ checkInBounds();
+ Segment segment = playlist.segments.get((int) getCurrentIndex());
+ Uri chunkUri = UriUtil.resolveToUri(playlist.baseUri, segment.url);
+ return new DataSpec(
+ chunkUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null);
+ }
+
+ @Override
+ public long getChunkStartTimeUs() {
+ checkInBounds();
+ Segment segment = playlist.segments.get((int) getCurrentIndex());
+ return startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
+ }
+
+ @Override
+ public long getChunkEndTimeUs() {
+ checkInBounds();
+ Segment segment = playlist.segments.get((int) getCurrentIndex());
+ long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
+ return segmentStartTimeInPeriodUs + segment.durationUs;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java
new file mode 100644
index 0000000000..66fac54b8d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+
+/**
+ * Creates {@link DataSource}s for HLS playlists, encryption and media chunks.
+ */
+public interface HlsDataSourceFactory {
+
+ /**
+ * Creates a {@link DataSource} for the given data type.
+ *
+ * @param dataType The data type for which the {@link DataSource} will be used. One of {@link C}
+ * {@code .DATA_TYPE_*} constants.
+ * @return A {@link DataSource} for the given data type.
+ */
+ DataSource createDataSource(int dataType);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java
new file mode 100644
index 0000000000..8f445f97ed
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017 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;
+
+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.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Factory for HLS media chunk extractors.
+ */
+public interface HlsExtractorFactory {
+
+ /** Holds an {@link Extractor} and associated parameters. */
+ final class Result {
+
+ /** The created extractor; */
+ public final Extractor extractor;
+ /** Whether the segments for which {@link #extractor} is created are packed audio segments. */
+ public final boolean isPackedAudioExtractor;
+ /**
+ * Whether {@link #extractor} may be reused for following continuous (no immediately preceding
+ * discontinuities) segments of the same variant.
+ */
+ public final boolean isReusable;
+
+ /**
+ * Creates a result.
+ *
+ * @param extractor See {@link #extractor}.
+ * @param isPackedAudioExtractor See {@link #isPackedAudioExtractor}.
+ * @param isReusable See {@link #isReusable}.
+ */
+ public Result(Extractor extractor, boolean isPackedAudioExtractor, boolean isReusable) {
+ this.extractor = extractor;
+ this.isPackedAudioExtractor = isPackedAudioExtractor;
+ this.isReusable = isReusable;
+ }
+ }
+
+ HlsExtractorFactory DEFAULT = new DefaultHlsExtractorFactory();
+
+ /**
+ * Creates an {@link Extractor} for extracting HLS media chunks.
+ *
+ * @param previousExtractor A previously used {@link Extractor} which can be reused if the current
+ * chunk is a continuation of the previously extracted chunk, or null otherwise. It is the
+ * responsibility of implementers to only reuse extractors that are suited for reusage.
+ * @param uri The URI of the media chunk.
+ * @param format A {@link Format} associated with the chunk to extract.
+ * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
+ * information is available in the master playlist.
+ * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
+ * @param responseHeaders The HTTP response headers associated with the media segment or
+ * initialization section to extract.
+ * @param sniffingExtractorInput The first extractor input that will be passed to the returned
+ * extractor's {@link Extractor#read(ExtractorInput, PositionHolder)}. Must only be used to
+ * call {@link Extractor#sniff(ExtractorInput)}.
+ * @return A {@link Result}.
+ * @throws InterruptedException If the thread is interrupted while sniffing.
+ * @throws IOException If an I/O error is encountered while sniffing.
+ */
+ Result createExtractor(
+ @Nullable Extractor previousExtractor,
+ Uri uri,
+ Format format,
+ @Nullable List<Format> muxedCaptionFormats,
+ TimestampAdjuster timestampAdjuster,
+ Map<String, List<String>> responseHeaders,
+ ExtractorInput sniffingExtractorInput)
+ throws InterruptedException, IOException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsManifest.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsManifest.java
new file mode 100644
index 0000000000..52a5632134
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsManifest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+
+/**
+ * Holds a master playlist along with a snapshot of one of its media playlists.
+ */
+public final class HlsManifest {
+
+ /**
+ * The master playlist of an HLS stream.
+ */
+ public final HlsMasterPlaylist masterPlaylist;
+ /**
+ * A snapshot of a media playlist referred to by {@link #masterPlaylist}.
+ */
+ public final HlsMediaPlaylist mediaPlaylist;
+
+ /**
+ * @param masterPlaylist The master playlist.
+ * @param mediaPlaylist The media playlist.
+ */
+ HlsManifest(HlsMasterPlaylist masterPlaylist, HlsMediaPlaylist mediaPlaylist) {
+ this.masterPlaylist = masterPlaylist;
+ this.mediaPlaylist = mediaPlaylist;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
new file mode 100644
index 0000000000..173e53faad
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
@@ -0,0 +1,519 @@
+/*
+ * 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;
+
+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.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.PrivFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.UriUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * An HLS {@link MediaChunk}.
+ */
+/* package */ final class HlsMediaChunk extends MediaChunk {
+
+ /**
+ * Creates a new instance.
+ *
+ * @param extractorFactory A {@link HlsExtractorFactory} from which the HLS media chunk extractor
+ * is obtained.
+ * @param dataSource The source from which the data should be loaded.
+ * @param format The chunk format.
+ * @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds.
+ * @param mediaPlaylist The media playlist from which this chunk was obtained.
+ * @param playlistUrl The url of the playlist from which this chunk was obtained.
+ * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
+ * information is available in the master playlist.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.
+ * @param timestampAdjusterProvider The provider from which to obtain the {@link
+ * TimestampAdjuster}.
+ * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null.
+ * @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise.
+ * @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null
+ * otherwise.
+ */
+ public static HlsMediaChunk createInstance(
+ HlsExtractorFactory extractorFactory,
+ DataSource dataSource,
+ Format format,
+ long startOfPlaylistInPeriodUs,
+ HlsMediaPlaylist mediaPlaylist,
+ int segmentIndexInPlaylist,
+ Uri playlistUrl,
+ @Nullable List<Format> muxedCaptionFormats,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ boolean isMasterTimestampSource,
+ TimestampAdjusterProvider timestampAdjusterProvider,
+ @Nullable HlsMediaChunk previousChunk,
+ @Nullable byte[] mediaSegmentKey,
+ @Nullable byte[] initSegmentKey) {
+ // Media segment.
+ HlsMediaPlaylist.Segment mediaSegment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
+ DataSpec dataSpec =
+ new DataSpec(
+ UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url),
+ mediaSegment.byterangeOffset,
+ mediaSegment.byterangeLength,
+ /* key= */ null);
+ boolean mediaSegmentEncrypted = mediaSegmentKey != null;
+ byte[] mediaSegmentIv =
+ mediaSegmentEncrypted
+ ? getEncryptionIvArray(Assertions.checkNotNull(mediaSegment.encryptionIV))
+ : null;
+ DataSource mediaDataSource = buildDataSource(dataSource, mediaSegmentKey, mediaSegmentIv);
+
+ // Init segment.
+ HlsMediaPlaylist.Segment initSegment = mediaSegment.initializationSegment;
+ DataSpec initDataSpec = null;
+ boolean initSegmentEncrypted = false;
+ DataSource initDataSource = null;
+ if (initSegment != null) {
+ initSegmentEncrypted = initSegmentKey != null;
+ byte[] initSegmentIv =
+ initSegmentEncrypted
+ ? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV))
+ : null;
+ Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);
+ initDataSpec =
+ new DataSpec(
+ initSegmentUri,
+ initSegment.byterangeOffset,
+ initSegment.byterangeLength,
+ /* key= */ null);
+ initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv);
+ }
+
+ long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + mediaSegment.relativeStartTimeUs;
+ long segmentEndTimeInPeriodUs = segmentStartTimeInPeriodUs + mediaSegment.durationUs;
+ int discontinuitySequenceNumber =
+ mediaPlaylist.discontinuitySequence + mediaSegment.relativeDiscontinuitySequence;
+
+ Extractor previousExtractor = null;
+ Id3Decoder id3Decoder;
+ ParsableByteArray scratchId3Data;
+ boolean shouldSpliceIn;
+ if (previousChunk != null) {
+ id3Decoder = previousChunk.id3Decoder;
+ scratchId3Data = previousChunk.scratchId3Data;
+ shouldSpliceIn =
+ !playlistUrl.equals(previousChunk.playlistUrl) || !previousChunk.loadCompleted;
+ previousExtractor =
+ previousChunk.isExtractorReusable
+ && previousChunk.discontinuitySequenceNumber == discontinuitySequenceNumber
+ && !shouldSpliceIn
+ ? previousChunk.extractor
+ : null;
+ } else {
+ id3Decoder = new Id3Decoder();
+ scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
+ shouldSpliceIn = false;
+ }
+
+ return new HlsMediaChunk(
+ extractorFactory,
+ mediaDataSource,
+ dataSpec,
+ format,
+ mediaSegmentEncrypted,
+ initDataSource,
+ initDataSpec,
+ initSegmentEncrypted,
+ playlistUrl,
+ muxedCaptionFormats,
+ trackSelectionReason,
+ trackSelectionData,
+ segmentStartTimeInPeriodUs,
+ segmentEndTimeInPeriodUs,
+ /* chunkMediaSequence= */ mediaPlaylist.mediaSequence + segmentIndexInPlaylist,
+ discontinuitySequenceNumber,
+ mediaSegment.hasGapTag,
+ isMasterTimestampSource,
+ /* timestampAdjuster= */ timestampAdjusterProvider.getAdjuster(discontinuitySequenceNumber),
+ mediaSegment.drmInitData,
+ previousExtractor,
+ id3Decoder,
+ scratchId3Data,
+ shouldSpliceIn);
+ }
+
+ public static final String PRIV_TIMESTAMP_FRAME_OWNER =
+ "com.apple.streaming.transportStreamTimestamp";
+ private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder();
+
+ private static final AtomicInteger uidSource = new AtomicInteger();
+
+ /**
+ * A unique identifier for the chunk.
+ */
+ public final int uid;
+
+ /**
+ * The discontinuity sequence number of the chunk.
+ */
+ public final int discontinuitySequenceNumber;
+
+ /** The url of the playlist from which this chunk was obtained. */
+ public final Uri playlistUrl;
+
+ @Nullable private final DataSource initDataSource;
+ @Nullable private final DataSpec initDataSpec;
+ @Nullable private final Extractor previousExtractor;
+
+ private final boolean isMasterTimestampSource;
+ private final boolean hasGapTag;
+ private final TimestampAdjuster timestampAdjuster;
+ private final boolean shouldSpliceIn;
+ private final HlsExtractorFactory extractorFactory;
+ @Nullable private final List<Format> muxedCaptionFormats;
+ @Nullable private final DrmInitData drmInitData;
+ private final Id3Decoder id3Decoder;
+ private final ParsableByteArray scratchId3Data;
+ private final boolean mediaSegmentEncrypted;
+ private final boolean initSegmentEncrypted;
+
+ @MonotonicNonNull private Extractor extractor;
+ private boolean isExtractorReusable;
+ @MonotonicNonNull private HlsSampleStreamWrapper output;
+ // nextLoadPosition refers to the init segment if initDataLoadRequired is true.
+ // Otherwise, nextLoadPosition refers to the media segment.
+ private int nextLoadPosition;
+ private boolean initDataLoadRequired;
+ private volatile boolean loadCanceled;
+ private boolean loadCompleted;
+
+ private HlsMediaChunk(
+ HlsExtractorFactory extractorFactory,
+ DataSource mediaDataSource,
+ DataSpec dataSpec,
+ Format format,
+ boolean mediaSegmentEncrypted,
+ @Nullable DataSource initDataSource,
+ @Nullable DataSpec initDataSpec,
+ boolean initSegmentEncrypted,
+ Uri playlistUrl,
+ @Nullable List<Format> muxedCaptionFormats,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs,
+ long chunkMediaSequence,
+ int discontinuitySequenceNumber,
+ boolean hasGapTag,
+ boolean isMasterTimestampSource,
+ TimestampAdjuster timestampAdjuster,
+ @Nullable DrmInitData drmInitData,
+ @Nullable Extractor previousExtractor,
+ Id3Decoder id3Decoder,
+ ParsableByteArray scratchId3Data,
+ boolean shouldSpliceIn) {
+ super(
+ mediaDataSource,
+ dataSpec,
+ format,
+ trackSelectionReason,
+ trackSelectionData,
+ startTimeUs,
+ endTimeUs,
+ chunkMediaSequence);
+ this.mediaSegmentEncrypted = mediaSegmentEncrypted;
+ this.discontinuitySequenceNumber = discontinuitySequenceNumber;
+ this.initDataSpec = initDataSpec;
+ this.initDataSource = initDataSource;
+ this.initDataLoadRequired = initDataSpec != null;
+ this.initSegmentEncrypted = initSegmentEncrypted;
+ this.playlistUrl = playlistUrl;
+ this.isMasterTimestampSource = isMasterTimestampSource;
+ this.timestampAdjuster = timestampAdjuster;
+ this.hasGapTag = hasGapTag;
+ this.extractorFactory = extractorFactory;
+ this.muxedCaptionFormats = muxedCaptionFormats;
+ this.drmInitData = drmInitData;
+ this.previousExtractor = previousExtractor;
+ this.id3Decoder = id3Decoder;
+ this.scratchId3Data = scratchId3Data;
+ this.shouldSpliceIn = shouldSpliceIn;
+ uid = uidSource.getAndIncrement();
+ }
+
+ /**
+ * Initializes the chunk for loading, setting the {@link HlsSampleStreamWrapper} that will receive
+ * samples as they are loaded.
+ *
+ * @param output The output that will receive the loaded samples.
+ */
+ public void init(HlsSampleStreamWrapper output) {
+ this.output = output;
+ output.init(uid, shouldSpliceIn);
+ }
+
+ @Override
+ public boolean isLoadCompleted() {
+ return loadCompleted;
+ }
+
+ // Loadable implementation
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ // output == null means init() hasn't been called.
+ Assertions.checkNotNull(output);
+ if (extractor == null && previousExtractor != null) {
+ extractor = previousExtractor;
+ isExtractorReusable = true;
+ initDataLoadRequired = false;
+ }
+ maybeLoadInitData();
+ if (!loadCanceled) {
+ if (!hasGapTag) {
+ loadMedia();
+ }
+ loadCompleted = true;
+ }
+ }
+
+ // Internal methods.
+
+ @RequiresNonNull("output")
+ private void maybeLoadInitData() throws IOException, InterruptedException {
+ if (!initDataLoadRequired) {
+ return;
+ }
+ // initDataLoadRequired => initDataSource != null && initDataSpec != null
+ Assertions.checkNotNull(initDataSource);
+ Assertions.checkNotNull(initDataSpec);
+ feedDataToExtractor(initDataSource, initDataSpec, initSegmentEncrypted);
+ nextLoadPosition = 0;
+ initDataLoadRequired = false;
+ }
+
+ @RequiresNonNull("output")
+ private void loadMedia() throws IOException, InterruptedException {
+ if (!isMasterTimestampSource) {
+ timestampAdjuster.waitUntilInitialized();
+ } else if (timestampAdjuster.getFirstSampleTimestampUs() == TimestampAdjuster.DO_NOT_OFFSET) {
+ // We're the master and we haven't set the desired first sample timestamp yet.
+ timestampAdjuster.setFirstSampleTimestampUs(startTimeUs);
+ }
+ feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted);
+ }
+
+ /**
+ * Attempts to feed the given {@code dataSpec} to {@code this.extractor}. Whenever the operation
+ * concludes (because of a thrown exception or because the operation finishes), the number of fed
+ * bytes is written to {@code nextLoadPosition}.
+ */
+ @RequiresNonNull("output")
+ private void feedDataToExtractor(
+ DataSource dataSource, DataSpec dataSpec, boolean dataIsEncrypted)
+ throws IOException, InterruptedException {
+ // If we previously fed part of this chunk to the extractor, we need to skip it this time. For
+ // encrypted content we need to skip the data by reading it through the source, so as to ensure
+ // correct decryption of the remainder of the chunk. For clear content, we can request the
+ // remainder of the chunk directly.
+ DataSpec loadDataSpec;
+ boolean skipLoadedBytes;
+ if (dataIsEncrypted) {
+ loadDataSpec = dataSpec;
+ skipLoadedBytes = nextLoadPosition != 0;
+ } else {
+ loadDataSpec = dataSpec.subrange(nextLoadPosition);
+ skipLoadedBytes = false;
+ }
+ try {
+ ExtractorInput input = prepareExtraction(dataSource, loadDataSpec);
+ if (skipLoadedBytes) {
+ input.skipFully(nextLoadPosition);
+ }
+ try {
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ result = extractor.read(input, DUMMY_POSITION_HOLDER);
+ }
+ } finally {
+ nextLoadPosition = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+ @RequiresNonNull("output")
+ @EnsuresNonNull("extractor")
+ private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec)
+ throws IOException, InterruptedException {
+ long bytesToRead = dataSource.open(dataSpec);
+ DefaultExtractorInput extractorInput =
+ new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead);
+
+ if (extractor == null) {
+ long id3Timestamp = peekId3PrivTimestamp(extractorInput);
+ extractorInput.resetPeekPosition();
+
+ HlsExtractorFactory.Result result =
+ extractorFactory.createExtractor(
+ previousExtractor,
+ dataSpec.uri,
+ trackFormat,
+ muxedCaptionFormats,
+ timestampAdjuster,
+ dataSource.getResponseHeaders(),
+ extractorInput);
+ extractor = result.extractor;
+ isExtractorReusable = result.isReusable;
+ if (result.isPackedAudioExtractor) {
+ output.setSampleOffsetUs(
+ id3Timestamp != C.TIME_UNSET
+ ? timestampAdjuster.adjustTsTimestamp(id3Timestamp)
+ : startTimeUs);
+ } else {
+ // In case the container format changes mid-stream to non-packed-audio, we need to reset
+ // the timestamp offset.
+ output.setSampleOffsetUs(/* sampleOffsetUs= */ 0L);
+ }
+ output.onNewExtractor();
+ extractor.init(output);
+ }
+ output.setDrmInitData(drmInitData);
+ return extractorInput;
+ }
+
+ /**
+ * Peek the presentation timestamp of the first sample in the chunk from an ID3 PRIV as defined
+ * in the HLS spec, version 20, Section 3.4. Returns {@link C#TIME_UNSET} if the frame is not
+ * found. This method only modifies the peek position.
+ *
+ * @param input The {@link ExtractorInput} to obtain the PRIV frame from.
+ * @return The parsed, adjusted timestamp in microseconds
+ * @throws IOException If an error occurred peeking from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ try {
+ input.peekFully(scratchId3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+ } catch (EOFException e) {
+ // The input isn't long enough for there to be any ID3 data.
+ return C.TIME_UNSET;
+ }
+ scratchId3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);
+ int id = scratchId3Data.readUnsignedInt24();
+ if (id != Id3Decoder.ID3_TAG) {
+ return C.TIME_UNSET;
+ }
+ scratchId3Data.skipBytes(3); // version(2), flags(1).
+ int id3Size = scratchId3Data.readSynchSafeInt();
+ int requiredCapacity = id3Size + Id3Decoder.ID3_HEADER_LENGTH;
+ if (requiredCapacity > scratchId3Data.capacity()) {
+ byte[] data = scratchId3Data.data;
+ scratchId3Data.reset(requiredCapacity);
+ System.arraycopy(data, 0, scratchId3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+ }
+ input.peekFully(scratchId3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size);
+ Metadata metadata = id3Decoder.decode(scratchId3Data.data, id3Size);
+ if (metadata == null) {
+ return C.TIME_UNSET;
+ }
+ int metadataLength = metadata.length();
+ for (int i = 0; i < metadataLength; i++) {
+ Metadata.Entry frame = metadata.get(i);
+ if (frame instanceof PrivFrame) {
+ PrivFrame privFrame = (PrivFrame) frame;
+ if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {
+ System.arraycopy(
+ privFrame.privateData, 0, scratchId3Data.data, 0, 8 /* timestamp size */);
+ scratchId3Data.reset(8);
+ // The top 31 bits should be zeros, but explicitly zero them to wrap in the case that the
+ // streaming provider forgot. See: https://github.com/google/ExoPlayer/pull/3495.
+ return scratchId3Data.readLong() & 0x1FFFFFFFFL;
+ }
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ // Internal methods.
+
+ private static byte[] getEncryptionIvArray(String ivString) {
+ String trimmedIv;
+ if (Util.toLowerInvariant(ivString).startsWith("0x")) {
+ trimmedIv = ivString.substring(2);
+ } else {
+ trimmedIv = ivString;
+ }
+
+ byte[] ivData = new BigInteger(trimmedIv, /* radix= */ 16).toByteArray();
+ byte[] ivDataWithPadding = new byte[16];
+ int offset = ivData.length > 16 ? ivData.length - 16 : 0;
+ System.arraycopy(
+ ivData,
+ offset,
+ ivDataWithPadding,
+ ivDataWithPadding.length - ivData.length + offset,
+ ivData.length - offset);
+ return ivDataWithPadding;
+ }
+
+ /**
+ * If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original
+ * in order to decrypt the loaded data. Else returns the original.
+ *
+ * <p>{@code fullSegmentEncryptionKey} & {@code encryptionIv} can either both be null, or neither.
+ */
+ private static DataSource buildDataSource(
+ DataSource dataSource,
+ @Nullable byte[] fullSegmentEncryptionKey,
+ @Nullable byte[] encryptionIv) {
+ if (fullSegmentEncryptionKey != null) {
+ Assertions.checkNotNull(encryptionIv);
+ return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv);
+ }
+ return dataSource;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
new file mode 100644
index 0000000000..60aa5298c3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
@@ -0,0 +1,858 @@
+/*
+ * 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;
+
+import android.net.Uri;
+import android.text.TextUtils;
+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.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SequenceableLoader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
+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.HlsPlaylistTracker;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+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.TransferListener;
+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.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * A {@link MediaPeriod} that loads an HLS stream.
+ */
+public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback,
+ HlsPlaylistTracker.PlaylistEventListener {
+
+ private final HlsExtractorFactory extractorFactory;
+ private final HlsPlaylistTracker playlistTracker;
+ private final HlsDataSourceFactory dataSourceFactory;
+ @Nullable private final TransferListener mediaTransferListener;
+ private final DrmSessionManager<?> drmSessionManager;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final EventDispatcher eventDispatcher;
+ private final Allocator allocator;
+ private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;
+ private final TimestampAdjusterProvider timestampAdjusterProvider;
+ private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
+ private final boolean allowChunklessPreparation;
+ private final @HlsMediaSource.MetadataType int metadataType;
+ private final boolean useSessionKeys;
+
+ @Nullable private Callback callback;
+ private int pendingPrepareCount;
+ private @MonotonicNonNull TrackGroupArray trackGroups;
+ private HlsSampleStreamWrapper[] sampleStreamWrappers;
+ private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
+ // Maps sample stream wrappers to variant/rendition index by matching array positions.
+ private int[][] manifestUrlIndicesPerWrapper;
+ private SequenceableLoader compositeSequenceableLoader;
+ private boolean notifiedReadingStarted;
+
+ /**
+ * Creates an HLS media period.
+ *
+ * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the segments.
+ * @param playlistTracker A tracker for HLS playlists.
+ * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for segments
+ * and keys.
+ * @param mediaTransferListener The transfer listener to inform of any media data transfers. May
+ * be null if no listener is available.
+ * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession
+ * DrmSessions} with.
+ * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
+ * @param eventDispatcher A dispatcher to notify of events.
+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+ * @param compositeSequenceableLoaderFactory A factory to create composite {@link
+ * SequenceableLoader}s for when this media source loads data from multiple streams.
+ * @param allowChunklessPreparation Whether chunkless preparation is allowed.
+ * @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags.
+ */
+ public HlsMediaPeriod(
+ HlsExtractorFactory extractorFactory,
+ HlsPlaylistTracker playlistTracker,
+ HlsDataSourceFactory dataSourceFactory,
+ @Nullable TransferListener mediaTransferListener,
+ DrmSessionManager<?> drmSessionManager,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ EventDispatcher eventDispatcher,
+ Allocator allocator,
+ CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
+ boolean allowChunklessPreparation,
+ @HlsMediaSource.MetadataType int metadataType,
+ boolean useSessionKeys) {
+ this.extractorFactory = extractorFactory;
+ this.playlistTracker = playlistTracker;
+ this.dataSourceFactory = dataSourceFactory;
+ this.mediaTransferListener = mediaTransferListener;
+ this.drmSessionManager = drmSessionManager;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ this.eventDispatcher = eventDispatcher;
+ this.allocator = allocator;
+ this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
+ this.allowChunklessPreparation = allowChunklessPreparation;
+ this.metadataType = metadataType;
+ this.useSessionKeys = useSessionKeys;
+ compositeSequenceableLoader =
+ compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();
+ streamWrapperIndices = new IdentityHashMap<>();
+ timestampAdjusterProvider = new TimestampAdjusterProvider();
+ sampleStreamWrappers = new HlsSampleStreamWrapper[0];
+ enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0];
+ manifestUrlIndicesPerWrapper = new int[0][];
+ eventDispatcher.mediaPeriodCreated();
+ }
+
+ public void release() {
+ playlistTracker.removeListener(this);
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ sampleStreamWrapper.release();
+ }
+ callback = null;
+ eventDispatcher.mediaPeriodReleased();
+ }
+
+ @Override
+ public void prepare(Callback callback, long positionUs) {
+ this.callback = callback;
+ playlistTracker.addListener(this);
+ buildAndPrepareSampleStreamWrappers(positionUs);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ sampleStreamWrapper.maybeThrowPrepareError();
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ // trackGroups will only be null if period hasn't been prepared or has been released.
+ return Assertions.checkNotNull(trackGroups);
+ }
+
+ // TODO: When the master playlist does not de-duplicate variants by URL and allows Renditions with
+ // null URLs, this method must be updated to calculate stream keys that are compatible with those
+ // that may already be persisted for offline.
+ @Override
+ public List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
+ // See HlsMasterPlaylist.copy for interpretation of StreamKeys.
+ HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist());
+ boolean hasVariants = !masterPlaylist.variants.isEmpty();
+ int audioWrapperOffset = hasVariants ? 1 : 0;
+ // Subtitle sample stream wrappers are held last.
+ int subtitleWrapperOffset = sampleStreamWrappers.length - masterPlaylist.subtitles.size();
+
+ TrackGroupArray mainWrapperTrackGroups;
+ int mainWrapperPrimaryGroupIndex;
+ int[] mainWrapperVariantIndices;
+ if (hasVariants) {
+ HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0];
+ mainWrapperVariantIndices = manifestUrlIndicesPerWrapper[0];
+ mainWrapperTrackGroups = mainWrapper.getTrackGroups();
+ mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex();
+ } else {
+ mainWrapperVariantIndices = new int[0];
+ mainWrapperTrackGroups = TrackGroupArray.EMPTY;
+ mainWrapperPrimaryGroupIndex = 0;
+ }
+
+ List<StreamKey> streamKeys = new ArrayList<>();
+ boolean needsPrimaryTrackGroupSelection = false;
+ boolean hasPrimaryTrackGroupSelection = false;
+ for (TrackSelection trackSelection : trackSelections) {
+ TrackGroup trackSelectionGroup = trackSelection.getTrackGroup();
+ int mainWrapperTrackGroupIndex = mainWrapperTrackGroups.indexOf(trackSelectionGroup);
+ if (mainWrapperTrackGroupIndex != C.INDEX_UNSET) {
+ if (mainWrapperTrackGroupIndex == mainWrapperPrimaryGroupIndex) {
+ // Primary group in main wrapper.
+ hasPrimaryTrackGroupSelection = true;
+ for (int i = 0; i < trackSelection.length(); i++) {
+ int variantIndex = mainWrapperVariantIndices[trackSelection.getIndexInTrackGroup(i)];
+ streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex));
+ }
+ } else {
+ // Embedded group in main wrapper.
+ needsPrimaryTrackGroupSelection = true;
+ }
+ } else {
+ // Audio or subtitle group.
+ for (int i = audioWrapperOffset; i < sampleStreamWrappers.length; i++) {
+ TrackGroupArray wrapperTrackGroups = sampleStreamWrappers[i].getTrackGroups();
+ int selectedTrackGroupIndex = wrapperTrackGroups.indexOf(trackSelectionGroup);
+ if (selectedTrackGroupIndex != C.INDEX_UNSET) {
+ int groupIndexType =
+ i < subtitleWrapperOffset
+ ? HlsMasterPlaylist.GROUP_INDEX_AUDIO
+ : HlsMasterPlaylist.GROUP_INDEX_SUBTITLE;
+ int[] selectedWrapperUrlIndices = manifestUrlIndicesPerWrapper[i];
+ for (int trackIndex = 0; trackIndex < trackSelection.length(); trackIndex++) {
+ int renditionIndex =
+ selectedWrapperUrlIndices[trackSelection.getIndexInTrackGroup(trackIndex)];
+ streamKeys.add(new StreamKey(groupIndexType, renditionIndex));
+ }
+ break;
+ }
+ }
+ }
+ }
+ if (needsPrimaryTrackGroupSelection && !hasPrimaryTrackGroupSelection) {
+ // A track selection includes a variant-embedded track, but no variant is added yet. We use
+ // the valid variant with the lowest bitrate to reduce overhead.
+ int lowestBitrateIndex = mainWrapperVariantIndices[0];
+ int lowestBitrate = masterPlaylist.variants.get(mainWrapperVariantIndices[0]).format.bitrate;
+ for (int i = 1; i < mainWrapperVariantIndices.length; i++) {
+ int variantBitrate =
+ masterPlaylist.variants.get(mainWrapperVariantIndices[i]).format.bitrate;
+ if (variantBitrate < lowestBitrate) {
+ lowestBitrate = variantBitrate;
+ lowestBitrateIndex = mainWrapperVariantIndices[i];
+ }
+ }
+ streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex));
+ }
+ return streamKeys;
+ }
+
+ @Override
+ public long selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs) {
+ // Map each selection and stream onto a child period index.
+ int[] streamChildIndices = new int[selections.length];
+ int[] selectionChildIndices = new int[selections.length];
+ for (int i = 0; i < selections.length; i++) {
+ streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET
+ : streamWrapperIndices.get(streams[i]);
+ selectionChildIndices[i] = C.INDEX_UNSET;
+ if (selections[i] != null) {
+ TrackGroup trackGroup = selections[i].getTrackGroup();
+ for (int j = 0; j < sampleStreamWrappers.length; j++) {
+ if (sampleStreamWrappers[j].getTrackGroups().indexOf(trackGroup) != C.INDEX_UNSET) {
+ selectionChildIndices[i] = j;
+ break;
+ }
+ }
+ }
+ }
+
+ boolean forceReset = false;
+ streamWrapperIndices.clear();
+ // Select tracks for each child, copying the resulting streams back into a new streams array.
+ SampleStream[] newStreams = new SampleStream[selections.length];
+ @NullableType SampleStream[] childStreams = new SampleStream[selections.length];
+ @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length];
+ int newEnabledSampleStreamWrapperCount = 0;
+ HlsSampleStreamWrapper[] newEnabledSampleStreamWrappers =
+ new HlsSampleStreamWrapper[sampleStreamWrappers.length];
+ for (int i = 0; i < sampleStreamWrappers.length; i++) {
+ for (int j = 0; j < selections.length; j++) {
+ childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
+ childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
+ }
+ HlsSampleStreamWrapper sampleStreamWrapper = sampleStreamWrappers[i];
+ boolean wasReset = sampleStreamWrapper.selectTracks(childSelections, mayRetainStreamFlags,
+ childStreams, streamResetFlags, positionUs, forceReset);
+ boolean wrapperEnabled = false;
+ for (int j = 0; j < selections.length; j++) {
+ SampleStream childStream = childStreams[j];
+ if (selectionChildIndices[j] == i) {
+ // Assert that the child provided a stream for the selection.
+ Assertions.checkNotNull(childStream);
+ newStreams[j] = childStream;
+ wrapperEnabled = true;
+ streamWrapperIndices.put(childStream, i);
+ } else if (streamChildIndices[j] == i) {
+ // Assert that the child cleared any previous stream.
+ Assertions.checkState(childStream == null);
+ }
+ }
+ if (wrapperEnabled) {
+ newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper;
+ if (newEnabledSampleStreamWrapperCount++ == 0) {
+ // The first enabled wrapper is responsible for initializing timestamp adjusters. This
+ // way, if enabled, variants are responsible. Else audio renditions. Else text renditions.
+ sampleStreamWrapper.setIsTimestampMaster(true);
+ if (wasReset || enabledSampleStreamWrappers.length == 0
+ || sampleStreamWrapper != enabledSampleStreamWrappers[0]) {
+ // The wrapper responsible for initializing the timestamp adjusters was reset or
+ // changed. We need to reset the timestamp adjuster provider and all other wrappers.
+ timestampAdjusterProvider.reset();
+ forceReset = true;
+ }
+ } else {
+ sampleStreamWrapper.setIsTimestampMaster(false);
+ }
+ }
+ }
+ // Copy the new streams back into the streams array.
+ System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
+ // Update the local state.
+ enabledSampleStreamWrappers =
+ Util.nullSafeArrayCopy(newEnabledSampleStreamWrappers, newEnabledSampleStreamWrapperCount);
+ compositeSequenceableLoader =
+ compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(
+ enabledSampleStreamWrappers);
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {
+ sampleStreamWrapper.discardBuffer(positionUs, toKeyframe);
+ }
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ compositeSequenceableLoader.reevaluateBuffer(positionUs);
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (trackGroups == null) {
+ // Preparation is still going on.
+ for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) {
+ wrapper.continuePreparing();
+ }
+ return false;
+ } else {
+ return compositeSequenceableLoader.continueLoading(positionUs);
+ }
+ }
+
+ @Override
+ public boolean isLoading() {
+ return compositeSequenceableLoader.isLoading();
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return compositeSequenceableLoader.getNextLoadPositionUs();
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ if (!notifiedReadingStarted) {
+ eventDispatcher.readingStarted();
+ notifiedReadingStarted = true;
+ }
+ return C.TIME_UNSET;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return compositeSequenceableLoader.getBufferedPositionUs();
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ if (enabledSampleStreamWrappers.length > 0) {
+ // We need to reset all wrappers if the one responsible for initializing timestamp adjusters
+ // is reset. Else each wrapper can decide whether to reset independently.
+ boolean forceReset = enabledSampleStreamWrappers[0].seekToUs(positionUs, false);
+ for (int i = 1; i < enabledSampleStreamWrappers.length; i++) {
+ enabledSampleStreamWrappers[i].seekToUs(positionUs, forceReset);
+ }
+ if (forceReset) {
+ timestampAdjusterProvider.reset();
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return positionUs;
+ }
+
+ // HlsSampleStreamWrapper.Callback implementation.
+
+ @Override
+ public void onPrepared() {
+ if (--pendingPrepareCount > 0) {
+ return;
+ }
+
+ int totalTrackGroupCount = 0;
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length;
+ }
+ TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
+ int trackGroupIndex = 0;
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ int wrapperTrackGroupCount = sampleStreamWrapper.getTrackGroups().length;
+ for (int j = 0; j < wrapperTrackGroupCount; j++) {
+ trackGroupArray[trackGroupIndex++] = sampleStreamWrapper.getTrackGroups().get(j);
+ }
+ }
+ trackGroups = new TrackGroupArray(trackGroupArray);
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void onPlaylistRefreshRequired(Uri url) {
+ playlistTracker.refreshPlaylist(url);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) {
+ callback.onContinueLoadingRequested(this);
+ }
+
+ // PlaylistListener implementation.
+
+ @Override
+ public void onPlaylistChanged() {
+ callback.onContinueLoadingRequested(this);
+ }
+
+ @Override
+ public boolean onPlaylistError(Uri url, long blacklistDurationMs) {
+ boolean noBlacklistingFailure = true;
+ for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) {
+ noBlacklistingFailure &= streamWrapper.onPlaylistError(url, blacklistDurationMs);
+ }
+ callback.onContinueLoadingRequested(this);
+ return noBlacklistingFailure;
+ }
+
+ // Internal methods.
+
+ private void buildAndPrepareSampleStreamWrappers(long positionUs) {
+ HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist());
+ Map<String, DrmInitData> overridingDrmInitData =
+ useSessionKeys
+ ? deriveOverridingDrmInitData(masterPlaylist.sessionKeyDrmInitData)
+ : Collections.emptyMap();
+
+ boolean hasVariants = !masterPlaylist.variants.isEmpty();
+ List<Rendition> audioRenditions = masterPlaylist.audios;
+ List<Rendition> subtitleRenditions = masterPlaylist.subtitles;
+
+ pendingPrepareCount = 0;
+ ArrayList<HlsSampleStreamWrapper> sampleStreamWrappers = new ArrayList<>();
+ ArrayList<int[]> manifestUrlIndicesPerWrapper = new ArrayList<>();
+
+ if (hasVariants) {
+ buildAndPrepareMainSampleStreamWrapper(
+ masterPlaylist,
+ positionUs,
+ sampleStreamWrappers,
+ manifestUrlIndicesPerWrapper,
+ overridingDrmInitData);
+ }
+
+ // TODO: Build video stream wrappers here.
+
+ buildAndPrepareAudioSampleStreamWrappers(
+ positionUs,
+ audioRenditions,
+ sampleStreamWrappers,
+ manifestUrlIndicesPerWrapper,
+ overridingDrmInitData);
+
+ // Subtitle stream wrappers. We can always use master playlist information to prepare these.
+ for (int i = 0; i < subtitleRenditions.size(); i++) {
+ Rendition subtitleRendition = subtitleRenditions.get(i);
+ HlsSampleStreamWrapper sampleStreamWrapper =
+ buildSampleStreamWrapper(
+ C.TRACK_TYPE_TEXT,
+ new Uri[] {subtitleRendition.url},
+ new Format[] {subtitleRendition.format},
+ null,
+ Collections.emptyList(),
+ overridingDrmInitData,
+ positionUs);
+ manifestUrlIndicesPerWrapper.add(new int[] {i});
+ sampleStreamWrappers.add(sampleStreamWrapper);
+ sampleStreamWrapper.prepareWithMasterPlaylistInfo(
+ new TrackGroup[] {new TrackGroup(subtitleRendition.format)},
+ /* primaryTrackGroupIndex= */ 0);
+ }
+
+ this.sampleStreamWrappers = sampleStreamWrappers.toArray(new HlsSampleStreamWrapper[0]);
+ this.manifestUrlIndicesPerWrapper = manifestUrlIndicesPerWrapper.toArray(new int[0][]);
+ pendingPrepareCount = this.sampleStreamWrappers.length;
+ // Set timestamp master and trigger preparation (if not already prepared)
+ this.sampleStreamWrappers[0].setIsTimestampMaster(true);
+ for (HlsSampleStreamWrapper sampleStreamWrapper : this.sampleStreamWrappers) {
+ sampleStreamWrapper.continuePreparing();
+ }
+ // All wrappers are enabled during preparation.
+ enabledSampleStreamWrappers = this.sampleStreamWrappers;
+ }
+
+ /**
+ * This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}.
+ *
+ * <p>The main sample stream wrapper is the first element of {@link #sampleStreamWrappers}. It
+ * provides {@link SampleStream}s for the variant urls in the master playlist. It may be adaptive
+ * and may contain multiple muxed tracks.
+ *
+ * <p>If chunkless preparation is allowed, the media period will try preparation without segment
+ * downloads. This is only possible if variants contain the CODECS attribute. If not, traditional
+ * preparation with segment downloads will take place. The following points apply to chunkless
+ * preparation:
+ *
+ * <ul>
+ * <li>A muxed audio track will be exposed if the codecs list contain an audio entry and the
+ * master playlist either contains an EXT-X-MEDIA tag without the URI attribute or does not
+ * contain any EXT-X-MEDIA tag.
+ * <li>Closed captions will only be exposed if they are declared by the master playlist.
+ * <li>An ID3 track is exposed preemptively, in case the segments contain an ID3 track.
+ * </ul>
+ *
+ * @param masterPlaylist The HLS master playlist.
+ * @param positionUs If preparation requires any chunk downloads, the position in microseconds at
+ * which downloading should start. Ignored otherwise.
+ * @param sampleStreamWrappers List to which the built main sample stream wrapper should be added.
+ * @param manifestUrlIndicesPerWrapper List to which the selected variant indices should be added.
+ * @param overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type
+ * (i.e. {@link DrmInitData#schemeType}).
+ */
+ private void buildAndPrepareMainSampleStreamWrapper(
+ HlsMasterPlaylist masterPlaylist,
+ long positionUs,
+ List<HlsSampleStreamWrapper> sampleStreamWrappers,
+ List<int[]> manifestUrlIndicesPerWrapper,
+ Map<String, DrmInitData> overridingDrmInitData) {
+ int[] variantTypes = new int[masterPlaylist.variants.size()];
+ int videoVariantCount = 0;
+ int audioVariantCount = 0;
+ for (int i = 0; i < masterPlaylist.variants.size(); i++) {
+ Variant variant = masterPlaylist.variants.get(i);
+ Format format = variant.format;
+ if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) {
+ variantTypes[i] = C.TRACK_TYPE_VIDEO;
+ videoVariantCount++;
+ } else if (Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_AUDIO) != null) {
+ variantTypes[i] = C.TRACK_TYPE_AUDIO;
+ audioVariantCount++;
+ } else {
+ variantTypes[i] = C.TRACK_TYPE_UNKNOWN;
+ }
+ }
+ boolean useVideoVariantsOnly = false;
+ boolean useNonAudioVariantsOnly = false;
+ int selectedVariantsCount = variantTypes.length;
+ if (videoVariantCount > 0) {
+ // We've identified some variants as definitely containing video. Assume variants within the
+ // master playlist are marked consistently, and hence that we have the full set. Filter out
+ // any other variants, which are likely to be audio only.
+ useVideoVariantsOnly = true;
+ selectedVariantsCount = videoVariantCount;
+ } else if (audioVariantCount < variantTypes.length) {
+ // We've identified some variants, but not all, as being audio only. Filter them out to leave
+ // the remaining variants, which are likely to contain video.
+ useNonAudioVariantsOnly = true;
+ selectedVariantsCount = variantTypes.length - audioVariantCount;
+ }
+ Uri[] selectedPlaylistUrls = new Uri[selectedVariantsCount];
+ Format[] selectedPlaylistFormats = new Format[selectedVariantsCount];
+ int[] selectedVariantIndices = new int[selectedVariantsCount];
+ int outIndex = 0;
+ for (int i = 0; i < masterPlaylist.variants.size(); i++) {
+ if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO)
+ && (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) {
+ Variant variant = masterPlaylist.variants.get(i);
+ selectedPlaylistUrls[outIndex] = variant.url;
+ selectedPlaylistFormats[outIndex] = variant.format;
+ selectedVariantIndices[outIndex++] = i;
+ }
+ }
+ String codecs = selectedPlaylistFormats[0].codecs;
+ HlsSampleStreamWrapper sampleStreamWrapper =
+ buildSampleStreamWrapper(
+ C.TRACK_TYPE_DEFAULT,
+ selectedPlaylistUrls,
+ selectedPlaylistFormats,
+ masterPlaylist.muxedAudioFormat,
+ masterPlaylist.muxedCaptionFormats,
+ overridingDrmInitData,
+ positionUs);
+ sampleStreamWrappers.add(sampleStreamWrapper);
+ manifestUrlIndicesPerWrapper.add(selectedVariantIndices);
+ if (allowChunklessPreparation && codecs != null) {
+ boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null;
+ boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null;
+ List<TrackGroup> muxedTrackGroups = new ArrayList<>();
+ if (variantsContainVideoCodecs) {
+ Format[] videoFormats = new Format[selectedVariantsCount];
+ for (int i = 0; i < videoFormats.length; i++) {
+ videoFormats[i] = deriveVideoFormat(selectedPlaylistFormats[i]);
+ }
+ muxedTrackGroups.add(new TrackGroup(videoFormats));
+
+ if (variantsContainAudioCodecs
+ && (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) {
+ muxedTrackGroups.add(
+ new TrackGroup(
+ deriveAudioFormat(
+ selectedPlaylistFormats[0],
+ masterPlaylist.muxedAudioFormat,
+ /* isPrimaryTrackInVariant= */ false)));
+ }
+ List<Format> ccFormats = masterPlaylist.muxedCaptionFormats;
+ if (ccFormats != null) {
+ for (int i = 0; i < ccFormats.size(); i++) {
+ muxedTrackGroups.add(new TrackGroup(ccFormats.get(i)));
+ }
+ }
+ } else if (variantsContainAudioCodecs) {
+ // Variants only contain audio.
+ Format[] audioFormats = new Format[selectedVariantsCount];
+ for (int i = 0; i < audioFormats.length; i++) {
+ audioFormats[i] =
+ deriveAudioFormat(
+ /* variantFormat= */ selectedPlaylistFormats[i],
+ masterPlaylist.muxedAudioFormat,
+ /* isPrimaryTrackInVariant= */ true);
+ }
+ muxedTrackGroups.add(new TrackGroup(audioFormats));
+ } else {
+ // Variants contain codecs but no video or audio entries could be identified.
+ throw new IllegalArgumentException("Unexpected codecs attribute: " + codecs);
+ }
+
+ TrackGroup id3TrackGroup =
+ new TrackGroup(
+ Format.createSampleFormat(
+ /* id= */ "ID3",
+ MimeTypes.APPLICATION_ID3,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* drmInitData= */ null));
+ muxedTrackGroups.add(id3TrackGroup);
+
+ sampleStreamWrapper.prepareWithMasterPlaylistInfo(
+ muxedTrackGroups.toArray(new TrackGroup[0]),
+ /* primaryTrackGroupIndex= */ 0,
+ /* optionalTrackGroupsIndices= */ muxedTrackGroups.indexOf(id3TrackGroup));
+ }
+ }
+
+ private void buildAndPrepareAudioSampleStreamWrappers(
+ long positionUs,
+ List<Rendition> audioRenditions,
+ List<HlsSampleStreamWrapper> sampleStreamWrappers,
+ List<int[]> manifestUrlsIndicesPerWrapper,
+ Map<String, DrmInitData> overridingDrmInitData) {
+ ArrayList<Uri> scratchPlaylistUrls =
+ new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
+ ArrayList<Format> scratchPlaylistFormats =
+ new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
+ ArrayList<Integer> scratchIndicesList =
+ new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
+ HashSet<String> alreadyGroupedNames = new HashSet<>();
+ for (int renditionByNameIndex = 0;
+ renditionByNameIndex < audioRenditions.size();
+ renditionByNameIndex++) {
+ String name = audioRenditions.get(renditionByNameIndex).name;
+ if (!alreadyGroupedNames.add(name)) {
+ // This name already has a corresponding group.
+ continue;
+ }
+
+ boolean renditionsHaveCodecs = true;
+ scratchPlaylistUrls.clear();
+ scratchPlaylistFormats.clear();
+ scratchIndicesList.clear();
+ // Group all renditions with matching name.
+ for (int renditionIndex = 0; renditionIndex < audioRenditions.size(); renditionIndex++) {
+ if (Util.areEqual(name, audioRenditions.get(renditionIndex).name)) {
+ Rendition rendition = audioRenditions.get(renditionIndex);
+ scratchIndicesList.add(renditionIndex);
+ scratchPlaylistUrls.add(rendition.url);
+ scratchPlaylistFormats.add(rendition.format);
+ renditionsHaveCodecs &= rendition.format.codecs != null;
+ }
+ }
+
+ HlsSampleStreamWrapper sampleStreamWrapper =
+ buildSampleStreamWrapper(
+ C.TRACK_TYPE_AUDIO,
+ scratchPlaylistUrls.toArray(Util.castNonNullTypeArray(new Uri[0])),
+ scratchPlaylistFormats.toArray(new Format[0]),
+ /* muxedAudioFormat= */ null,
+ /* muxedCaptionFormats= */ Collections.emptyList(),
+ overridingDrmInitData,
+ positionUs);
+ manifestUrlsIndicesPerWrapper.add(Util.toArray(scratchIndicesList));
+ sampleStreamWrappers.add(sampleStreamWrapper);
+
+ if (allowChunklessPreparation && renditionsHaveCodecs) {
+ Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]);
+ sampleStreamWrapper.prepareWithMasterPlaylistInfo(
+ new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0);
+ }
+ }
+ }
+
+ private HlsSampleStreamWrapper buildSampleStreamWrapper(
+ int trackType,
+ Uri[] playlistUrls,
+ Format[] playlistFormats,
+ @Nullable Format muxedAudioFormat,
+ @Nullable List<Format> muxedCaptionFormats,
+ Map<String, DrmInitData> overridingDrmInitData,
+ long positionUs) {
+ HlsChunkSource defaultChunkSource =
+ new HlsChunkSource(
+ extractorFactory,
+ playlistTracker,
+ playlistUrls,
+ playlistFormats,
+ dataSourceFactory,
+ mediaTransferListener,
+ timestampAdjusterProvider,
+ muxedCaptionFormats);
+ return new HlsSampleStreamWrapper(
+ trackType,
+ /* callback= */ this,
+ defaultChunkSource,
+ overridingDrmInitData,
+ allocator,
+ positionUs,
+ muxedAudioFormat,
+ drmSessionManager,
+ loadErrorHandlingPolicy,
+ eventDispatcher,
+ metadataType);
+ }
+
+ private static Map<String, DrmInitData> deriveOverridingDrmInitData(
+ List<DrmInitData> sessionKeyDrmInitData) {
+ ArrayList<DrmInitData> mutableSessionKeyDrmInitData = new ArrayList<>(sessionKeyDrmInitData);
+ HashMap<String, DrmInitData> drmInitDataBySchemeType = new HashMap<>();
+ for (int i = 0; i < mutableSessionKeyDrmInitData.size(); i++) {
+ DrmInitData drmInitData = sessionKeyDrmInitData.get(i);
+ String scheme = drmInitData.schemeType;
+ // Merge any subsequent drmInitData instances that have the same scheme type. This is valid
+ // due to the assumptions documented on HlsMediaSource.Builder.setUseSessionKeys, and is
+ // necessary to get data for different CDNs (e.g. Widevine and PlayReady) into a single
+ // drmInitData.
+ int j = i + 1;
+ while (j < mutableSessionKeyDrmInitData.size()) {
+ DrmInitData nextDrmInitData = mutableSessionKeyDrmInitData.get(j);
+ if (TextUtils.equals(nextDrmInitData.schemeType, scheme)) {
+ drmInitData = drmInitData.merge(nextDrmInitData);
+ mutableSessionKeyDrmInitData.remove(j);
+ } else {
+ j++;
+ }
+ }
+ drmInitDataBySchemeType.put(scheme, drmInitData);
+ }
+ return drmInitDataBySchemeType;
+ }
+
+ private static Format deriveVideoFormat(Format variantFormat) {
+ String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO);
+ String sampleMimeType = MimeTypes.getMediaMimeType(codecs);
+ return Format.createVideoContainerFormat(
+ variantFormat.id,
+ variantFormat.label,
+ variantFormat.containerMimeType,
+ sampleMimeType,
+ codecs,
+ variantFormat.metadata,
+ variantFormat.bitrate,
+ variantFormat.width,
+ variantFormat.height,
+ variantFormat.frameRate,
+ /* initializationData= */ null,
+ variantFormat.selectionFlags,
+ variantFormat.roleFlags);
+ }
+
+ private static Format deriveAudioFormat(
+ Format variantFormat, @Nullable Format mediaTagFormat, boolean isPrimaryTrackInVariant) {
+ String codecs;
+ Metadata metadata;
+ int channelCount = Format.NO_VALUE;
+ int selectionFlags = 0;
+ int roleFlags = 0;
+ String language = null;
+ String label = null;
+ if (mediaTagFormat != null) {
+ codecs = mediaTagFormat.codecs;
+ metadata = mediaTagFormat.metadata;
+ channelCount = mediaTagFormat.channelCount;
+ selectionFlags = mediaTagFormat.selectionFlags;
+ roleFlags = mediaTagFormat.roleFlags;
+ language = mediaTagFormat.language;
+ label = mediaTagFormat.label;
+ } else {
+ codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO);
+ metadata = variantFormat.metadata;
+ if (isPrimaryTrackInVariant) {
+ channelCount = variantFormat.channelCount;
+ selectionFlags = variantFormat.selectionFlags;
+ roleFlags = variantFormat.roleFlags;
+ language = variantFormat.language;
+ label = variantFormat.label;
+ }
+ }
+ String sampleMimeType = MimeTypes.getMediaMimeType(codecs);
+ int bitrate = isPrimaryTrackInVariant ? variantFormat.bitrate : Format.NO_VALUE;
+ return Format.createAudioContainerFormat(
+ variantFormat.id,
+ label,
+ variantFormat.containerMimeType,
+ sampleMimeType,
+ codecs,
+ metadata,
+ bitrate,
+ channelCount,
+ /* sampleRate= */ Format.NO_VALUE,
+ /* initializationData= */ null,
+ selectionFlags,
+ roleFlags,
+ language);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
new file mode 100644
index 0000000000..2fa49e13f0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
@@ -0,0 +1,528 @@
+/*
+ * 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;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.net.Uri;
+import android.os.Handler;
+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.ExoPlayerLibraryInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.BaseMediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SequenceableLoader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SinglePeriodTimeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParserFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.util.List;
+
+/** An HLS {@link MediaSource}. */
+public final class HlsMediaSource extends BaseMediaSource
+ implements HlsPlaylistTracker.PrimaryPlaylistListener {
+
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.hls");
+ }
+
+ /**
+ * The types of metadata that can be extracted from HLS streams.
+ *
+ * <p>Allowed values:
+ *
+ * <ul>
+ * <li>{@link #METADATA_TYPE_ID3}
+ * <li>{@link #METADATA_TYPE_EMSG}
+ * </ul>
+ *
+ * <p>See {@link Factory#setMetadataType(int)}.
+ */
+ @Documented
+ @Retention(SOURCE)
+ @IntDef({METADATA_TYPE_ID3, METADATA_TYPE_EMSG})
+ public @interface MetadataType {}
+
+ /** Type for ID3 metadata in HLS streams. */
+ public static final int METADATA_TYPE_ID3 = 1;
+ /** Type for ESMG metadata in HLS streams. */
+ public static final int METADATA_TYPE_EMSG = 3;
+
+ /** Factory for {@link HlsMediaSource}s. */
+ public static final class Factory implements MediaSourceFactory {
+
+ private final HlsDataSourceFactory hlsDataSourceFactory;
+
+ private HlsExtractorFactory extractorFactory;
+ private HlsPlaylistParserFactory playlistParserFactory;
+ @Nullable private List<StreamKey> streamKeys;
+ private HlsPlaylistTracker.Factory playlistTrackerFactory;
+ private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
+ private DrmSessionManager<?> drmSessionManager;
+ private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private boolean allowChunklessPreparation;
+ @MetadataType private int metadataType;
+ private boolean useSessionKeys;
+ private boolean isCreateCalled;
+ @Nullable private Object tag;
+
+ /**
+ * Creates a new factory for {@link HlsMediaSource}s.
+ *
+ * @param dataSourceFactory A data source factory that will be wrapped by a {@link
+ * DefaultHlsDataSourceFactory} to create {@link DataSource}s for manifests, segments and
+ * keys.
+ */
+ public Factory(DataSource.Factory dataSourceFactory) {
+ this(new DefaultHlsDataSourceFactory(dataSourceFactory));
+ }
+
+ /**
+ * Creates a new factory for {@link HlsMediaSource}s.
+ *
+ * @param hlsDataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for
+ * manifests, segments and keys.
+ */
+ public Factory(HlsDataSourceFactory hlsDataSourceFactory) {
+ this.hlsDataSourceFactory = Assertions.checkNotNull(hlsDataSourceFactory);
+ playlistParserFactory = new DefaultHlsPlaylistParserFactory();
+ playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY;
+ extractorFactory = HlsExtractorFactory.DEFAULT;
+ drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
+ compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
+ metadataType = METADATA_TYPE_ID3;
+ }
+
+ /**
+ * Sets a tag for the media source which will be published in the {@link
+ * org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline} of the source as {@link
+ * org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Window#tag}.
+ *
+ * @param tag A tag for the media source.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setTag(@Nullable Object tag) {
+ Assertions.checkState(!isCreateCalled);
+ this.tag = tag;
+ return this;
+ }
+
+ /**
+ * Sets the factory for {@link Extractor}s for the segments. The default value is {@link
+ * HlsExtractorFactory#DEFAULT}.
+ *
+ * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the
+ * segments.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) {
+ Assertions.checkState(!isCreateCalled);
+ this.extractorFactory = Assertions.checkNotNull(extractorFactory);
+ return this;
+ }
+
+ /**
+ * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link
+ * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.
+ *
+ * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}.
+ *
+ * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
+ Assertions.checkState(!isCreateCalled);
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ return this;
+ }
+
+ /**
+ * Sets the minimum number of times to retry if a loading error occurs. The default value is
+ * {@link DefaultLoadErrorHandlingPolicy#DEFAULT_MIN_LOADABLE_RETRY_COUNT}.
+ *
+ * <p>Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with
+ * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int)
+ * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)}
+ *
+ * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead.
+ */
+ @Deprecated
+ public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {
+ Assertions.checkState(!isCreateCalled);
+ this.loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount);
+ return this;
+ }
+
+ /**
+ * Sets the factory from which playlist parsers will be obtained. The default value is a {@link
+ * DefaultHlsPlaylistParserFactory}.
+ *
+ * @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setPlaylistParserFactory(HlsPlaylistParserFactory playlistParserFactory) {
+ Assertions.checkState(!isCreateCalled);
+ this.playlistParserFactory = Assertions.checkNotNull(playlistParserFactory);
+ return this;
+ }
+
+ /**
+ * Sets the {@link HlsPlaylistTracker} factory. The default value is {@link
+ * DefaultHlsPlaylistTracker#FACTORY}.
+ *
+ * @param playlistTrackerFactory A factory for {@link HlsPlaylistTracker} instances.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setPlaylistTrackerFactory(HlsPlaylistTracker.Factory playlistTrackerFactory) {
+ Assertions.checkState(!isCreateCalled);
+ this.playlistTrackerFactory = Assertions.checkNotNull(playlistTrackerFactory);
+ return this;
+ }
+
+ /**
+ * Sets the factory to create composite {@link SequenceableLoader}s for when this media source
+ * loads data from multiple streams (video, audio etc...). The default is an instance of {@link
+ * DefaultCompositeSequenceableLoaderFactory}.
+ *
+ * @param compositeSequenceableLoaderFactory A factory to create composite {@link
+ * SequenceableLoader}s for when this media source loads data from multiple streams (video,
+ * audio etc...).
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setCompositeSequenceableLoaderFactory(
+ CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) {
+ Assertions.checkState(!isCreateCalled);
+ this.compositeSequenceableLoaderFactory =
+ Assertions.checkNotNull(compositeSequenceableLoaderFactory);
+ return this;
+ }
+
+ /**
+ * Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads
+ * will be enabled for streams that provide sufficient information in their master playlist.
+ *
+ * @param allowChunklessPreparation Whether chunkless preparation is allowed.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setAllowChunklessPreparation(boolean allowChunklessPreparation) {
+ Assertions.checkState(!isCreateCalled);
+ this.allowChunklessPreparation = allowChunklessPreparation;
+ return this;
+ }
+
+ /**
+ * Sets the type of metadata to extract from the HLS source (defaults to {@link
+ * #METADATA_TYPE_ID3}).
+ *
+ * <p>HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is
+ * wrapped in an EMSG box [<a href="https://aomediacodec.github.io/av1-id3/">spec</a>].
+ *
+ * <p>If this is set to {@link #METADATA_TYPE_ID3} then raw ID3 metadata of will be extracted
+ * from TS sources. From fMP4 streams EMSGs containing metadata of this type (in the variant
+ * stream only) will be unwrapped to expose the inner data. All other in-band metadata will be
+ * dropped.
+ *
+ * <p>If this is set to {@link #METADATA_TYPE_EMSG} then all EMSG data from the fMP4 variant
+ * stream will be extracted. No metadata will be extracted from TS streams, since they don't
+ * support EMSG.
+ *
+ * @param metadataType The type of metadata to extract.
+ * @return This factory, for convenience.
+ */
+ public Factory setMetadataType(@MetadataType int metadataType) {
+ Assertions.checkState(!isCreateCalled);
+ this.metadataType = metadataType;
+ return this;
+ }
+
+ /**
+ * Sets whether to use #EXT-X-SESSION-KEY tags provided in the master playlist. If enabled, it's
+ * assumed that any single session key declared in the master playlist can be used to obtain all
+ * of the keys required for playback. For media where this is not true, this option should not
+ * be enabled.
+ *
+ * @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags.
+ * @return This factory, for convenience.
+ */
+ public Factory setUseSessionKeys(boolean useSessionKeys) {
+ this.useSessionKeys = useSessionKeys;
+ return this;
+ }
+
+ /**
+ * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler,
+ * MediaSourceEventListener)} instead.
+ */
+ @Deprecated
+ public HlsMediaSource createMediaSource(
+ Uri playlistUri,
+ @Nullable Handler eventHandler,
+ @Nullable MediaSourceEventListener eventListener) {
+ HlsMediaSource mediaSource = createMediaSource(playlistUri);
+ if (eventHandler != null && eventListener != null) {
+ mediaSource.addEventListener(eventHandler, eventListener);
+ }
+ return mediaSource;
+ }
+
+ /**
+ * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
+ * default value is {@link DrmSessionManager#DUMMY}.
+ *
+ * @param drmSessionManager The {@link DrmSessionManager}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ @Override
+ public Factory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
+ Assertions.checkState(!isCreateCalled);
+ this.drmSessionManager = drmSessionManager;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link HlsMediaSource} using the current parameters.
+ *
+ * @return The new {@link HlsMediaSource}.
+ */
+ @Override
+ public HlsMediaSource createMediaSource(Uri playlistUri) {
+ isCreateCalled = true;
+ if (streamKeys != null) {
+ playlistParserFactory =
+ new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys);
+ }
+ return new HlsMediaSource(
+ playlistUri,
+ hlsDataSourceFactory,
+ extractorFactory,
+ compositeSequenceableLoaderFactory,
+ drmSessionManager,
+ loadErrorHandlingPolicy,
+ playlistTrackerFactory.createTracker(
+ hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),
+ allowChunklessPreparation,
+ metadataType,
+ useSessionKeys,
+ tag);
+ }
+
+ @Override
+ public Factory setStreamKeys(List<StreamKey> streamKeys) {
+ Assertions.checkState(!isCreateCalled);
+ this.streamKeys = streamKeys;
+ return this;
+ }
+
+ @Override
+ public int[] getSupportedTypes() {
+ return new int[] {C.TYPE_HLS};
+ }
+
+ }
+
+ private final HlsExtractorFactory extractorFactory;
+ private final Uri manifestUri;
+ private final HlsDataSourceFactory dataSourceFactory;
+ private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
+ private final DrmSessionManager<?> drmSessionManager;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final boolean allowChunklessPreparation;
+ private final @MetadataType int metadataType;
+ private final boolean useSessionKeys;
+ private final HlsPlaylistTracker playlistTracker;
+ @Nullable private final Object tag;
+
+ @Nullable private TransferListener mediaTransferListener;
+
+ private HlsMediaSource(
+ Uri manifestUri,
+ HlsDataSourceFactory dataSourceFactory,
+ HlsExtractorFactory extractorFactory,
+ CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
+ DrmSessionManager<?> drmSessionManager,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ HlsPlaylistTracker playlistTracker,
+ boolean allowChunklessPreparation,
+ @MetadataType int metadataType,
+ boolean useSessionKeys,
+ @Nullable Object tag) {
+ this.manifestUri = manifestUri;
+ this.dataSourceFactory = dataSourceFactory;
+ this.extractorFactory = extractorFactory;
+ this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
+ this.drmSessionManager = drmSessionManager;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ this.playlistTracker = playlistTracker;
+ this.allowChunklessPreparation = allowChunklessPreparation;
+ this.metadataType = metadataType;
+ this.useSessionKeys = useSessionKeys;
+ this.tag = tag;
+ }
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return tag;
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ this.mediaTransferListener = mediaTransferListener;
+ drmSessionManager.prepare();
+ EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
+ playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this);
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ playlistTracker.maybeThrowPrimaryPlaylistRefreshError();
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ EventDispatcher eventDispatcher = createEventDispatcher(id);
+ return new HlsMediaPeriod(
+ extractorFactory,
+ playlistTracker,
+ dataSourceFactory,
+ mediaTransferListener,
+ drmSessionManager,
+ loadErrorHandlingPolicy,
+ eventDispatcher,
+ allocator,
+ compositeSequenceableLoaderFactory,
+ allowChunklessPreparation,
+ metadataType,
+ useSessionKeys);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ ((HlsMediaPeriod) mediaPeriod).release();
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ playlistTracker.stop();
+ drmSessionManager.release();
+ }
+
+ @Override
+ public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
+ SinglePeriodTimeline timeline;
+ long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs)
+ : C.TIME_UNSET;
+ // For playlist types EVENT and VOD we know segments are never removed, so the presentation
+ // started at the same time as the window. Otherwise, we don't know the presentation start time.
+ long presentationStartTimeMs =
+ playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
+ || playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
+ ? windowStartTimeMs
+ : C.TIME_UNSET;
+ long windowDefaultStartPositionUs = playlist.startOffsetUs;
+ // masterPlaylist is non-null because the first playlist has been fetched by now.
+ HlsManifest manifest =
+ new HlsManifest(Assertions.checkNotNull(playlistTracker.getMasterPlaylist()), playlist);
+ if (playlistTracker.isLive()) {
+ long offsetFromInitialStartTimeUs =
+ playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
+ long periodDurationUs =
+ playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
+ List<HlsMediaPlaylist.Segment> segments = playlist.segments;
+ if (windowDefaultStartPositionUs == C.TIME_UNSET) {
+ windowDefaultStartPositionUs = 0;
+ if (!segments.isEmpty()) {
+ int defaultStartSegmentIndex = Math.max(0, segments.size() - 3);
+ // We attempt to set the default start position to be at least twice the target duration
+ // behind the live edge.
+ long minStartPositionUs = playlist.durationUs - playlist.targetDurationUs * 2;
+ while (defaultStartSegmentIndex > 0
+ && segments.get(defaultStartSegmentIndex).relativeStartTimeUs > minStartPositionUs) {
+ defaultStartSegmentIndex--;
+ }
+ windowDefaultStartPositionUs = segments.get(defaultStartSegmentIndex).relativeStartTimeUs;
+ }
+ }
+ timeline =
+ new SinglePeriodTimeline(
+ presentationStartTimeMs,
+ windowStartTimeMs,
+ periodDurationUs,
+ /* windowDurationUs= */ playlist.durationUs,
+ /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs,
+ windowDefaultStartPositionUs,
+ /* isSeekable= */ true,
+ /* isDynamic= */ !playlist.hasEndTag,
+ /* isLive= */ true,
+ manifest,
+ tag);
+ } else /* not live */ {
+ if (windowDefaultStartPositionUs == C.TIME_UNSET) {
+ windowDefaultStartPositionUs = 0;
+ }
+ timeline =
+ new SinglePeriodTimeline(
+ presentationStartTimeMs,
+ windowStartTimeMs,
+ /* periodDurationUs= */ playlist.durationUs,
+ /* windowDurationUs= */ playlist.durationUs,
+ /* windowPositionInPeriodUs= */ 0,
+ windowDefaultStartPositionUs,
+ /* isSeekable= */ true,
+ /* isDynamic= */ false,
+ /* isLive= */ false,
+ manifest,
+ tag);
+ }
+ refreshSourceInfo(timeline);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
new file mode 100644
index 0000000000..5f44810af5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * {@link SampleStream} for a particular sample queue in HLS.
+ */
+/* package */ final class HlsSampleStream implements SampleStream {
+
+ private final int trackGroupIndex;
+ private final HlsSampleStreamWrapper sampleStreamWrapper;
+ private int sampleQueueIndex;
+
+ public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) {
+ this.sampleStreamWrapper = sampleStreamWrapper;
+ this.trackGroupIndex = trackGroupIndex;
+ sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;
+ }
+
+ public void bindSampleQueue() {
+ Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING);
+ sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
+ }
+
+ public void unbindSampleQueue() {
+ if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
+ sampleStreamWrapper.unbindSampleQueue(trackGroupIndex);
+ sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;
+ }
+ }
+
+ // SampleStream implementation.
+
+ @Override
+ public boolean isReady() {
+ return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
+ || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex));
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) {
+ throw new SampleQueueMappingException(
+ sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType);
+ } else if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
+ sampleStreamWrapper.maybeThrowError();
+ } else if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) {
+ sampleStreamWrapper.maybeThrowError(sampleQueueIndex);
+ }
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {
+ if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) {
+ buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ }
+ return hasValidSampleQueueIndex()
+ ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat)
+ : C.RESULT_NOTHING_READ;
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ return hasValidSampleQueueIndex()
+ ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs)
+ : 0;
+ }
+
+ // Internal methods.
+
+ private boolean hasValidSampleQueueIndex() {
+ return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING
+ && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
+ && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
new file mode 100644
index 0000000000..833abbc29f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -0,0 +1,1535 @@
+/*
+ * 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;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.util.SparseIntArray;
+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.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessage;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.PrivFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SequenceableLoader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.Chunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+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.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides
+ * {@link SampleStream}s from which the loaded media can be consumed.
+ */
+/* package */ final class HlsSampleStreamWrapper implements Loader.Callback<Chunk>,
+ Loader.ReleaseCallback, SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener {
+
+ /**
+ * A callback to be notified of events.
+ */
+ public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWrapper> {
+
+ /**
+ * Called when the wrapper has been prepared.
+ *
+ * <p>Note: This method will be called on a later handler loop than the one on which either
+ * {@link #prepareWithMasterPlaylistInfo} or {@link #continuePreparing} are invoked.
+ */
+ void onPrepared();
+
+ /**
+ * Called to schedule a {@link #continueLoading(long)} call when the playlist referred by the
+ * given url changes.
+ */
+ void onPlaylistRefreshRequired(Uri playlistUrl);
+ }
+
+ private static final String TAG = "HlsSampleStreamWrapper";
+
+ public static final int SAMPLE_QUEUE_INDEX_PENDING = -1;
+ public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL = -2;
+ public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL = -3;
+
+ private static final Set<Integer> MAPPABLE_TYPES =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_METADATA)));
+
+ private final int trackType;
+ private final Callback callback;
+ private final HlsChunkSource chunkSource;
+ private final Allocator allocator;
+ @Nullable private final Format muxedAudioFormat;
+ private final DrmSessionManager<?> drmSessionManager;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final Loader loader;
+ private final EventDispatcher eventDispatcher;
+ private final @HlsMediaSource.MetadataType int metadataType;
+ private final HlsChunkSource.HlsChunkHolder nextChunkHolder;
+ private final ArrayList<HlsMediaChunk> mediaChunks;
+ private final List<HlsMediaChunk> readOnlyMediaChunks;
+ // Using runnables rather than in-line method references to avoid repeated allocations.
+ private final Runnable maybeFinishPrepareRunnable;
+ private final Runnable onTracksEndedRunnable;
+ private final Handler handler;
+ private final ArrayList<HlsSampleStream> hlsSampleStreams;
+ private final Map<String, DrmInitData> overridingDrmInitData;
+
+ private FormatAdjustingSampleQueue[] sampleQueues;
+ private int[] sampleQueueTrackIds;
+ private Set<Integer> sampleQueueMappingDoneByType;
+ private SparseIntArray sampleQueueIndicesByType;
+ @MonotonicNonNull private TrackOutput emsgUnwrappingTrackOutput;
+ private int primarySampleQueueType;
+ private int primarySampleQueueIndex;
+ private boolean sampleQueuesBuilt;
+ private boolean prepared;
+ private int enabledTrackGroupCount;
+ @MonotonicNonNull private Format upstreamTrackFormat;
+ @Nullable private Format downstreamTrackFormat;
+ private boolean released;
+
+ // Tracks are complicated in HLS. See documentation of buildTracksFromSampleStreams for details.
+ // Indexed by track (as exposed by this source).
+ @MonotonicNonNull private TrackGroupArray trackGroups;
+ @MonotonicNonNull private Set<TrackGroup> optionalTrackGroups;
+ // Indexed by track group.
+ private int @MonotonicNonNull [] trackGroupToSampleQueueIndex;
+ private int primaryTrackGroupIndex;
+ private boolean haveAudioVideoSampleQueues;
+ private boolean[] sampleQueuesEnabledStates;
+ private boolean[] sampleQueueIsAudioVideoFlags;
+
+ private long lastSeekPositionUs;
+ private long pendingResetPositionUs;
+ private boolean pendingResetUpstreamFormats;
+ private boolean seenFirstTrackSelection;
+ private boolean loadingFinished;
+
+ // Accessed only by the loading thread.
+ private boolean tracksEnded;
+ private long sampleOffsetUs;
+ @Nullable private DrmInitData drmInitData;
+ private int chunkUid;
+
+ /**
+ * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
+ * @param callback A callback for the wrapper.
+ * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained.
+ * @param overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type
+ * (i.e. {@link DrmInitData#schemeType}). If the stream has {@link DrmInitData} and uses a
+ * protection scheme type for which overriding {@link DrmInitData} is provided, then the
+ * stream's {@link DrmInitData} will be overridden.
+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+ * @param positionUs The position from which to start loading media.
+ * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist.
+ * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession
+ * DrmSessions} with.
+ * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
+ * @param eventDispatcher A dispatcher to notify of events.
+ */
+ public HlsSampleStreamWrapper(
+ int trackType,
+ Callback callback,
+ HlsChunkSource chunkSource,
+ Map<String, DrmInitData> overridingDrmInitData,
+ Allocator allocator,
+ long positionUs,
+ @Nullable Format muxedAudioFormat,
+ DrmSessionManager<?> drmSessionManager,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ EventDispatcher eventDispatcher,
+ @HlsMediaSource.MetadataType int metadataType) {
+ this.trackType = trackType;
+ this.callback = callback;
+ this.chunkSource = chunkSource;
+ this.overridingDrmInitData = overridingDrmInitData;
+ this.allocator = allocator;
+ this.muxedAudioFormat = muxedAudioFormat;
+ this.drmSessionManager = drmSessionManager;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ this.eventDispatcher = eventDispatcher;
+ this.metadataType = metadataType;
+ loader = new Loader("Loader:HlsSampleStreamWrapper");
+ nextChunkHolder = new HlsChunkSource.HlsChunkHolder();
+ sampleQueueTrackIds = new int[0];
+ sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size());
+ sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size());
+ sampleQueues = new FormatAdjustingSampleQueue[0];
+ sampleQueueIsAudioVideoFlags = new boolean[0];
+ sampleQueuesEnabledStates = new boolean[0];
+ mediaChunks = new ArrayList<>();
+ readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
+ hlsSampleStreams = new ArrayList<>();
+ // Suppressions are needed because `this` is not initialized here.
+ @SuppressWarnings("nullness:methodref.receiver.bound.invalid")
+ Runnable maybeFinishPrepareRunnable = this::maybeFinishPrepare;
+ this.maybeFinishPrepareRunnable = maybeFinishPrepareRunnable;
+ @SuppressWarnings("nullness:methodref.receiver.bound.invalid")
+ Runnable onTracksEndedRunnable = this::onTracksEnded;
+ this.onTracksEndedRunnable = onTracksEndedRunnable;
+ handler = new Handler();
+ lastSeekPositionUs = positionUs;
+ pendingResetPositionUs = positionUs;
+ }
+
+ public void continuePreparing() {
+ if (!prepared) {
+ continueLoading(lastSeekPositionUs);
+ }
+ }
+
+ /**
+ * Prepares the sample stream wrapper with master playlist information.
+ *
+ * @param trackGroups The {@link TrackGroup TrackGroups} to expose through {@link
+ * #getTrackGroups()}.
+ * @param primaryTrackGroupIndex The index of the adaptive track group.
+ * @param optionalTrackGroupsIndices The indices of any {@code trackGroups} that should not
+ * trigger a failure if not found in the media playlist's segments.
+ */
+ public void prepareWithMasterPlaylistInfo(
+ TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) {
+ this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups);
+ optionalTrackGroups = new HashSet<>();
+ for (int optionalTrackGroupIndex : optionalTrackGroupsIndices) {
+ optionalTrackGroups.add(this.trackGroups.get(optionalTrackGroupIndex));
+ }
+ this.primaryTrackGroupIndex = primaryTrackGroupIndex;
+ handler.post(callback::onPrepared);
+ setIsPrepared();
+ }
+
+ public void maybeThrowPrepareError() throws IOException {
+ maybeThrowError();
+ if (loadingFinished && !prepared) {
+ throw new ParserException("Loading finished before preparation is complete.");
+ }
+ }
+
+ public TrackGroupArray getTrackGroups() {
+ assertIsPrepared();
+ return trackGroups;
+ }
+
+ public int getPrimaryTrackGroupIndex() {
+ return primaryTrackGroupIndex;
+ }
+
+ public int bindSampleQueueToSampleStream(int trackGroupIndex) {
+ assertIsPrepared();
+ Assertions.checkNotNull(trackGroupToSampleQueueIndex);
+
+ int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
+ if (sampleQueueIndex == C.INDEX_UNSET) {
+ return optionalTrackGroups.contains(trackGroups.get(trackGroupIndex))
+ ? SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
+ : SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;
+ }
+ if (sampleQueuesEnabledStates[sampleQueueIndex]) {
+ // This sample queue is already bound to a different sample stream.
+ return SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;
+ }
+ sampleQueuesEnabledStates[sampleQueueIndex] = true;
+ return sampleQueueIndex;
+ }
+
+ public void unbindSampleQueue(int trackGroupIndex) {
+ assertIsPrepared();
+ Assertions.checkNotNull(trackGroupToSampleQueueIndex);
+ int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
+ Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex]);
+ sampleQueuesEnabledStates[sampleQueueIndex] = false;
+ }
+
+ /**
+ * Called by the parent {@link HlsMediaPeriod} when a track selection occurs.
+ *
+ * @param selections The renderer track selections.
+ * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
+ * for each selection. A {@code true} value indicates that the selection is unchanged, and
+ * that the caller does not require that the sample stream be recreated.
+ * @param streams The existing sample streams, which will be updated to reflect the provided
+ * selections.
+ * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that
+ * have been retained but with the requirement that the consuming renderer be reset.
+ * @param positionUs The current playback position in microseconds.
+ * @param forceReset If true then a reset is forced (i.e. a seek will be performed with in-buffer
+ * seeking disabled).
+ * @return Whether this wrapper requires the parent {@link HlsMediaPeriod} to perform a seek as
+ * part of the track selection.
+ */
+ public boolean selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs,
+ boolean forceReset) {
+ assertIsPrepared();
+ int oldEnabledTrackGroupCount = enabledTrackGroupCount;
+ // Deselect old tracks.
+ for (int i = 0; i < selections.length; i++) {
+ HlsSampleStream stream = (HlsSampleStream) streams[i];
+ if (stream != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+ enabledTrackGroupCount--;
+ stream.unbindSampleQueue();
+ streams[i] = null;
+ }
+ }
+ // We'll always need to seek if we're being forced to reset, or if this is a first selection to
+ // a position other than the one we started preparing with, or if we're making a selection
+ // having previously disabled all tracks.
+ boolean seekRequired =
+ forceReset
+ || (seenFirstTrackSelection
+ ? oldEnabledTrackGroupCount == 0
+ : positionUs != lastSeekPositionUs);
+ // Get the old (i.e. current before the loop below executes) primary track selection. The new
+ // primary selection will equal the old one unless it's changed in the loop.
+ TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection();
+ TrackSelection primaryTrackSelection = oldPrimaryTrackSelection;
+ // Select new tracks.
+ for (int i = 0; i < selections.length; i++) {
+ TrackSelection selection = selections[i];
+ if (selection == null) {
+ continue;
+ }
+ int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup());
+ if (trackGroupIndex == primaryTrackGroupIndex) {
+ primaryTrackSelection = selection;
+ chunkSource.setTrackSelection(selection);
+ }
+ if (streams[i] == null) {
+ enabledTrackGroupCount++;
+ streams[i] = new HlsSampleStream(this, trackGroupIndex);
+ streamResetFlags[i] = true;
+ if (trackGroupToSampleQueueIndex != null) {
+ ((HlsSampleStream) streams[i]).bindSampleQueue();
+ // If there's still a chance of avoiding a seek, try and seek within the sample queue.
+ if (!seekRequired) {
+ SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]];
+ // A seek can be avoided if we're able to seek to the current playback position in
+ // the sample queue, or if we haven't read anything from the queue since the previous
+ // seek (this case is common for sparse tracks such as metadata tracks). In all other
+ // cases a seek is required.
+ seekRequired =
+ !sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true)
+ && sampleQueue.getReadIndex() != 0;
+ }
+ }
+ }
+ }
+
+ if (enabledTrackGroupCount == 0) {
+ chunkSource.reset();
+ downstreamTrackFormat = null;
+ pendingResetUpstreamFormats = true;
+ mediaChunks.clear();
+ if (loader.isLoading()) {
+ if (sampleQueuesBuilt) {
+ // Discard as much as we can synchronously.
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.discardToEnd();
+ }
+ }
+ loader.cancelLoading();
+ } else {
+ resetSampleQueues();
+ }
+ } else {
+ if (!mediaChunks.isEmpty()
+ && !Util.areEqual(primaryTrackSelection, oldPrimaryTrackSelection)) {
+ // The primary track selection has changed and we have buffered media. The buffered media
+ // may need to be discarded.
+ boolean primarySampleQueueDirty = false;
+ if (!seenFirstTrackSelection) {
+ long bufferedDurationUs = positionUs < 0 ? -positionUs : 0;
+ HlsMediaChunk lastMediaChunk = getLastMediaChunk();
+ MediaChunkIterator[] mediaChunkIterators =
+ chunkSource.createMediaChunkIterators(lastMediaChunk, positionUs);
+ primaryTrackSelection.updateSelectedTrack(
+ positionUs,
+ bufferedDurationUs,
+ C.TIME_UNSET,
+ readOnlyMediaChunks,
+ mediaChunkIterators);
+ int chunkIndex = chunkSource.getTrackGroup().indexOf(lastMediaChunk.trackFormat);
+ if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) {
+ // This is the first selection and the chunk loaded during preparation does not match
+ // the initially selected format.
+ primarySampleQueueDirty = true;
+ }
+ } else {
+ // The primary sample queue contains media buffered for the old primary track selection.
+ primarySampleQueueDirty = true;
+ }
+ if (primarySampleQueueDirty) {
+ forceReset = true;
+ seekRequired = true;
+ pendingResetUpstreamFormats = true;
+ }
+ }
+ if (seekRequired) {
+ seekToUs(positionUs, forceReset);
+ // We'll need to reset renderers consuming from all streams due to the seek.
+ for (int i = 0; i < streams.length; i++) {
+ if (streams[i] != null) {
+ streamResetFlags[i] = true;
+ }
+ }
+ }
+ }
+
+ updateSampleStreams(streams);
+ seenFirstTrackSelection = true;
+ return seekRequired;
+ }
+
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ if (!sampleQueuesBuilt || isPendingReset()) {
+ return;
+ }
+ int sampleQueueCount = sampleQueues.length;
+ for (int i = 0; i < sampleQueueCount; i++) {
+ sampleQueues[i].discardTo(positionUs, toKeyframe, sampleQueuesEnabledStates[i]);
+ }
+ }
+
+ /**
+ * Attempts to seek to the specified position in microseconds.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @param forceReset If true then a reset is forced (i.e. in-buffer seeking is disabled).
+ * @return Whether the wrapper was reset, meaning the wrapped sample queues were reset. If false,
+ * an in-buffer seek was performed.
+ */
+ public boolean seekToUs(long positionUs, boolean forceReset) {
+ lastSeekPositionUs = positionUs;
+ if (isPendingReset()) {
+ // A reset is already pending. We only need to update its position.
+ pendingResetPositionUs = positionUs;
+ return true;
+ }
+
+ // If we're not forced to reset, try and seek within the buffer.
+ if (sampleQueuesBuilt && !forceReset && seekInsideBufferUs(positionUs)) {
+ return false;
+ }
+
+ // We can't seek inside the buffer, and so need to reset.
+ pendingResetPositionUs = positionUs;
+ loadingFinished = false;
+ mediaChunks.clear();
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ } else {
+ loader.clearFatalError();
+ resetSampleQueues();
+ }
+ return true;
+ }
+
+ public void release() {
+ if (prepared) {
+ // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise
+ // sampleQueues may still be being modified by the loading thread.
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.preRelease();
+ }
+ }
+ loader.release(this);
+ handler.removeCallbacksAndMessages(null);
+ released = true;
+ hlsSampleStreams.clear();
+ }
+
+ @Override
+ public void onLoaderReleased() {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.release();
+ }
+ }
+
+ public void setIsTimestampMaster(boolean isTimestampMaster) {
+ chunkSource.setIsTimestampMaster(isTimestampMaster);
+ }
+
+ public boolean onPlaylistError(Uri playlistUrl, long blacklistDurationMs) {
+ return chunkSource.onPlaylistError(playlistUrl, blacklistDurationMs);
+ }
+
+ // SampleStream implementation.
+
+ public boolean isReady(int sampleQueueIndex) {
+ return !isPendingReset() && sampleQueues[sampleQueueIndex].isReady(loadingFinished);
+ }
+
+ public void maybeThrowError(int sampleQueueIndex) throws IOException {
+ maybeThrowError();
+ sampleQueues[sampleQueueIndex].maybeThrowError();
+ }
+
+ public void maybeThrowError() throws IOException {
+ loader.maybeThrowError();
+ chunkSource.maybeThrowError();
+ }
+
+ public int readData(int sampleQueueIndex, FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean requireFormat) {
+ if (isPendingReset()) {
+ return C.RESULT_NOTHING_READ;
+ }
+
+ // TODO: Split into discard (in discardBuffer) and format change (here and in skipData) steps.
+ if (!mediaChunks.isEmpty()) {
+ int discardToMediaChunkIndex = 0;
+ while (discardToMediaChunkIndex < mediaChunks.size() - 1
+ && finishedReadingChunk(mediaChunks.get(discardToMediaChunkIndex))) {
+ discardToMediaChunkIndex++;
+ }
+ Util.removeRange(mediaChunks, 0, discardToMediaChunkIndex);
+ HlsMediaChunk currentChunk = mediaChunks.get(0);
+ Format trackFormat = currentChunk.trackFormat;
+ if (!trackFormat.equals(downstreamTrackFormat)) {
+ eventDispatcher.downstreamFormatChanged(trackType, trackFormat,
+ currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+ currentChunk.startTimeUs);
+ }
+ downstreamTrackFormat = trackFormat;
+ }
+
+ int result =
+ sampleQueues[sampleQueueIndex].read(
+ formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs);
+ if (result == C.RESULT_FORMAT_READ) {
+ Format format = Assertions.checkNotNull(formatHolder.format);
+ if (sampleQueueIndex == primarySampleQueueIndex) {
+ // Fill in primary sample format with information from the track format.
+ int chunkUid = sampleQueues[sampleQueueIndex].peekSourceId();
+ int chunkIndex = 0;
+ while (chunkIndex < mediaChunks.size() && mediaChunks.get(chunkIndex).uid != chunkUid) {
+ chunkIndex++;
+ }
+ Format trackFormat =
+ chunkIndex < mediaChunks.size()
+ ? mediaChunks.get(chunkIndex).trackFormat
+ : Assertions.checkNotNull(upstreamTrackFormat);
+ format = format.copyWithManifestFormatInfo(trackFormat);
+ }
+ formatHolder.format = format;
+ }
+ return result;
+ }
+
+ public int skipData(int sampleQueueIndex, long positionUs) {
+ if (isPendingReset()) {
+ return 0;
+ }
+
+ SampleQueue sampleQueue = sampleQueues[sampleQueueIndex];
+ if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
+ return sampleQueue.advanceToEnd();
+ } else {
+ return sampleQueue.advanceTo(positionUs);
+ }
+ }
+
+ // SequenceableLoader implementation
+
+ @Override
+ public long getBufferedPositionUs() {
+ if (loadingFinished) {
+ return C.TIME_END_OF_SOURCE;
+ } else if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ long bufferedPositionUs = lastSeekPositionUs;
+ HlsMediaChunk lastMediaChunk = getLastMediaChunk();
+ HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk
+ : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null;
+ if (lastCompletedMediaChunk != null) {
+ bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);
+ }
+ if (sampleQueuesBuilt) {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ bufferedPositionUs =
+ Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs());
+ }
+ }
+ return bufferedPositionUs;
+ }
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs;
+ }
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (loadingFinished || loader.isLoading() || loader.hasFatalError()) {
+ return false;
+ }
+
+ List<HlsMediaChunk> chunkQueue;
+ long loadPositionUs;
+ if (isPendingReset()) {
+ chunkQueue = Collections.emptyList();
+ loadPositionUs = pendingResetPositionUs;
+ } else {
+ chunkQueue = readOnlyMediaChunks;
+ HlsMediaChunk lastMediaChunk = getLastMediaChunk();
+ loadPositionUs =
+ lastMediaChunk.isLoadCompleted()
+ ? lastMediaChunk.endTimeUs
+ : Math.max(lastSeekPositionUs, lastMediaChunk.startTimeUs);
+ }
+ chunkSource.getNextChunk(
+ positionUs,
+ loadPositionUs,
+ chunkQueue,
+ /* allowEndOfStream= */ prepared || !chunkQueue.isEmpty(),
+ nextChunkHolder);
+ boolean endOfStream = nextChunkHolder.endOfStream;
+ Chunk loadable = nextChunkHolder.chunk;
+ Uri playlistUrlToLoad = nextChunkHolder.playlistUrl;
+ nextChunkHolder.clear();
+
+ if (endOfStream) {
+ pendingResetPositionUs = C.TIME_UNSET;
+ loadingFinished = true;
+ return true;
+ }
+
+ if (loadable == null) {
+ if (playlistUrlToLoad != null) {
+ callback.onPlaylistRefreshRequired(playlistUrlToLoad);
+ }
+ return false;
+ }
+
+ if (isMediaChunk(loadable)) {
+ pendingResetPositionUs = C.TIME_UNSET;
+ HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable;
+ mediaChunk.init(this);
+ mediaChunks.add(mediaChunk);
+ upstreamTrackFormat = mediaChunk.trackFormat;
+ }
+ long elapsedRealtimeMs =
+ loader.startLoading(
+ loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));
+ eventDispatcher.loadStarted(
+ loadable.dataSpec,
+ loadable.type,
+ trackType,
+ loadable.trackFormat,
+ loadable.trackSelectionReason,
+ loadable.trackSelectionData,
+ loadable.startTimeUs,
+ loadable.endTimeUs,
+ elapsedRealtimeMs);
+ return true;
+ }
+
+ @Override
+ public boolean isLoading() {
+ return loader.isLoading();
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ // Do nothing.
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
+ chunkSource.onChunkLoadCompleted(loadable);
+ eventDispatcher.loadCompleted(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ loadable.type,
+ trackType,
+ loadable.trackFormat,
+ loadable.trackSelectionReason,
+ loadable.trackSelectionData,
+ loadable.startTimeUs,
+ loadable.endTimeUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded());
+ if (!prepared) {
+ continueLoading(lastSeekPositionUs);
+ } else {
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+
+ @Override
+ public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+ boolean released) {
+ eventDispatcher.loadCanceled(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ loadable.type,
+ trackType,
+ loadable.trackFormat,
+ loadable.trackSelectionReason,
+ loadable.trackSelectionData,
+ loadable.startTimeUs,
+ loadable.endTimeUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded());
+ if (!released) {
+ resetSampleQueues();
+ if (enabledTrackGroupCount > 0) {
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+ }
+
+ @Override
+ public LoadErrorAction onLoadError(
+ Chunk loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ IOException error,
+ int errorCount) {
+ long bytesLoaded = loadable.bytesLoaded();
+ boolean isMediaChunk = isMediaChunk(loadable);
+ boolean blacklistSucceeded = false;
+ LoadErrorAction loadErrorAction;
+
+ long blacklistDurationMs =
+ loadErrorHandlingPolicy.getBlacklistDurationMsFor(
+ loadable.type, loadDurationMs, error, errorCount);
+ if (blacklistDurationMs != C.TIME_UNSET) {
+ blacklistSucceeded = chunkSource.maybeBlacklistTrack(loadable, blacklistDurationMs);
+ }
+
+ if (blacklistSucceeded) {
+ if (isMediaChunk && bytesLoaded == 0) {
+ HlsMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1);
+ Assertions.checkState(removed == loadable);
+ if (mediaChunks.isEmpty()) {
+ pendingResetPositionUs = lastSeekPositionUs;
+ }
+ }
+ loadErrorAction = Loader.DONT_RETRY;
+ } else /* did not blacklist */ {
+ long retryDelayMs =
+ loadErrorHandlingPolicy.getRetryDelayMsFor(
+ loadable.type, loadDurationMs, error, errorCount);
+ loadErrorAction =
+ retryDelayMs != C.TIME_UNSET
+ ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs)
+ : Loader.DONT_RETRY_FATAL;
+ }
+
+ eventDispatcher.loadError(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ loadable.type,
+ trackType,
+ loadable.trackFormat,
+ loadable.trackSelectionReason,
+ loadable.trackSelectionData,
+ loadable.startTimeUs,
+ loadable.endTimeUs,
+ elapsedRealtimeMs,
+ loadDurationMs,
+ bytesLoaded,
+ error,
+ /* wasCanceled= */ !loadErrorAction.isRetry());
+
+ if (blacklistSucceeded) {
+ if (!prepared) {
+ continueLoading(lastSeekPositionUs);
+ } else {
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+ return loadErrorAction;
+ }
+
+ // Called by the consuming thread, but only when there is no loading thread.
+
+ /**
+ * Initializes the wrapper for loading a chunk.
+ *
+ * @param chunkUid The chunk's uid.
+ * @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any
+ * samples already queued to the wrapper.
+ */
+ public void init(int chunkUid, boolean shouldSpliceIn) {
+ this.chunkUid = chunkUid;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.sourceId(chunkUid);
+ }
+ if (shouldSpliceIn) {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.splice();
+ }
+ }
+ }
+
+ // ExtractorOutput implementation. Called by the loading thread.
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ @Nullable TrackOutput trackOutput = null;
+ if (MAPPABLE_TYPES.contains(type)) {
+ // Track types in MAPPABLE_TYPES are handled manually to ignore IDs.
+ trackOutput = getMappedTrackOutput(id, type);
+ } else /* non-mappable type track */ {
+ for (int i = 0; i < sampleQueues.length; i++) {
+ if (sampleQueueTrackIds[i] == id) {
+ trackOutput = sampleQueues[i];
+ break;
+ }
+ }
+ }
+
+ if (trackOutput == null) {
+ if (tracksEnded) {
+ return createDummyTrackOutput(id, type);
+ } else {
+ // The relevant SampleQueue hasn't been constructed yet - so construct it.
+ trackOutput = createSampleQueue(id, type);
+ }
+ }
+
+ if (type == C.TRACK_TYPE_METADATA) {
+ if (emsgUnwrappingTrackOutput == null) {
+ emsgUnwrappingTrackOutput = new EmsgUnwrappingTrackOutput(trackOutput, metadataType);
+ }
+ return emsgUnwrappingTrackOutput;
+ }
+ return trackOutput;
+ }
+
+ /**
+ * Returns the {@link TrackOutput} for the provided {@code type} and {@code id}, or null if none
+ * has been created yet.
+ *
+ * <p>If a {@link SampleQueue} for {@code type} has been created and is mapped, but it has a
+ * different ID, then return a {@link DummyTrackOutput} that does nothing.
+ *
+ * <p>If a {@link SampleQueue} for {@code type} has been created but is not mapped, then map it to
+ * this {@code id} and return it. This situation can happen after a call to {@link
+ * #onNewExtractor}.
+ *
+ * @param id The ID of the track.
+ * @param type The type of the track, must be one of {@link #MAPPABLE_TYPES}.
+ * @return The the mapped {@link TrackOutput}, or null if it's not been created yet.
+ */
+ @Nullable
+ private TrackOutput getMappedTrackOutput(int id, int type) {
+ Assertions.checkArgument(MAPPABLE_TYPES.contains(type));
+ int sampleQueueIndex = sampleQueueIndicesByType.get(type, C.INDEX_UNSET);
+ if (sampleQueueIndex == C.INDEX_UNSET) {
+ return null;
+ }
+
+ if (sampleQueueMappingDoneByType.add(type)) {
+ sampleQueueTrackIds[sampleQueueIndex] = id;
+ }
+ return sampleQueueTrackIds[sampleQueueIndex] == id
+ ? sampleQueues[sampleQueueIndex]
+ : createDummyTrackOutput(id, type);
+ }
+
+ private SampleQueue createSampleQueue(int id, int type) {
+ int trackCount = sampleQueues.length;
+
+ boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;
+ FormatAdjustingSampleQueue trackOutput =
+ new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData);
+ if (isAudioVideo) {
+ trackOutput.setDrmInitData(drmInitData);
+ }
+ trackOutput.setSampleOffsetUs(sampleOffsetUs);
+ trackOutput.sourceId(chunkUid);
+ trackOutput.setUpstreamFormatChangeListener(this);
+ sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
+ sampleQueueTrackIds[trackCount] = id;
+ sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput);
+ sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1);
+ sampleQueueIsAudioVideoFlags[trackCount] = isAudioVideo;
+ haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount];
+ sampleQueueMappingDoneByType.add(type);
+ sampleQueueIndicesByType.append(type, trackCount);
+ if (getTrackTypeScore(type) > getTrackTypeScore(primarySampleQueueType)) {
+ primarySampleQueueIndex = trackCount;
+ primarySampleQueueType = type;
+ }
+ sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1);
+ return trackOutput;
+ }
+
+ @Override
+ public void endTracks() {
+ tracksEnded = true;
+ handler.post(onTracksEndedRunnable);
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ // Do nothing.
+ }
+
+ // UpstreamFormatChangedListener implementation. Called by the loading thread.
+
+ @Override
+ public void onUpstreamFormatChanged(Format format) {
+ handler.post(maybeFinishPrepareRunnable);
+ }
+
+ // Called by the loading thread.
+
+ /** Called when an {@link HlsMediaChunk} starts extracting media with a new {@link Extractor}. */
+ public void onNewExtractor() {
+ sampleQueueMappingDoneByType.clear();
+ }
+
+ /**
+ * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that
+ * are subsequently loaded by this wrapper.
+ *
+ * @param sampleOffsetUs The timestamp offset in microseconds.
+ */
+ public void setSampleOffsetUs(long sampleOffsetUs) {
+ if (this.sampleOffsetUs != sampleOffsetUs) {
+ this.sampleOffsetUs = sampleOffsetUs;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.setSampleOffsetUs(sampleOffsetUs);
+ }
+ }
+ }
+
+ /**
+ * Sets default {@link DrmInitData} for samples that are subsequently loaded by this wrapper.
+ *
+ * <p>This method should be called prior to loading each {@link HlsMediaChunk}. The {@link
+ * DrmInitData} passed should be that of an EXT-X-KEY tag that applies to the chunk, or {@code
+ * null} otherwise.
+ *
+ * <p>The final {@link DrmInitData} for subsequently queued samples is determined as followed:
+ *
+ * <ol>
+ * <li>It is initially set to {@code drmInitData}, unless {@code drmInitData} is null in which
+ * case it's set to {@link Format#drmInitData} of the upstream {@link Format}.
+ * <li>If the initial {@link DrmInitData} is non-null and {@link #overridingDrmInitData}
+ * contains an entry whose key matches the {@link DrmInitData#schemeType}, then the sample's
+ * {@link DrmInitData} is overridden to be this entry's value.
+ * </ol>
+ *
+ * <p>
+ *
+ * @param drmInitData The default {@link DrmInitData} for samples that are subsequently queued. If
+ * non-null then it takes precedence over {@link Format#drmInitData} of the upstream {@link
+ * Format}, but will still be overridden by a matching override in {@link
+ * #overridingDrmInitData}.
+ */
+ public void setDrmInitData(@Nullable DrmInitData drmInitData) {
+ if (!Util.areEqual(this.drmInitData, drmInitData)) {
+ this.drmInitData = drmInitData;
+ for (int i = 0; i < sampleQueues.length; i++) {
+ if (sampleQueueIsAudioVideoFlags[i]) {
+ sampleQueues[i].setDrmInitData(drmInitData);
+ }
+ }
+ }
+ }
+
+ // Internal methods.
+
+ private void updateSampleStreams(@NullableType SampleStream[] streams) {
+ hlsSampleStreams.clear();
+ for (SampleStream stream : streams) {
+ if (stream != null) {
+ hlsSampleStreams.add((HlsSampleStream) stream);
+ }
+ }
+ }
+
+ private boolean finishedReadingChunk(HlsMediaChunk chunk) {
+ int chunkUid = chunk.uid;
+ int sampleQueueCount = sampleQueues.length;
+ for (int i = 0; i < sampleQueueCount; i++) {
+ if (sampleQueuesEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void resetSampleQueues() {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset(pendingResetUpstreamFormats);
+ }
+ pendingResetUpstreamFormats = false;
+ }
+
+ private void onTracksEnded() {
+ sampleQueuesBuilt = true;
+ maybeFinishPrepare();
+ }
+
+ private void maybeFinishPrepare() {
+ if (released || trackGroupToSampleQueueIndex != null || !sampleQueuesBuilt) {
+ return;
+ }
+ for (SampleQueue sampleQueue : sampleQueues) {
+ if (sampleQueue.getUpstreamFormat() == null) {
+ return;
+ }
+ }
+ if (trackGroups != null) {
+ // The track groups were created with master playlist information. They only need to be mapped
+ // to a sample queue.
+ mapSampleQueuesToMatchTrackGroups();
+ } else {
+ // Tracks are created using media segment information.
+ buildTracksFromSampleStreams();
+ setIsPrepared();
+ callback.onPrepared();
+ }
+ }
+
+ @RequiresNonNull("trackGroups")
+ @EnsuresNonNull("trackGroupToSampleQueueIndex")
+ private void mapSampleQueuesToMatchTrackGroups() {
+ int trackGroupCount = trackGroups.length;
+ trackGroupToSampleQueueIndex = new int[trackGroupCount];
+ Arrays.fill(trackGroupToSampleQueueIndex, C.INDEX_UNSET);
+ for (int i = 0; i < trackGroupCount; i++) {
+ for (int queueIndex = 0; queueIndex < sampleQueues.length; queueIndex++) {
+ SampleQueue sampleQueue = sampleQueues[queueIndex];
+ if (formatsMatch(sampleQueue.getUpstreamFormat(), trackGroups.get(i).getFormat(0))) {
+ trackGroupToSampleQueueIndex[i] = queueIndex;
+ break;
+ }
+ }
+ }
+ for (HlsSampleStream sampleStream : hlsSampleStreams) {
+ sampleStream.bindSampleQueue();
+ }
+ }
+
+ /**
+ * Builds tracks that are exposed by this {@link HlsSampleStreamWrapper} instance, as well as
+ * internal data-structures required for operation.
+ *
+ * <p>Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each
+ * variant stream typically contains muxed video, audio and (possibly) additional audio, metadata
+ * and caption tracks. We wish to allow the user to select between an adaptive track that spans
+ * all variants, as well as each individual variant. If multiple audio tracks are present within
+ * each variant then we wish to allow the user to select between those also.
+ *
+ * <p>To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1)
+ * tracks, where N is the number of variants defined in the HLS master playlist. These consist of
+ * one adaptive track defined to span all variants and a track for each individual variant. The
+ * adaptive track is initially selected. The extractor is then prepared to discover the tracks
+ * inside of each variant stream. The two sets of tracks are then combined by this method to
+ * create a third set, which is the set exposed by this {@link HlsSampleStreamWrapper}:
+ *
+ * <ul>
+ * <li>The extractor tracks are inspected to infer a "primary" track type. If a video track is
+ * present then it is always the primary type. If not, audio is the primary type if present.
+ * Else text is the primary type if present. Else there is no primary type.
+ * <li>If there is exactly one extractor track of the primary type, it's expanded into (N+1)
+ * exposed tracks, all of which correspond to the primary extractor track and each of which
+ * corresponds to a different chunk source track. Selecting one of these tracks has the
+ * effect of switching the selected track on the chunk source.
+ * <li>All other extractor tracks are exposed directly. Selecting one of these tracks has the
+ * effect of selecting an extractor track, leaving the selected track on the chunk source
+ * unchanged.
+ * </ul>
+ */
+ @EnsuresNonNull({"trackGroups", "optionalTrackGroups", "trackGroupToSampleQueueIndex"})
+ private void buildTracksFromSampleStreams() {
+ // Iterate through the extractor tracks to discover the "primary" track type, and the index
+ // of the single track of this type.
+ int primaryExtractorTrackType = C.TRACK_TYPE_NONE;
+ int primaryExtractorTrackIndex = C.INDEX_UNSET;
+ int extractorTrackCount = sampleQueues.length;
+ for (int i = 0; i < extractorTrackCount; i++) {
+ String sampleMimeType = sampleQueues[i].getUpstreamFormat().sampleMimeType;
+ int trackType;
+ if (MimeTypes.isVideo(sampleMimeType)) {
+ trackType = C.TRACK_TYPE_VIDEO;
+ } else if (MimeTypes.isAudio(sampleMimeType)) {
+ trackType = C.TRACK_TYPE_AUDIO;
+ } else if (MimeTypes.isText(sampleMimeType)) {
+ trackType = C.TRACK_TYPE_TEXT;
+ } else {
+ trackType = C.TRACK_TYPE_NONE;
+ }
+ if (getTrackTypeScore(trackType) > getTrackTypeScore(primaryExtractorTrackType)) {
+ primaryExtractorTrackType = trackType;
+ primaryExtractorTrackIndex = i;
+ } else if (trackType == primaryExtractorTrackType
+ && primaryExtractorTrackIndex != C.INDEX_UNSET) {
+ // We have multiple tracks of the primary type. We only want an index if there only exists a
+ // single track of the primary type, so unset the index again.
+ primaryExtractorTrackIndex = C.INDEX_UNSET;
+ }
+ }
+
+ TrackGroup chunkSourceTrackGroup = chunkSource.getTrackGroup();
+ int chunkSourceTrackCount = chunkSourceTrackGroup.length;
+
+ // Instantiate the necessary internal data-structures.
+ primaryTrackGroupIndex = C.INDEX_UNSET;
+ trackGroupToSampleQueueIndex = new int[extractorTrackCount];
+ for (int i = 0; i < extractorTrackCount; i++) {
+ trackGroupToSampleQueueIndex[i] = i;
+ }
+
+ // Construct the set of exposed track groups.
+ TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount];
+ for (int i = 0; i < extractorTrackCount; i++) {
+ Format sampleFormat = sampleQueues[i].getUpstreamFormat();
+ if (i == primaryExtractorTrackIndex) {
+ Format[] formats = new Format[chunkSourceTrackCount];
+ if (chunkSourceTrackCount == 1) {
+ formats[0] = sampleFormat.copyWithManifestFormatInfo(chunkSourceTrackGroup.getFormat(0));
+ } else {
+ for (int j = 0; j < chunkSourceTrackCount; j++) {
+ formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat, true);
+ }
+ }
+ trackGroups[i] = new TrackGroup(formats);
+ primaryTrackGroupIndex = i;
+ } else {
+ Format trackFormat =
+ primaryExtractorTrackType == C.TRACK_TYPE_VIDEO
+ && MimeTypes.isAudio(sampleFormat.sampleMimeType)
+ ? muxedAudioFormat
+ : null;
+ trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false));
+ }
+ }
+ this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups);
+ Assertions.checkState(optionalTrackGroups == null);
+ optionalTrackGroups = Collections.emptySet();
+ }
+
+ private TrackGroupArray createTrackGroupArrayWithDrmInfo(TrackGroup[] trackGroups) {
+ for (int i = 0; i < trackGroups.length; i++) {
+ TrackGroup trackGroup = trackGroups[i];
+ Format[] exposedFormats = new Format[trackGroup.length];
+ for (int j = 0; j < trackGroup.length; j++) {
+ Format format = trackGroup.getFormat(j);
+ if (format.drmInitData != null) {
+ format =
+ format.copyWithExoMediaCryptoType(
+ drmSessionManager.getExoMediaCryptoType(format.drmInitData));
+ }
+ exposedFormats[j] = format;
+ }
+ trackGroups[i] = new TrackGroup(exposedFormats);
+ }
+ return new TrackGroupArray(trackGroups);
+ }
+
+ private HlsMediaChunk getLastMediaChunk() {
+ return mediaChunks.get(mediaChunks.size() - 1);
+ }
+
+ private boolean isPendingReset() {
+ return pendingResetPositionUs != C.TIME_UNSET;
+ }
+
+ /**
+ * Attempts to seek to the specified position within the sample queues.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @return Whether the in-buffer seek was successful.
+ */
+ private boolean seekInsideBufferUs(long positionUs) {
+ int sampleQueueCount = sampleQueues.length;
+ for (int i = 0; i < sampleQueueCount; i++) {
+ SampleQueue sampleQueue = sampleQueues[i];
+ boolean seekInsideQueue = sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false);
+ // If we have AV tracks then an in-queue seek is successful if the seek into every AV queue
+ // is successful. We ignore whether seeks within non-AV queues are successful in this case, as
+ // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is
+ // successful only if the seek into every queue succeeds.
+ if (!seekInsideQueue && (sampleQueueIsAudioVideoFlags[i] || !haveAudioVideoSampleQueues)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @RequiresNonNull({"trackGroups", "optionalTrackGroups"})
+ private void setIsPrepared() {
+ prepared = true;
+ }
+
+ @EnsuresNonNull({"trackGroups", "optionalTrackGroups"})
+ private void assertIsPrepared() {
+ Assertions.checkState(prepared);
+ Assertions.checkNotNull(trackGroups);
+ Assertions.checkNotNull(optionalTrackGroups);
+ }
+
+ /**
+ * Scores a track type. Where multiple tracks are muxed into a container, the track with the
+ * highest score is the primary track.
+ *
+ * @param trackType The track type.
+ * @return The score.
+ */
+ private static int getTrackTypeScore(int trackType) {
+ switch (trackType) {
+ case C.TRACK_TYPE_VIDEO:
+ return 3;
+ case C.TRACK_TYPE_AUDIO:
+ return 2;
+ case C.TRACK_TYPE_TEXT:
+ return 1;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Derives a track sample format from the corresponding format in the master playlist, and a
+ * sample format that may have been obtained from a chunk belonging to a different track.
+ *
+ * @param playlistFormat The format information obtained from the master playlist.
+ * @param sampleFormat The format information obtained from the samples.
+ * @param propagateBitrate Whether the bitrate from the playlist format should be included in the
+ * derived format.
+ * @return The derived track format.
+ */
+ private static Format deriveFormat(
+ @Nullable Format playlistFormat, Format sampleFormat, boolean propagateBitrate) {
+ if (playlistFormat == null) {
+ return sampleFormat;
+ }
+ int bitrate = propagateBitrate ? playlistFormat.bitrate : Format.NO_VALUE;
+ int channelCount =
+ playlistFormat.channelCount != Format.NO_VALUE
+ ? playlistFormat.channelCount
+ : sampleFormat.channelCount;
+ int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);
+ String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType);
+ String mimeType = MimeTypes.getMediaMimeType(codecs);
+ if (mimeType == null) {
+ mimeType = sampleFormat.sampleMimeType;
+ }
+ return sampleFormat.copyWithContainerInfo(
+ playlistFormat.id,
+ playlistFormat.label,
+ mimeType,
+ codecs,
+ playlistFormat.metadata,
+ bitrate,
+ playlistFormat.width,
+ playlistFormat.height,
+ channelCount,
+ playlistFormat.selectionFlags,
+ playlistFormat.language);
+ }
+
+ private static boolean isMediaChunk(Chunk chunk) {
+ return chunk instanceof HlsMediaChunk;
+ }
+
+ private static boolean formatsMatch(Format manifestFormat, Format sampleFormat) {
+ String manifestFormatMimeType = manifestFormat.sampleMimeType;
+ String sampleFormatMimeType = sampleFormat.sampleMimeType;
+ int manifestFormatTrackType = MimeTypes.getTrackType(manifestFormatMimeType);
+ if (manifestFormatTrackType != C.TRACK_TYPE_TEXT) {
+ return manifestFormatTrackType == MimeTypes.getTrackType(sampleFormatMimeType);
+ } else if (!Util.areEqual(manifestFormatMimeType, sampleFormatMimeType)) {
+ return false;
+ }
+ if (MimeTypes.APPLICATION_CEA608.equals(manifestFormatMimeType)
+ || MimeTypes.APPLICATION_CEA708.equals(manifestFormatMimeType)) {
+ return manifestFormat.accessibilityChannel == sampleFormat.accessibilityChannel;
+ }
+ return true;
+ }
+
+ private static DummyTrackOutput createDummyTrackOutput(int id, int type) {
+ Log.w(TAG, "Unmapped track with id " + id + " of type " + type);
+ return new DummyTrackOutput();
+ }
+
+ private static final class FormatAdjustingSampleQueue extends SampleQueue {
+
+ private final Map<String, DrmInitData> overridingDrmInitData;
+ @Nullable private DrmInitData drmInitData;
+
+ public FormatAdjustingSampleQueue(
+ Allocator allocator,
+ DrmSessionManager<?> drmSessionManager,
+ Map<String, DrmInitData> overridingDrmInitData) {
+ super(allocator, drmSessionManager);
+ this.overridingDrmInitData = overridingDrmInitData;
+ }
+
+ public void setDrmInitData(@Nullable DrmInitData drmInitData) {
+ this.drmInitData = drmInitData;
+ invalidateUpstreamFormatAdjustment();
+ }
+
+ @Override
+ public Format getAdjustedUpstreamFormat(Format format) {
+ @Nullable
+ DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData;
+ if (drmInitData != null) {
+ @Nullable
+ DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType);
+ if (overridingDrmInitData != null) {
+ drmInitData = overridingDrmInitData;
+ }
+ }
+ return super.getAdjustedUpstreamFormat(
+ format.copyWithAdjustments(drmInitData, getAdjustedMetadata(format.metadata)));
+ }
+
+ /**
+ * Strips the private timestamp frame from metadata, if present. See:
+ * https://github.com/google/ExoPlayer/issues/5063
+ */
+ @Nullable
+ private Metadata getAdjustedMetadata(@Nullable Metadata metadata) {
+ if (metadata == null) {
+ return null;
+ }
+ int length = metadata.length();
+ int transportStreamTimestampMetadataIndex = C.INDEX_UNSET;
+ for (int i = 0; i < length; i++) {
+ Metadata.Entry metadataEntry = metadata.get(i);
+ if (metadataEntry instanceof PrivFrame) {
+ PrivFrame privFrame = (PrivFrame) metadataEntry;
+ if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {
+ transportStreamTimestampMetadataIndex = i;
+ break;
+ }
+ }
+ }
+ if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) {
+ return metadata;
+ }
+ if (length == 1) {
+ return null;
+ }
+ Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1];
+ for (int i = 0; i < length; i++) {
+ if (i != transportStreamTimestampMetadataIndex) {
+ int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1;
+ newMetadataEntries[newIndex] = metadata.get(i);
+ }
+ }
+ return new Metadata(newMetadataEntries);
+ }
+ }
+
+ private static class EmsgUnwrappingTrackOutput implements TrackOutput {
+
+ private static final String TAG = "EmsgUnwrappingTrackOutput";
+
+ // TODO(ibaker): Create a Formats util class with common constants like this.
+ private static final Format ID3_FORMAT =
+ Format.createSampleFormat(
+ /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE);
+ private static final Format EMSG_FORMAT =
+ Format.createSampleFormat(
+ /* id= */ null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE);
+
+ private final EventMessageDecoder emsgDecoder;
+ private final TrackOutput delegate;
+ private final Format delegateFormat;
+ @MonotonicNonNull private Format format;
+
+ private byte[] buffer;
+ private int bufferPosition;
+
+ public EmsgUnwrappingTrackOutput(
+ TrackOutput delegate, @HlsMediaSource.MetadataType int metadataType) {
+ this.emsgDecoder = new EventMessageDecoder();
+ this.delegate = delegate;
+ switch (metadataType) {
+ case HlsMediaSource.METADATA_TYPE_ID3:
+ delegateFormat = ID3_FORMAT;
+ break;
+ case HlsMediaSource.METADATA_TYPE_EMSG:
+ delegateFormat = EMSG_FORMAT;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown metadataType: " + metadataType);
+ }
+
+ this.buffer = new byte[0];
+ this.bufferPosition = 0;
+ }
+
+ @Override
+ public void format(Format format) {
+ this.format = format;
+ delegate.format(delegateFormat);
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ ensureBufferCapacity(bufferPosition + length);
+ int numBytesRead = input.read(buffer, bufferPosition, length);
+ if (numBytesRead == C.RESULT_END_OF_INPUT) {
+ if (allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ } else {
+ throw new EOFException();
+ }
+ }
+ bufferPosition += numBytesRead;
+ return numBytesRead;
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray buffer, int length) {
+ ensureBufferCapacity(bufferPosition + length);
+ buffer.readBytes(this.buffer, bufferPosition, length);
+ bufferPosition += length;
+ }
+
+ @Override
+ public void sampleMetadata(
+ long timeUs,
+ @C.BufferFlags int flags,
+ int size,
+ int offset,
+ @Nullable CryptoData cryptoData) {
+ Assertions.checkNotNull(format);
+ ParsableByteArray sample = getSampleAndTrimBuffer(size, offset);
+ ParsableByteArray sampleForDelegate;
+ if (Util.areEqual(format.sampleMimeType, delegateFormat.sampleMimeType)) {
+ // Incoming format matches delegate track's format, so pass straight through.
+ sampleForDelegate = sample;
+ } else if (MimeTypes.APPLICATION_EMSG.equals(format.sampleMimeType)) {
+ // Incoming sample is EMSG, and delegate track is not expecting EMSG, so try unwrapping.
+ EventMessage emsg = emsgDecoder.decode(sample);
+ if (!emsgContainsExpectedWrappedFormat(emsg)) {
+ Log.w(
+ TAG,
+ String.format(
+ "Ignoring EMSG. Expected it to contain wrapped %s but actual wrapped format: %s",
+ delegateFormat.sampleMimeType, emsg.getWrappedMetadataFormat()));
+ return;
+ }
+ sampleForDelegate =
+ new ParsableByteArray(Assertions.checkNotNull(emsg.getWrappedMetadataBytes()));
+ } else {
+ Log.w(TAG, "Ignoring sample for unsupported format: " + format.sampleMimeType);
+ return;
+ }
+
+ int sampleSize = sampleForDelegate.bytesLeft();
+
+ delegate.sampleData(sampleForDelegate, sampleSize);
+ delegate.sampleMetadata(timeUs, flags, sampleSize, offset, cryptoData);
+ }
+
+ private boolean emsgContainsExpectedWrappedFormat(EventMessage emsg) {
+ @Nullable Format wrappedMetadataFormat = emsg.getWrappedMetadataFormat();
+ return wrappedMetadataFormat != null
+ && Util.areEqual(delegateFormat.sampleMimeType, wrappedMetadataFormat.sampleMimeType);
+ }
+
+ private void ensureBufferCapacity(int requiredLength) {
+ if (buffer.length < requiredLength) {
+ buffer = Arrays.copyOf(buffer, requiredLength + requiredLength / 2);
+ }
+ }
+
+ /**
+ * Removes a complete sample from the {@link #buffer} field & reshuffles the tail data skipped
+ * by {@code offset} to the head of the array.
+ *
+ * @param size see {@code size} param of {@link #sampleMetadata}.
+ * @param offset see {@code offset} param of {@link #sampleMetadata}.
+ * @return A {@link ParsableByteArray} containing the sample removed from {@link #buffer}.
+ */
+ private ParsableByteArray getSampleAndTrimBuffer(int size, int offset) {
+ int sampleEnd = bufferPosition - offset;
+ int sampleStart = sampleEnd - size;
+
+ byte[] sampleBytes = Arrays.copyOfRange(buffer, sampleStart, sampleEnd);
+ ParsableByteArray sample = new ParsableByteArray(sampleBytes);
+
+ System.arraycopy(buffer, sampleEnd, buffer, 0, offset);
+ bufferPosition = offset;
+ return sample;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java
new file mode 100644
index 0000000000..681fe57240
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java
@@ -0,0 +1,245 @@
+/*
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Holds metadata associated to an HLS media track. */
+public final class HlsTrackMetadataEntry implements Metadata.Entry {
+
+ /** Holds attributes defined in an EXT-X-STREAM-INF tag. */
+ public static final class VariantInfo implements Parcelable {
+
+ /** The bitrate as declared by the EXT-X-STREAM-INF tag. */
+ public final long bitrate;
+
+ /**
+ * The VIDEO value as defined in the EXT-X-STREAM-INF tag, or null if the VIDEO attribute is not
+ * present.
+ */
+ @Nullable public final String videoGroupId;
+
+ /**
+ * The AUDIO value as defined in the EXT-X-STREAM-INF tag, or null if the AUDIO attribute is not
+ * present.
+ */
+ @Nullable public final String audioGroupId;
+
+ /**
+ * The SUBTITLES value as defined in the EXT-X-STREAM-INF tag, or null if the SUBTITLES
+ * attribute is not present.
+ */
+ @Nullable public final String subtitleGroupId;
+
+ /**
+ * The CLOSED-CAPTIONS value as defined in the EXT-X-STREAM-INF tag, or null if the
+ * CLOSED-CAPTIONS attribute is not present.
+ */
+ @Nullable public final String captionGroupId;
+
+ /**
+ * Creates an instance.
+ *
+ * @param bitrate See {@link #bitrate}.
+ * @param videoGroupId See {@link #videoGroupId}.
+ * @param audioGroupId See {@link #audioGroupId}.
+ * @param subtitleGroupId See {@link #subtitleGroupId}.
+ * @param captionGroupId See {@link #captionGroupId}.
+ */
+ public VariantInfo(
+ long bitrate,
+ @Nullable String videoGroupId,
+ @Nullable String audioGroupId,
+ @Nullable String subtitleGroupId,
+ @Nullable String captionGroupId) {
+ this.bitrate = bitrate;
+ this.videoGroupId = videoGroupId;
+ this.audioGroupId = audioGroupId;
+ this.subtitleGroupId = subtitleGroupId;
+ this.captionGroupId = captionGroupId;
+ }
+
+ /* package */ VariantInfo(Parcel in) {
+ bitrate = in.readLong();
+ videoGroupId = in.readString();
+ audioGroupId = in.readString();
+ subtitleGroupId = in.readString();
+ captionGroupId = in.readString();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ VariantInfo that = (VariantInfo) other;
+ return bitrate == that.bitrate
+ && TextUtils.equals(videoGroupId, that.videoGroupId)
+ && TextUtils.equals(audioGroupId, that.audioGroupId)
+ && TextUtils.equals(subtitleGroupId, that.subtitleGroupId)
+ && TextUtils.equals(captionGroupId, that.captionGroupId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (bitrate ^ (bitrate >>> 32));
+ result = 31 * result + (videoGroupId != null ? videoGroupId.hashCode() : 0);
+ result = 31 * result + (audioGroupId != null ? audioGroupId.hashCode() : 0);
+ result = 31 * result + (subtitleGroupId != null ? subtitleGroupId.hashCode() : 0);
+ result = 31 * result + (captionGroupId != null ? captionGroupId.hashCode() : 0);
+ return result;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(bitrate);
+ dest.writeString(videoGroupId);
+ dest.writeString(audioGroupId);
+ dest.writeString(subtitleGroupId);
+ dest.writeString(captionGroupId);
+ }
+
+ public static final Parcelable.Creator<VariantInfo> CREATOR =
+ new Parcelable.Creator<VariantInfo>() {
+ @Override
+ public VariantInfo createFromParcel(Parcel in) {
+ return new VariantInfo(in);
+ }
+
+ @Override
+ public VariantInfo[] newArray(int size) {
+ return new VariantInfo[size];
+ }
+ };
+ }
+
+ /**
+ * The GROUP-ID value of this track, if the track is derived from an EXT-X-MEDIA tag. Null if the
+ * track is not derived from an EXT-X-MEDIA TAG.
+ */
+ @Nullable public final String groupId;
+ /**
+ * The NAME value of this track, if the track is derived from an EXT-X-MEDIA tag. Null if the
+ * track is not derived from an EXT-X-MEDIA TAG.
+ */
+ @Nullable public final String name;
+ /**
+ * The EXT-X-STREAM-INF tags attributes associated with this track. This field is non-applicable
+ * (and therefore empty) if this track is derived from an EXT-X-MEDIA tag.
+ */
+ public final List<VariantInfo> variantInfos;
+
+ /**
+ * Creates an instance.
+ *
+ * @param groupId See {@link #groupId}.
+ * @param name See {@link #name}.
+ * @param variantInfos See {@link #variantInfos}.
+ */
+ public HlsTrackMetadataEntry(
+ @Nullable String groupId, @Nullable String name, List<VariantInfo> variantInfos) {
+ this.groupId = groupId;
+ this.name = name;
+ this.variantInfos = Collections.unmodifiableList(new ArrayList<>(variantInfos));
+ }
+
+ /* package */ HlsTrackMetadataEntry(Parcel in) {
+ groupId = in.readString();
+ name = in.readString();
+ int variantInfoSize = in.readInt();
+ ArrayList<VariantInfo> variantInfos = new ArrayList<>(variantInfoSize);
+ for (int i = 0; i < variantInfoSize; i++) {
+ variantInfos.add(in.readParcelable(VariantInfo.class.getClassLoader()));
+ }
+ this.variantInfos = Collections.unmodifiableList(variantInfos);
+ }
+
+ @Override
+ public String toString() {
+ return "HlsTrackMetadataEntry" + (groupId != null ? (" [" + groupId + ", " + name + "]") : "");
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+
+ HlsTrackMetadataEntry that = (HlsTrackMetadataEntry) other;
+ return TextUtils.equals(groupId, that.groupId)
+ && TextUtils.equals(name, that.name)
+ && variantInfos.equals(that.variantInfos);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = groupId != null ? groupId.hashCode() : 0;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + variantInfos.hashCode();
+ return result;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(groupId);
+ dest.writeString(name);
+ int variantInfosSize = variantInfos.size();
+ dest.writeInt(variantInfosSize);
+ for (int i = 0; i < variantInfosSize; i++) {
+ dest.writeParcelable(variantInfos.get(i), /* parcelableFlags= */ 0);
+ }
+ }
+
+ public static final Parcelable.Creator<HlsTrackMetadataEntry> CREATOR =
+ new Parcelable.Creator<HlsTrackMetadataEntry>() {
+ @Override
+ public HlsTrackMetadataEntry createFromParcel(Parcel in) {
+ return new HlsTrackMetadataEntry(in);
+ }
+
+ @Override
+ public HlsTrackMetadataEntry[] newArray(int size) {
+ return new HlsTrackMetadataEntry[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java
new file mode 100644
index 0000000000..a67a92b4b7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 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;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import java.io.IOException;
+
+/** Thrown when it is not possible to map a {@link TrackGroup} to a {@link SampleQueue}. */
+public final class SampleQueueMappingException extends IOException {
+
+ /** @param mimeType The mime type of the track group whose mapping failed. */
+ public SampleQueueMappingException(@Nullable String mimeType) {
+ super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + ".");
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java
new file mode 100644
index 0000000000..e2a652d05c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+
+import android.util.SparseArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Provides {@link TimestampAdjuster} instances for use during HLS playbacks.
+ */
+public final class TimestampAdjusterProvider {
+
+ // TODO: Prevent this array from growing indefinitely large by removing adjusters that are no
+ // longer required.
+ private final SparseArray<TimestampAdjuster> timestampAdjusters;
+
+ public TimestampAdjusterProvider() {
+ timestampAdjusters = new SparseArray<>();
+ }
+
+ /**
+ * Returns a {@link TimestampAdjuster} suitable for adjusting the pts timestamps contained in
+ * a chunk with a given discontinuity sequence.
+ *
+ * @param discontinuitySequence The chunk's discontinuity sequence.
+ * @return A {@link TimestampAdjuster}.
+ */
+ public TimestampAdjuster getAdjuster(int discontinuitySequence) {
+ TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence);
+ if (adjuster == null) {
+ adjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET);
+ timestampAdjusters.put(discontinuitySequence, adjuster);
+ }
+ return adjuster;
+ }
+
+ /**
+ * Resets the provider.
+ */
+ public void reset() {
+ timestampAdjusters.clear();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java
new file mode 100644
index 0000000000..1d5e669a03
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java
@@ -0,0 +1,195 @@
+/*
+ * 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;
+
+import android.text.TextUtils;
+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.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt.WebvttParserUtil;
+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.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * A special purpose extractor for WebVTT content in HLS.
+ *
+ * <p>This extractor passes through non-empty WebVTT files untouched, however derives the correct
+ * sample timestamp for each by sniffing the X-TIMESTAMP-MAP header along with the start timestamp
+ * of the first cue header. Empty WebVTT files are not passed through, since it's not possible to
+ * derive a sample timestamp in this case.
+ */
+public final class WebvttExtractor implements Extractor {
+
+ private static final Pattern LOCAL_TIMESTAMP = Pattern.compile("LOCAL:([^,]+)");
+ private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(-?\\d+)");
+ private static final int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */;
+ private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH;
+
+ @Nullable private final String language;
+ private final TimestampAdjuster timestampAdjuster;
+ private final ParsableByteArray sampleDataWrapper;
+
+ private @MonotonicNonNull ExtractorOutput output;
+
+ private byte[] sampleData;
+ private int sampleSize;
+
+ public WebvttExtractor(@Nullable String language, TimestampAdjuster timestampAdjuster) {
+ this.language = language;
+ this.timestampAdjuster = timestampAdjuster;
+ this.sampleDataWrapper = new ParsableByteArray();
+ sampleData = new byte[1024];
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Check whether there is a header without BOM.
+ input.peekFully(
+ sampleData, /* offset= */ 0, /* length= */ HEADER_MIN_LENGTH, /* allowEndOfInput= */ false);
+ sampleDataWrapper.reset(sampleData, HEADER_MIN_LENGTH);
+ if (WebvttParserUtil.isWebvttHeaderLine(sampleDataWrapper)) {
+ return true;
+ }
+ // The header did not match, try including the BOM.
+ input.peekFully(
+ sampleData,
+ /* offset= */ HEADER_MIN_LENGTH,
+ HEADER_MAX_LENGTH - HEADER_MIN_LENGTH,
+ /* allowEndOfInput= */ false);
+ sampleDataWrapper.reset(sampleData, HEADER_MAX_LENGTH);
+ return WebvttParserUtil.isWebvttHeaderLine(sampleDataWrapper);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ // This extractor is only used for the HLS use case, which should not call this method.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ // output == null suggests init() hasn't been called
+ Assertions.checkNotNull(output);
+ int currentFileSize = (int) input.getLength();
+
+ // Increase the size of sampleData if necessary.
+ if (sampleSize == sampleData.length) {
+ sampleData = Arrays.copyOf(sampleData,
+ (currentFileSize != C.LENGTH_UNSET ? currentFileSize : sampleData.length) * 3 / 2);
+ }
+
+ // Consume to the input.
+ int bytesRead = input.read(sampleData, sampleSize, sampleData.length - sampleSize);
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ sampleSize += bytesRead;
+ if (currentFileSize == C.LENGTH_UNSET || sampleSize != currentFileSize) {
+ return Extractor.RESULT_CONTINUE;
+ }
+ }
+
+ // We've reached the end of the input, which corresponds to the end of the current file.
+ processSample();
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+
+ @RequiresNonNull("output")
+ private void processSample() throws ParserException {
+ ParsableByteArray webvttData = new ParsableByteArray(sampleData);
+
+ // Validate the first line of the header.
+ WebvttParserUtil.validateWebvttHeaderLine(webvttData);
+
+ // Defaults to use if the header doesn't contain an X-TIMESTAMP-MAP header.
+ long vttTimestampUs = 0;
+ long tsTimestampUs = 0;
+
+ // Parse the remainder of the header looking for X-TIMESTAMP-MAP.
+ for (String line = webvttData.readLine();
+ !TextUtils.isEmpty(line);
+ line = webvttData.readLine()) {
+ if (line.startsWith("X-TIMESTAMP-MAP")) {
+ Matcher localTimestampMatcher = LOCAL_TIMESTAMP.matcher(line);
+ if (!localTimestampMatcher.find()) {
+ throw new ParserException("X-TIMESTAMP-MAP doesn't contain local timestamp: " + line);
+ }
+ Matcher mediaTimestampMatcher = MEDIA_TIMESTAMP.matcher(line);
+ if (!mediaTimestampMatcher.find()) {
+ throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line);
+ }
+ vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1));
+ tsTimestampUs = TimestampAdjuster.ptsToUs(Long.parseLong(mediaTimestampMatcher.group(1)));
+ }
+ }
+
+ // Find the first cue header and parse the start time.
+ Matcher cueHeaderMatcher = WebvttParserUtil.findNextCueHeader(webvttData);
+ if (cueHeaderMatcher == null) {
+ // No cues found. Don't output a sample, but still output a corresponding track.
+ buildTrackOutput(0);
+ return;
+ }
+
+ long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1));
+ long sampleTimeUs = timestampAdjuster.adjustTsTimestamp(
+ TimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs));
+ long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs;
+ // Output the track.
+ TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs);
+ // Output the sample.
+ sampleDataWrapper.reset(sampleData, sampleSize);
+ trackOutput.sampleData(sampleDataWrapper, sampleSize);
+ trackOutput.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ }
+
+ @RequiresNonNull("output")
+ private TrackOutput buildTrackOutput(long subsampleOffsetUs) {
+ TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT);
+ trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null,
+ Format.NO_VALUE, 0, language, null, subsampleOffsetUs));
+ output.endTracks();
+ return trackOutput;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
new file mode 100644
index 0000000000..636100a8a9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2017 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.offline;
+
+import android.net.Uri;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.SegmentDownloader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.UriUtil;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * A downloader for HLS streams.
+ *
+ * <p>Example usage:
+ *
+ * <pre>{@code
+ * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
+ * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
+ * DownloaderConstructorHelper constructorHelper =
+ * new DownloaderConstructorHelper(cache, factory);
+ * // Create a downloader for the first variant in a master playlist.
+ * HlsDownloader hlsDownloader =
+ * new HlsDownloader(
+ * playlistUri,
+ * Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)),
+ * constructorHelper);
+ * // Perform the download.
+ * hlsDownloader.download(progressListener);
+ * // Access downloaded data using CacheDataSource
+ * CacheDataSource cacheDataSource =
+ * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
+ * }</pre>
+ */
+public final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {
+
+ /**
+ * @param playlistUri The {@link Uri} of the playlist to be downloaded.
+ * @param streamKeys Keys defining which renditions in the playlist should be selected for
+ * download. If empty, all renditions are downloaded.
+ * @param constructorHelper A {@link DownloaderConstructorHelper} instance.
+ */
+ public HlsDownloader(
+ Uri playlistUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {
+ super(playlistUri, streamKeys, constructorHelper);
+ }
+
+ @Override
+ protected HlsPlaylist getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException {
+ return loadManifest(dataSource, dataSpec);
+ }
+
+ @Override
+ protected List<Segment> getSegments(
+ DataSource dataSource, HlsPlaylist playlist, boolean allowIncompleteList) throws IOException {
+ ArrayList<DataSpec> mediaPlaylistDataSpecs = new ArrayList<>();
+ if (playlist instanceof HlsMasterPlaylist) {
+ HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
+ addMediaPlaylistDataSpecs(masterPlaylist.mediaPlaylistUrls, mediaPlaylistDataSpecs);
+ } else {
+ mediaPlaylistDataSpecs.add(
+ SegmentDownloader.getCompressibleDataSpec(Uri.parse(playlist.baseUri)));
+ }
+
+ ArrayList<Segment> segments = new ArrayList<>();
+ HashSet<Uri> seenEncryptionKeyUris = new HashSet<>();
+ for (DataSpec mediaPlaylistDataSpec : mediaPlaylistDataSpecs) {
+ segments.add(new Segment(/* startTimeUs= */ 0, mediaPlaylistDataSpec));
+ HlsMediaPlaylist mediaPlaylist;
+ try {
+ mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, mediaPlaylistDataSpec);
+ } catch (IOException e) {
+ if (!allowIncompleteList) {
+ throw e;
+ }
+ // Generating an incomplete segment list is allowed. Advance to the next media playlist.
+ continue;
+ }
+ HlsMediaPlaylist.Segment lastInitSegment = null;
+ List<HlsMediaPlaylist.Segment> hlsSegments = mediaPlaylist.segments;
+ for (int i = 0; i < hlsSegments.size(); i++) {
+ HlsMediaPlaylist.Segment segment = hlsSegments.get(i);
+ HlsMediaPlaylist.Segment initSegment = segment.initializationSegment;
+ if (initSegment != null && initSegment != lastInitSegment) {
+ lastInitSegment = initSegment;
+ addSegment(mediaPlaylist, initSegment, seenEncryptionKeyUris, segments);
+ }
+ addSegment(mediaPlaylist, segment, seenEncryptionKeyUris, segments);
+ }
+ }
+ return segments;
+ }
+
+ private void addMediaPlaylistDataSpecs(List<Uri> mediaPlaylistUrls, List<DataSpec> out) {
+ for (int i = 0; i < mediaPlaylistUrls.size(); i++) {
+ out.add(SegmentDownloader.getCompressibleDataSpec(mediaPlaylistUrls.get(i)));
+ }
+ }
+
+ private static HlsPlaylist loadManifest(DataSource dataSource, DataSpec dataSpec)
+ throws IOException {
+ return ParsingLoadable.load(
+ dataSource, new HlsPlaylistParser(), dataSpec, C.DATA_TYPE_MANIFEST);
+ }
+
+ private void addSegment(
+ HlsMediaPlaylist mediaPlaylist,
+ HlsMediaPlaylist.Segment segment,
+ HashSet<Uri> seenEncryptionKeyUris,
+ ArrayList<Segment> out) {
+ String baseUri = mediaPlaylist.baseUri;
+ long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;
+ if (segment.fullSegmentEncryptionKeyUri != null) {
+ Uri keyUri = UriUtil.resolveToUri(baseUri, segment.fullSegmentEncryptionKeyUri);
+ if (seenEncryptionKeyUris.add(keyUri)) {
+ out.add(new Segment(startTimeUs, SegmentDownloader.getCompressibleDataSpec(keyUri)));
+ }
+ }
+ Uri segmentUri = UriUtil.resolveToUri(baseUri, segment.url);
+ DataSpec dataSpec =
+ new DataSpec(segmentUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null);
+ out.add(new Segment(startTimeUs, dataSpec));
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java
new file mode 100644
index 0000000000..669bd44c89
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/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.offline;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java
new file mode 100644
index 0000000000..89882bb596
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/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;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;
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;