/**
  * For live playbacks, determines the seek position that snaps playback to be {@code
  * liveEdgeLatencyUs} behind the live edge of the provided manifest.
  *
  * @param manifest The manifest.
  * @param liveEdgeLatencyUs The live edge latency, in microseconds.
  * @return The seek position in microseconds.
  */
 private static long getLiveSeekPosition(
     SmoothStreamingManifest manifest, long liveEdgeLatencyUs) {
   long liveEdgeTimestampUs = Long.MIN_VALUE;
   for (int i = 0; i < manifest.streamElements.length; i++) {
     StreamElement streamElement = manifest.streamElements[i];
     if (streamElement.chunkCount > 0) {
       long elementLiveEdgeTimestampUs =
           streamElement.getStartTimeUs(streamElement.chunkCount - 1)
               + streamElement.getChunkDurationUs(streamElement.chunkCount - 1);
       liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, elementLiveEdgeTimestampUs);
     }
   }
   return liveEdgeTimestampUs - liveEdgeLatencyUs;
 }
  @Override
  public void continueBuffering(long playbackPositionUs) {
    if (manifestFetcher == null || !currentManifest.isLive || fatalError != null) {
      return;
    }

    SmoothStreamingManifest newManifest = manifestFetcher.getManifest();
    if (currentManifest != newManifest && newManifest != null) {
      StreamElement currentElement = currentManifest.streamElements[enabledTrack.elementIndex];
      int currentElementChunkCount = currentElement.chunkCount;
      StreamElement newElement = newManifest.streamElements[enabledTrack.elementIndex];
      if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
        // There's no overlap between the old and new elements because at least one is empty.
        currentManifestChunkOffset += currentElementChunkCount;
      } else {
        long currentElementEndTimeUs =
            currentElement.getStartTimeUs(currentElementChunkCount - 1)
                + currentElement.getChunkDurationUs(currentElementChunkCount - 1);
        long newElementStartTimeUs = newElement.getStartTimeUs(0);
        if (currentElementEndTimeUs <= newElementStartTimeUs) {
          // There's no overlap between the old and new elements.
          currentManifestChunkOffset += currentElementChunkCount;
        } else {
          // The new element overlaps with the old one.
          currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs);
        }
      }
      currentManifest = newManifest;
      needManifestRefresh = false;
    }

    if (needManifestRefresh
        && (SystemClock.elapsedRealtime()
            > manifestFetcher.getManifestLoadStartTimestamp()
                + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) {
      manifestFetcher.requestRefresh();
    }
  }
  @Override
  public final void getChunkOperation(
      List<? extends MediaChunk> queue, long playbackPositionUs, ChunkOperationHolder out) {
    if (fatalError != null) {
      out.chunk = null;
      return;
    }

    evaluation.queueSize = queue.size();
    if (enabledTrack.isAdaptive()) {
      adaptiveFormatEvaluator.evaluate(
          queue, playbackPositionUs, enabledTrack.adaptiveFormats, evaluation);
    } else {
      evaluation.format = enabledTrack.fixedFormat;
      evaluation.trigger = Chunk.TRIGGER_MANUAL;
    }

    Format selectedFormat = evaluation.format;
    out.queueSize = evaluation.queueSize;

    if (selectedFormat == null) {
      out.chunk = null;
      return;
    } else if (out.queueSize == queue.size()
        && out.chunk != null
        && out.chunk.format.equals(selectedFormat)) {
      // We already have a chunk, and the evaluation hasn't changed either the format or the size
      // of the queue. Leave unchanged.
      return;
    }

    // In all cases where we return before instantiating a new chunk, we want out.chunk to be null.
    out.chunk = null;

    StreamElement streamElement = currentManifest.streamElements[enabledTrack.elementIndex];
    if (streamElement.chunkCount == 0) {
      if (currentManifest.isLive) {
        needManifestRefresh = true;
      } else {
        out.endOfStream = true;
      }
      return;
    }

    int chunkIndex;
    if (queue.isEmpty()) {
      if (live) {
        playbackPositionUs = getLiveSeekPosition(currentManifest, liveEdgeLatencyUs);
      }
      chunkIndex = streamElement.getChunkIndex(playbackPositionUs);
    } else {
      MediaChunk previous = queue.get(out.queueSize - 1);
      chunkIndex = previous.chunkIndex + 1 - currentManifestChunkOffset;
    }

    if (live && chunkIndex < 0) {
      // This is before the first chunk in the current manifest.
      fatalError = new BehindLiveWindowException();
      return;
    } else if (currentManifest.isLive) {
      if (chunkIndex >= streamElement.chunkCount) {
        // This is beyond the last chunk in the current manifest.
        needManifestRefresh = true;
        return;
      } else if (chunkIndex == streamElement.chunkCount - 1) {
        // This is the last chunk in the current manifest. Mark the manifest as being finished,
        // but continue to return the final chunk.
        needManifestRefresh = true;
      }
    } else if (chunkIndex >= streamElement.chunkCount) {
      out.endOfStream = true;
      return;
    }

    boolean isLastChunk = !currentManifest.isLive && chunkIndex == streamElement.chunkCount - 1;
    long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex);
    long chunkEndTimeUs =
        isLastChunk ? -1 : chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);
    int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset;

    int manifestTrackIndex = getManifestTrackIndex(streamElement, selectedFormat);
    int manifestTrackKey = getManifestTrackKey(enabledTrack.elementIndex, manifestTrackIndex);
    Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
    Chunk mediaChunk =
        newMediaChunk(
            selectedFormat,
            uri,
            null,
            extractorWrappers.get(manifestTrackKey),
            drmInitData,
            dataSource,
            currentAbsoluteChunkIndex,
            chunkStartTimeUs,
            chunkEndTimeUs,
            evaluation.trigger,
            mediaFormats.get(manifestTrackKey),
            enabledTrack.adaptiveMaxWidth,
            enabledTrack.adaptiveMaxHeight);
    out.chunk = mediaChunk;
  }