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