summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java668
1 files changed, 668 insertions, 0 deletions
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;
+ }
+ }
+}