@Override
  public final void getChunkOperation(
      List<? extends MediaChunk> queue,
      long seekPositionUs,
      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) {
        seekPositionUs = getLiveSeekPosition(currentManifest, liveEdgeLatencyUs);
      }
      chunkIndex = streamElement.getChunkIndex(seekPositionUs);
    } 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;
  }
Пример #2
0
  /**
   * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should be
   * performed by the calling {@link HlsSampleSource}.
   *
   * @param previousTsChunk The previously loaded chunk that the next chunk should follow.
   * @param playbackPositionUs The current playback position. If previousTsChunk is null then this
   *     parameter is the position from which playback is expected to start (or restart) and hence
   *     should be interpreted as a seek position.
   * @param out The holder to populate with the result. {@link ChunkOperationHolder#queueSize} is
   *     unused.
   */
  public void getChunkOperation(
      TsChunk previousTsChunk,
      long seekPositionUs,
      long playbackPositionUs,
      ChunkOperationHolder out) {
    int nextVariantIndex;
    boolean switchingVariantSpliced;
    if (adaptiveMode == ADAPTIVE_MODE_NONE) {
      nextVariantIndex = selectedVariantIndex;
      switchingVariantSpliced = false;
    } else {
      nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs);
      switchingVariantSpliced =
          previousTsChunk != null
              && !variants[nextVariantIndex].format.equals(previousTsChunk.format)
              && adaptiveMode == ADAPTIVE_MODE_SPLICE;
    }

    HlsMediaPlaylist mediaPlaylist = variantPlaylists[nextVariantIndex];
    if (mediaPlaylist == null) {
      // We don't have the media playlist for the next variant. Request it now.
      out.chunk = newMediaPlaylistChunk(nextVariantIndex);
      return;
    }

    selectedVariantIndex = nextVariantIndex;
    int chunkMediaSequence = 0;
    if (live) {
      if (previousTsChunk == null) {
        if (seekPositionUs == 0) {
          chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex);
        } else {
          chunkMediaSequence =
              Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, true)
                  + mediaPlaylist.mediaSequence;
        }
      } else {
        chunkMediaSequence =
            switchingVariantSpliced ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1;
        if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
          fatalError = new BehindLiveWindowException();
          return;
        }
      }
    } else {
      // Not live.
      if (previousTsChunk == null) {
        chunkMediaSequence =
            Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, true, true)
                + mediaPlaylist.mediaSequence;
      } else {
        chunkMediaSequence =
            switchingVariantSpliced ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1;
      }
    }

    int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
    if (chunkIndex >= mediaPlaylist.segments.size()) {
      if (!mediaPlaylist.live) {
        out.endOfStream = true;
      } else if (shouldRerequestLiveMediaPlaylist(nextVariantIndex)) {
        out.chunk = newMediaPlaylistChunk(nextVariantIndex);
      }
      return;
    }

    HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
    Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);

    // Check if encryption is specified.
    if (segment.isEncrypted) {
      Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
      if (!keyUri.equals(encryptionKeyUri)) {
        // Encryption is specified and the key has changed.
        out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex);
        return;
      }
      if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) {
        setEncryptionData(keyUri, segment.encryptionIV, encryptionKey);
      }
    } else {
      clearEncryptionData();
    }

    // Configure the data source and spec for the chunk.
    DataSpec dataSpec =
        new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null);

    // Compute start and end times, and the sequence number of the next chunk.
    long startTimeUs;
    if (live) {
      if (previousTsChunk == null) {
        startTimeUs = segment.startTimeUs;
      } else if (switchingVariantSpliced) {
        startTimeUs = previousTsChunk.startTimeUs;
      } else {
        startTimeUs = previousTsChunk.endTimeUs;
      }
    } else /* Not live */ {
      startTimeUs = segment.startTimeUs;
    }
    long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND);
    int trigger = Chunk.TRIGGER_UNSPECIFIED;
    Format format = variants[selectedVariantIndex].format;

    // Configure the extractor that will read the chunk.
    HlsExtractorWrapper extractorWrapper;
    String lastPathSegment = chunkUri.getLastPathSegment();
    if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
      // TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner
      // identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3
      // case below.
      Extractor extractor = new AdtsExtractor(startTimeUs);
      extractorWrapper =
          new HlsExtractorWrapper(
              trigger,
              format,
              startTimeUs,
              extractor,
              switchingVariantSpliced,
              MediaFormat.NO_VALUE,
              MediaFormat.NO_VALUE);
    } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
      Extractor extractor = new Mp3Extractor(startTimeUs);
      extractorWrapper =
          new HlsExtractorWrapper(
              trigger,
              format,
              startTimeUs,
              extractor,
              switchingVariantSpliced,
              MediaFormat.NO_VALUE,
              MediaFormat.NO_VALUE);
    } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
        || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
      PtsTimestampAdjuster timestampAdjuster =
          timestampAdjusterProvider.getAdjuster(
              isMaster, segment.discontinuitySequenceNumber, startTimeUs);
      if (timestampAdjuster == null) {
        // The master source has yet to instantiate an adjuster for the discontinuity sequence.
        // TODO: There's probably an edge case if the master starts playback at a chunk belonging to
        // a discontinuity sequence greater than the one that this source is trying to start at.
        return;
      }
      Extractor extractor = new WebvttExtractor(timestampAdjuster);
      extractorWrapper =
          new HlsExtractorWrapper(
              trigger,
              format,
              startTimeUs,
              extractor,
              switchingVariantSpliced,
              MediaFormat.NO_VALUE,
              MediaFormat.NO_VALUE);
    } else if (previousTsChunk == null
        || previousTsChunk.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
        || !format.equals(previousTsChunk.format)) {
      // MPEG-2 TS segments, but we need a new extractor.
      PtsTimestampAdjuster timestampAdjuster =
          timestampAdjusterProvider.getAdjuster(
              isMaster, segment.discontinuitySequenceNumber, startTimeUs);
      if (timestampAdjuster == null) {
        // The master source has yet to instantiate an adjuster for the discontinuity sequence.
        return;
      }
      int workaroundFlags = 0;
      String codecs = format.codecs;
      if (!TextUtils.isEmpty(codecs)) {
        // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
        // exist. If we know from the codec attribute that they don't exist, then we can explicitly
        // ignore them even if they're declared.
        if (MimeTypes.getAudioMediaMimeType(codecs) != MimeTypes.AUDIO_AAC) {
          workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_AAC_STREAM;
        }
        if (MimeTypes.getVideoMediaMimeType(codecs) != MimeTypes.VIDEO_H264) {
          workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM;
        }
      }
      Extractor extractor = new TsExtractor(timestampAdjuster, workaroundFlags);
      ExposedTrack selectedTrack = tracks.get(selectedTrackIndex);
      extractorWrapper =
          new HlsExtractorWrapper(
              trigger,
              format,
              startTimeUs,
              extractor,
              switchingVariantSpliced,
              selectedTrack.adaptiveMaxWidth,
              selectedTrack.adaptiveMaxHeight);
    } else {
      // MPEG-2 TS segments, and we need to continue using the same extractor.
      extractorWrapper = previousTsChunk.extractorWrapper;
    }

    out.chunk =
        new TsChunk(
            dataSource,
            dataSpec,
            trigger,
            format,
            startTimeUs,
            endTimeUs,
            chunkMediaSequence,
            segment.discontinuitySequenceNumber,
            extractorWrapper,
            encryptionKey,
            encryptionIv);
  }