content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package com.google.android.exoplayer2.source;
  
  import android.net.Uri;
  import android.os.Handler;
  import android.support.annotation.Nullable;
  import com.google.android.exoplayer2.C;
  import com.google.android.exoplayer2.Format;
  import com.google.android.exoplayer2.FormatHolder;
  import com.google.android.exoplayer2.SeekParameters;
  import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
  import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
  import com.google.android.exoplayer2.extractor.Extractor;
  import com.google.android.exoplayer2.extractor.ExtractorInput;
  import com.google.android.exoplayer2.extractor.ExtractorOutput;
  import com.google.android.exoplayer2.extractor.PositionHolder;
  import com.google.android.exoplayer2.extractor.SeekMap;
  import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
  import com.google.android.exoplayer2.extractor.TrackOutput;
  import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
  import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
  import com.google.android.exoplayer2.trackselection.TrackSelection;
  import com.google.android.exoplayer2.upstream.Allocator;
  import com.google.android.exoplayer2.upstream.DataSource;
  import com.google.android.exoplayer2.upstream.DataSpec;
  import com.google.android.exoplayer2.upstream.Loader;
  import com.google.android.exoplayer2.upstream.Loader.Loadable;
  import com.google.android.exoplayer2.util.Assertions;
  import com.google.android.exoplayer2.util.ConditionVariable;
  import com.google.android.exoplayer2.util.MimeTypes;
  import com.google.android.exoplayer2.util.Util;
  import java.io.EOFException;
  import java.io.IOException;
  import java.util.Arrays;
  
  /**
   * A {@link MediaPeriod} that extracts data using an {@link Extractor}.
   */
  /* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput,
      Loader.Callback<ExtractorMediaPeriod.ExtractingLoadable>, Loader.ReleaseCallback,
      UpstreamFormatChangedListener {
  
    /**
     * Listener for information about the period.
     */
    interface Listener {
  
      /**
       * Called when the duration or ability to seek within the period changes.
       *
       * @param durationUs The duration of the period, or {@link C#TIME_UNSET}.
       * @param isSeekable Whether the period is seekable.
       */
      void onSourceInfoRefreshed(long durationUs, boolean isSeekable);
  
    }
  
    /**
     * 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 final Uri uri;
    private final DataSource dataSource;
    private final int minLoadableRetryCount;
    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;
  
    private Callback callback;
    private SeekMap seekMap;
    private SampleQueue[] sampleQueues;
    private int[] sampleQueueTrackIds;
    private boolean sampleQueuesBuilt;
    private boolean prepared;
    private int actualMinLoadableRetryCount;
  
    private boolean seenFirstTrackSelection;
    private boolean notifyDiscontinuity;
    private int enabledTrackCount;
    private TrackGroupArray tracks;
    private long durationUs;
    private boolean[] trackEnabledStates;
    private boolean[] trackIsAudioVideoFlags;
    private boolean[] trackFormatNotificationSent;
    private boolean haveAudioVideoTracks;
    private long length;
  
    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 minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
     * @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)}.
     */
    public ExtractorMediaPeriod(
        Uri uri,
        DataSource dataSource,
        Extractor[] extractors,
        int minLoadableRetryCount,
        EventDispatcher eventDispatcher,
        Listener listener,
        Allocator allocator,
        @Nullable String customCacheKey,
        int continueLoadingCheckIntervalBytes) {
      this.uri = uri;
      this.dataSource = dataSource;
      this.minLoadableRetryCount = minLoadableRetryCount;
      this.eventDispatcher = eventDispatcher;
      this.listener = listener;
      this.allocator = allocator;
      this.customCacheKey = customCacheKey;
      this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
      loader = new Loader("Loader:ExtractorMediaPeriod");
      extractorHolder = new ExtractorHolder(extractors, this);
      loadCondition = new ConditionVariable();
      maybeFinishPrepareRunnable = new Runnable() {
        @Override
        public void run() {
          maybeFinishPrepare();
        }
      };
      onContinueLoadingRequestedRunnable = new Runnable() {
        @Override
        public void run() {
          if (!released) {
            callback.onContinueLoadingRequested(ExtractorMediaPeriod.this);
          }
        }
      };
      handler = new Handler();
      sampleQueueTrackIds = new int[0];
      sampleQueues = new SampleQueue[0];
      pendingResetPositionUs = C.TIME_UNSET;
      length = C.LENGTH_UNSET;
      durationUs = C.TIME_UNSET;
      // Assume on-demand for MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, until prepared.
      actualMinLoadableRetryCount =
          minLoadableRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA
          ? ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND
          : minLoadableRetryCount;
    }
  
    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.discardToEnd();
        }
      }
      loader.release(this);
      handler.removeCallbacksAndMessages(null);
      released = true;
    }
  
    @Override
    public void onLoaderReleased() {
      for (SampleQueue sampleQueue : sampleQueues) {
        sampleQueue.reset();
      }
      extractorHolder.release();
    }
  
    @Override
    public void prepare(Callback callback, long positionUs) {
      this.callback = callback;
      loadCondition.open();
      startLoading();
    }
  
    @Override
    public void maybeThrowPrepareError() throws IOException {
      maybeThrowError();
    }
  
    @Override
    public TrackGroupArray getTrackGroups() {
      return tracks;
    }
  
    @Override
    public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
        SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
      Assertions.checkState(prepared);
      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];
            sampleQueue.rewind();
            // A seek can be avoided if we're able to advance 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.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED
                && 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) {
      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 || pendingDeferredRetry || (prepared && enabledTrackCount == 0)) {
        return false;
      }
      boolean continuedLoading = loadCondition.open();
      if (!loader.isLoading()) {
        startLoading();
        continuedLoading = true;
      }
      return continuedLoading;
    }
  
    @Override
    public long getNextLoadPositionUs() {
      return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs();
    }
  
    @Override
    public long readDiscontinuity() {
      if (notifyDiscontinuity
          && (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) {
        notifyDiscontinuity = false;
        return lastSeekPositionUs;
      }
      return C.TIME_UNSET;
    }
  
    @Override
    public long getBufferedPositionUs() {
      if (loadingFinished) {
        return C.TIME_END_OF_SOURCE;
      } else if (isPendingReset()) {
        return pendingResetPositionUs;
      }
      long largestQueuedTimestampUs;
      if (haveAudioVideoTracks) {
        // Ignore non-AV tracks, which may be sparse or poorly interleaved.
        largestQueuedTimestampUs = Long.MAX_VALUE;
        int trackCount = sampleQueues.length;
        for (int i = 0; i < trackCount; i++) {
          if (trackIsAudioVideoFlags[i]) {
            largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
                sampleQueues[i].getLargestQueuedTimestampUs());
          }
        }
      } else {
        largestQueuedTimestampUs = getLargestQueuedTimestampUs();
      }
      return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
          : largestQueuedTimestampUs;
    }
  
    @Override
    public long seekToUs(long positionUs) {
      // Treat all seeks into non-seekable media as being to t=0.
      positionUs = seekMap.isSeekable() ? positionUs : 0;
      lastSeekPositionUs = positionUs;
      notifyDiscontinuity = false;
      // If we're not pending a reset, see if we can seek within the buffer.
      if (!isPendingReset() && seekInsideBufferUs(positionUs)) {
        return positionUs;
      }
      // We were unable to seek within the buffer, so need to reset.
      pendingDeferredRetry = false;
      pendingResetPositionUs = positionUs;
      loadingFinished = false;
      if (loader.isLoading()) {
        loader.cancelLoading();
      } else {
        for (SampleQueue sampleQueue : sampleQueues) {
          sampleQueue.reset();
        }
      }
      return positionUs;
    }
  
    @Override
    public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
      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() && (loadingFinished || sampleQueues[track].hasNextSample());
    }
  
    /* package */ void maybeThrowError() throws IOException {
      loader.maybeThrowError(actualMinLoadableRetryCount);
    }
  
    /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer,
        boolean formatRequired) {
      if (suppressRead()) {
        return C.RESULT_NOTHING_READ;
      }
      int result =
          sampleQueues[track].read(
              formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs);
      if (result == C.RESULT_BUFFER_READ) {
        maybeNotifyTrackFormat(track);
      } else if (result == C.RESULT_NOTHING_READ) {
        maybeStartDeferredRetry(track);
      }
      return result;
    }
  
    /* package */ int skipData(int track, long positionUs) {
      if (suppressRead()) {
        return 0;
      }
      SampleQueue sampleQueue = sampleQueues[track];
      int skipCount;
      if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
        skipCount = sampleQueue.advanceToEnd();
      } else {
        skipCount = sampleQueue.advanceTo(positionUs, true, true);
        if (skipCount == SampleQueue.ADVANCE_FAILED) {
          skipCount = 0;
        }
      }
      if (skipCount > 0) {
        maybeNotifyTrackFormat(track);
      } else {
        maybeStartDeferredRetry(track);
      }
      return skipCount;
    }
  
    private void maybeNotifyTrackFormat(int track) {
      if (!trackFormatNotificationSent[track]) {
        Format trackFormat = tracks.get(track).getFormat(0);
        eventDispatcher.downstreamFormatChanged(
            MimeTypes.getTrackType(trackFormat.sampleMimeType),
            trackFormat,
            C.SELECTION_REASON_UNKNOWN,
            /* trackSelectionData= */ null,
            lastSeekPositionUs);
        trackFormatNotificationSent[track] = true;
      }
    }
  
    private void maybeStartDeferredRetry(int track) {
      if (!pendingDeferredRetry
          || !trackIsAudioVideoFlags[track]
          || sampleQueues[track].hasNextSample()) {
        return;
      }
      pendingResetPositionUs = 0;
      pendingDeferredRetry = false;
      notifyDiscontinuity = true;
      lastSeekPositionUs = 0;
      extractedSamplesCountAtStartOfLoad = 0;
      for (SampleQueue sampleQueue : sampleQueues) {
        sampleQueue.reset();
      }
      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) {
        long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
        durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0
            : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
        listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable());
      }
      eventDispatcher.loadCompleted(
          loadable.dataSpec,
          C.DATA_TYPE_MEDIA,
          C.TRACK_TYPE_UNKNOWN,
          /* trackFormat= */ null,
          C.SELECTION_REASON_UNKNOWN,
          /* trackSelectionData= */ null,
          /* mediaStartTimeUs= */ loadable.seekTimeUs,
          durationUs,
          elapsedRealtimeMs,
          loadDurationMs,
          loadable.bytesLoaded);
      copyLengthFromLoader(loadable);
      loadingFinished = true;
      callback.onContinueLoadingRequested(this);
    }
  
    @Override
    public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,
        long loadDurationMs, boolean released) {
      eventDispatcher.loadCanceled(
          loadable.dataSpec,
          C.DATA_TYPE_MEDIA,
          C.TRACK_TYPE_UNKNOWN,
          /* trackFormat= */ null,
          C.SELECTION_REASON_UNKNOWN,
          /* trackSelectionData= */ null,
          /* mediaStartTimeUs= */ loadable.seekTimeUs,
          durationUs,
          elapsedRealtimeMs,
          loadDurationMs,
          loadable.bytesLoaded);
      if (!released) {
        copyLengthFromLoader(loadable);
        for (SampleQueue sampleQueue : sampleQueues) {
          sampleQueue.reset();
        }
        if (enabledTrackCount > 0) {
          callback.onContinueLoadingRequested(this);
        }
      }
    }
  
    @Override
    public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs,
        long loadDurationMs, IOException error) {
      boolean isErrorFatal = isLoadableExceptionFatal(error);
      eventDispatcher.loadError(
          loadable.dataSpec,
          C.DATA_TYPE_MEDIA,
          C.TRACK_TYPE_UNKNOWN,
          /* trackFormat= */ null,
          C.SELECTION_REASON_UNKNOWN,
          /* trackSelectionData= */ null,
          /* mediaStartTimeUs= */ loadable.seekTimeUs,
          durationUs,
          elapsedRealtimeMs,
          loadDurationMs,
          loadable.bytesLoaded,
          error,
          /* wasCanceled= */ isErrorFatal);
      copyLengthFromLoader(loadable);
      if (isErrorFatal) {
        return Loader.DONT_RETRY_FATAL;
      }
      int extractedSamplesCount = getExtractedSamplesCount();
      boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad;
      return configureRetry(loadable, extractedSamplesCount)
          ? (madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY)
          : Loader.DONT_RETRY;
    }
  
    // ExtractorOutput implementation. Called by the loading thread.
  
    @Override
    public TrackOutput track(int id, int type) {
      int trackCount = sampleQueues.length;
      for (int i = 0; i < trackCount; i++) {
        if (sampleQueueTrackIds[i] == id) {
          return sampleQueues[i];
        }
      }
      SampleQueue trackOutput = new SampleQueue(allocator);
      trackOutput.setUpstreamFormatChangeListener(this);
      sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
      sampleQueueTrackIds[trackCount] = id;
      sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1);
      sampleQueues[trackCount] = trackOutput;
      return trackOutput;
    }
  
    @Override
    public void endTracks() {
      sampleQueuesBuilt = true;
      handler.post(maybeFinishPrepareRunnable);
    }
  
    @Override
    public void seekMap(SeekMap seekMap) {
      this.seekMap = seekMap;
      handler.post(maybeFinishPrepareRunnable);
    }
  
    // UpstreamFormatChangedListener implementation. Called by the loading thread.
  
    @Override
    public void onUpstreamFormatChanged(Format format) {
      handler.post(maybeFinishPrepareRunnable);
    }
  
    // Internal methods.
  
    private void maybeFinishPrepare() {
      if (released || prepared || seekMap == null || !sampleQueuesBuilt) {
        return;
      }
      for (SampleQueue sampleQueue : sampleQueues) {
        if (sampleQueue.getUpstreamFormat() == null) {
          return;
        }
      }
      loadCondition.close();
      int trackCount = sampleQueues.length;
      TrackGroup[] trackArray = new TrackGroup[trackCount];
      trackIsAudioVideoFlags = new boolean[trackCount];
      trackEnabledStates = new boolean[trackCount];
      trackFormatNotificationSent = new boolean[trackCount];
      durationUs = seekMap.getDurationUs();
      for (int i = 0; i < trackCount; i++) {
        Format trackFormat = sampleQueues[i].getUpstreamFormat();
        trackArray[i] = new TrackGroup(trackFormat);
        String mimeType = trackFormat.sampleMimeType;
        boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType);
        trackIsAudioVideoFlags[i] = isAudioVideo;
        haveAudioVideoTracks |= isAudioVideo;
      }
      tracks = new TrackGroupArray(trackArray);
      if (minLoadableRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA
          && length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET) {
        actualMinLoadableRetryCount = ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE;
      }
      prepared = true;
      listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable());
      callback.onPrepared(this);
    }
  
    private void copyLengthFromLoader(ExtractingLoadable loadable) {
      if (length == C.LENGTH_UNSET) {
        length = loadable.length;
      }
    }
  
    private void startLoading() {
      ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder,
          loadCondition);
      if (prepared) {
        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, actualMinLoadableRetryCount);
      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 positionUs The seek position in microseconds.
     * @return Whether the in-buffer seek was successful.
     */
    private boolean seekInsideBufferUs(long positionUs) {
      int trackCount = sampleQueues.length;
      for (int i = 0; i < trackCount; i++) {
        SampleQueue sampleQueue = sampleQueues[i];
        sampleQueue.rewind();
        boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false)
            != SampleQueue.ADVANCE_FAILED;
        // 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 static boolean isLoadableExceptionFatal(IOException e) {
      return e instanceof UnrecognizedInputFormatException;
    }
  
    private final class SampleStreamImpl implements SampleStream {
  
      private final int track;
  
      public SampleStreamImpl(int track) {
        this.track = track;
      }
  
      @Override
      public boolean isReady() {
        return ExtractorMediaPeriod.this.isReady(track);
      }
  
      @Override
      public void maybeThrowError() throws IOException {
        ExtractorMediaPeriod.this.maybeThrowError();
      }
  
      @Override
      public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
          boolean formatRequired) {
        return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);
      }
  
      @Override
      public int skipData(long positionUs) {
        return ExtractorMediaPeriod.this.skipData(track, positionUs);
      }
  
    }
  
    /**
     * Loads the media stream and extracts sample data from it.
     */
    /* package */ final class ExtractingLoadable implements Loadable {
  
      private final Uri uri;
      private final DataSource dataSource;
      private final ExtractorHolder extractorHolder;
      private final ConditionVariable loadCondition;
      private final PositionHolder positionHolder;
  
      private volatile boolean loadCanceled;
  
      private boolean pendingExtractorSeek;
      private long seekTimeUs;
      private DataSpec dataSpec;
      private long length;
      private long bytesLoaded;
  
      public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder,
          ConditionVariable loadCondition) {
        this.uri = Assertions.checkNotNull(uri);
        this.dataSource = Assertions.checkNotNull(dataSource);
        this.extractorHolder = Assertions.checkNotNull(extractorHolder);
        this.loadCondition = loadCondition;
        this.positionHolder = new PositionHolder();
        this.pendingExtractorSeek = true;
        this.length = C.LENGTH_UNSET;
      }
  
      public void setLoadPosition(long position, long timeUs) {
        positionHolder.position = position;
        seekTimeUs = timeUs;
        pendingExtractorSeek = true;
      }
  
      @Override
      public void cancelLoad() {
        loadCanceled = true;
      }
  
      @Override
      public boolean isLoadCanceled() {
        return loadCanceled;
      }
  
      @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 = new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey);
            length = dataSource.open(dataSpec);
            if (length != C.LENGTH_UNSET) {
              length += position;
            }
            input = new DefaultExtractorInput(dataSource, position, length);
            Extractor extractor = extractorHolder.selectExtractor(input, dataSource.getUri());
            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();
              bytesLoaded = positionHolder.position - dataSpec.absoluteStreamPosition;
            }
            Util.closeQuietly(dataSource);
          }
        }
      }
  
    }
  
    /**
     * Stores a list of extractors and a selected extractor when the format has been detected.
     */
    private static final class ExtractorHolder {
  
      private final Extractor[] extractors;
      private final ExtractorOutput extractorOutput;
      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.
       * @param extractorOutput The output that will be used to initialize the selected extractor.
       */
      public ExtractorHolder(Extractor[] extractors, ExtractorOutput extractorOutput) {
        this.extractors = extractors;
        this.extractorOutput = extractorOutput;
      }
  
      /**
       * 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 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, Uri uri)
          throws IOException, InterruptedException {
        if (extractor != null) {
          return extractor;
        }
        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(extractorOutput);
        return extractor;
      }
  
      public void release() {
        if (extractor != null) {
          extractor.release();
          extractor = null;
        }
      }
  
    }
  
  }
  