public final class

MediaParserHlsMediaChunkExtractor

extends java.lang.Object

implements HlsMediaChunkExtractor

 java.lang.Object

↳androidx.media3.exoplayer.hls.MediaParserHlsMediaChunkExtractor

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer-hls', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer-hls
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-exoplayer-hls:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Overview

HlsMediaChunkExtractor implemented on top of the platform's MediaParser.

Summary

Fields
public static final HlsExtractorFactoryFACTORY

HlsExtractorFactory implementation that produces MediaParserHlsMediaChunkExtractor for all container formats except WebVTT, for which a BundledHlsMediaChunkExtractor is returned.

Constructors
publicMediaParserHlsMediaChunkExtractor(MediaParser mediaParser, OutputConsumerAdapterV30 outputConsumerAdapter, Format format, boolean overrideInBandCaptionDeclarations, <any> muxedCaptionMediaFormats, int leadingBytesToSkip, PlayerId playerId)

Creates a new instance.

Methods
public voidinit(ExtractorOutput extractorOutput)

public booleanisPackedAudioExtractor()

public booleanisReusable()

public voidonTruncatedSegmentParsed()

public booleanread(ExtractorInput extractorInput)

public HlsMediaChunkExtractorrecreate()

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final HlsExtractorFactory FACTORY

HlsExtractorFactory implementation that produces MediaParserHlsMediaChunkExtractor for all container formats except WebVTT, for which a BundledHlsMediaChunkExtractor is returned.

Constructors

public MediaParserHlsMediaChunkExtractor(MediaParser mediaParser, OutputConsumerAdapterV30 outputConsumerAdapter, Format format, boolean overrideInBandCaptionDeclarations, <any> muxedCaptionMediaFormats, int leadingBytesToSkip, PlayerId playerId)

Creates a new instance.

Parameters:

mediaParser: The MediaParser instance to use for extraction of segments. The provided instance must have completed sniffing, or must have been created by name.
outputConsumerAdapter: The OutputConsumerAdapterV30 with which mediaParser was created.
format: The Format associated with the segment.
overrideInBandCaptionDeclarations: Whether to ignore any in-band caption track declarations in favor of using the muxedCaptionMediaFormats instead. If false, caption declarations found in the extracted media will be used, causing muxedCaptionMediaFormats to be ignored instead.
muxedCaptionMediaFormats: The list of in-band caption that MediaParser should expose.
leadingBytesToSkip: The number of bytes to skip from the start of the input before starting extraction.
playerId: The PlayerId of the player using this chunk extractor.

Methods

public void init(ExtractorOutput extractorOutput)

public boolean read(ExtractorInput extractorInput)

public boolean isPackedAudioExtractor()

public boolean isReusable()

public HlsMediaChunkExtractor recreate()

public void onTruncatedSegmentParsed()

Source

/*
 * Copyright 2020 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 androidx.media3.exoplayer.hls;

import static android.media.MediaParser.PARAMETER_TS_IGNORE_AAC_STREAM;
import static android.media.MediaParser.PARAMETER_TS_IGNORE_AVC_STREAM;
import static android.media.MediaParser.PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM;
import static android.media.MediaParser.PARAMETER_TS_MODE;
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE;
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_EXPOSE_CAPTION_FORMATS;
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_IGNORE_TIMESTAMP_OFFSET;
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_IN_BAND_CRYPTO_INFO;
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS;

import android.annotation.SuppressLint;
import android.media.MediaFormat;
import android.media.MediaParser;
import android.media.MediaParser.OutputConsumer;
import android.media.MediaParser.SeekPoint;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.FileTypes;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.mediaparser.InputReaderAdapterV30;
import androidx.media3.exoplayer.source.mediaparser.MediaParserUtil;
import androidx.media3.exoplayer.source.mediaparser.OutputConsumerAdapterV30;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.ExtractorOutput;
import com.google.common.collect.ImmutableList;
import java.io.IOException;

/** {@link HlsMediaChunkExtractor} implemented on top of the platform's {@link MediaParser}. */
@RequiresApi(30)
@UnstableApi
public final class MediaParserHlsMediaChunkExtractor implements HlsMediaChunkExtractor {

  /**
   * {@link HlsExtractorFactory} implementation that produces {@link
   * MediaParserHlsMediaChunkExtractor} for all container formats except WebVTT, for which a {@link
   * BundledHlsMediaChunkExtractor} is returned.
   */
  public static final HlsExtractorFactory FACTORY =
      (uri,
          format,
          muxedCaptionFormats,
          timestampAdjuster,
          responseHeaders,
          sniffingExtractorInput,
          playerId) -> {
        if (FileTypes.inferFileTypeFromMimeType(format.sampleMimeType) == FileTypes.WEBVTT) {
          // The segment contains WebVTT. MediaParser does not support WebVTT parsing, so we use the
          // bundled extractor.
          return new BundledHlsMediaChunkExtractor(
              new WebvttExtractor(format.language, timestampAdjuster), format, timestampAdjuster);
        }

        boolean overrideInBandCaptionDeclarations = muxedCaptionFormats != null;
        ImmutableList.Builder<MediaFormat> muxedCaptionMediaFormatsBuilder =
            ImmutableList.builder();
        if (muxedCaptionFormats != null) {
          // The manifest contains captions declarations. We use those to determine which captions
          // will be exposed by MediaParser.
          for (int i = 0; i < muxedCaptionFormats.size(); i++) {
            muxedCaptionMediaFormatsBuilder.add(
                MediaParserUtil.toCaptionsMediaFormat(muxedCaptionFormats.get(i)));
          }
        } else {
          // The manifest does not declare any captions in the stream. Imitate the default HLS
          // extractor factory and declare a 608 track by default.
          muxedCaptionMediaFormatsBuilder.add(
              MediaParserUtil.toCaptionsMediaFormat(
                  new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA608).build()));
        }

        ImmutableList<MediaFormat> muxedCaptionMediaFormats =
            muxedCaptionMediaFormatsBuilder.build();

        // TODO: Factor out code for optimizing the sniffing order across both factories.
        OutputConsumerAdapterV30 outputConsumerAdapter = new OutputConsumerAdapterV30();
        outputConsumerAdapter.setMuxedCaptionFormats(
            muxedCaptionFormats != null ? muxedCaptionFormats : ImmutableList.of());
        outputConsumerAdapter.setTimestampAdjuster(timestampAdjuster);
        MediaParser mediaParser =
            createMediaParserInstance(
                outputConsumerAdapter,
                format,
                overrideInBandCaptionDeclarations,
                muxedCaptionMediaFormats,
                playerId,
                MediaParser.PARSER_NAME_FMP4,
                MediaParser.PARSER_NAME_AC3,
                MediaParser.PARSER_NAME_AC4,
                MediaParser.PARSER_NAME_ADTS,
                MediaParser.PARSER_NAME_MP3,
                MediaParser.PARSER_NAME_TS);

        PeekingInputReader peekingInputReader = new PeekingInputReader(sniffingExtractorInput);
        // The chunk extractor constructor requires an instance with a known parser name, so we
        // advance once for MediaParser to sniff the content.
        mediaParser.advance(peekingInputReader);
        outputConsumerAdapter.setSelectedParserName(mediaParser.getParserName());

        return new MediaParserHlsMediaChunkExtractor(
            mediaParser,
            outputConsumerAdapter,
            format,
            overrideInBandCaptionDeclarations,
            muxedCaptionMediaFormats,
            /* leadingBytesToSkip= */ peekingInputReader.totalPeekedBytes,
            playerId);
      };

  private final OutputConsumerAdapterV30 outputConsumerAdapter;
  private final InputReaderAdapterV30 inputReaderAdapter;
  private final MediaParser mediaParser;
  private final Format format;
  private final boolean overrideInBandCaptionDeclarations;
  private final ImmutableList<MediaFormat> muxedCaptionMediaFormats;
  private final PlayerId playerId;

  private int pendingSkipBytes;

  /**
   * Creates a new instance.
   *
   * @param mediaParser The {@link MediaParser} instance to use for extraction of segments. The
   *     provided instance must have completed sniffing, or must have been created by name.
   * @param outputConsumerAdapter The {@link OutputConsumerAdapterV30} with which {@code
   *     mediaParser} was created.
   * @param format The {@link Format} associated with the segment.
   * @param overrideInBandCaptionDeclarations Whether to ignore any in-band caption track
   *     declarations in favor of using the {@code muxedCaptionMediaFormats} instead. If false,
   *     caption declarations found in the extracted media will be used, causing {@code
   *     muxedCaptionMediaFormats} to be ignored instead.
   * @param muxedCaptionMediaFormats The list of in-band caption {@link MediaFormat MediaFormats}
   *     that {@link MediaParser} should expose.
   * @param leadingBytesToSkip The number of bytes to skip from the start of the input before
   *     starting extraction.
   * @param playerId The {@link PlayerId} of the player using this chunk extractor.
   */
  public MediaParserHlsMediaChunkExtractor(
      MediaParser mediaParser,
      OutputConsumerAdapterV30 outputConsumerAdapter,
      Format format,
      boolean overrideInBandCaptionDeclarations,
      ImmutableList<MediaFormat> muxedCaptionMediaFormats,
      int leadingBytesToSkip,
      PlayerId playerId) {
    this.mediaParser = mediaParser;
    this.outputConsumerAdapter = outputConsumerAdapter;
    this.overrideInBandCaptionDeclarations = overrideInBandCaptionDeclarations;
    this.muxedCaptionMediaFormats = muxedCaptionMediaFormats;
    this.format = format;
    this.playerId = playerId;
    pendingSkipBytes = leadingBytesToSkip;
    inputReaderAdapter = new InputReaderAdapterV30();
  }

  // ChunkExtractor implementation.

  @Override
  public void init(ExtractorOutput extractorOutput) {
    outputConsumerAdapter.setExtractorOutput(extractorOutput);
  }

  @Override
  public boolean read(ExtractorInput extractorInput) throws IOException {
    extractorInput.skipFully(pendingSkipBytes);
    pendingSkipBytes = 0;
    inputReaderAdapter.setDataReader(extractorInput, extractorInput.getLength());
    return mediaParser.advance(inputReaderAdapter);
  }

  @Override
  public boolean isPackedAudioExtractor() {
    String parserName = mediaParser.getParserName();
    return MediaParser.PARSER_NAME_AC3.equals(parserName)
        || MediaParser.PARSER_NAME_AC4.equals(parserName)
        || MediaParser.PARSER_NAME_ADTS.equals(parserName)
        || MediaParser.PARSER_NAME_MP3.equals(parserName);
  }

  @Override
  public boolean isReusable() {
    String parserName = mediaParser.getParserName();
    return MediaParser.PARSER_NAME_FMP4.equals(parserName)
        || MediaParser.PARSER_NAME_TS.equals(parserName);
  }

  @Override
  public HlsMediaChunkExtractor recreate() {
    Assertions.checkState(!isReusable());
    return new MediaParserHlsMediaChunkExtractor(
        createMediaParserInstance(
            outputConsumerAdapter,
            format,
            overrideInBandCaptionDeclarations,
            muxedCaptionMediaFormats,
            playerId,
            mediaParser.getParserName()),
        outputConsumerAdapter,
        format,
        overrideInBandCaptionDeclarations,
        muxedCaptionMediaFormats,
        /* leadingBytesToSkip= */ 0,
        playerId);
  }

  @Override
  public void onTruncatedSegmentParsed() {
    mediaParser.seek(SeekPoint.START);
  }

  // Allow constants that are not part of the public MediaParser API.
  @SuppressLint({"WrongConstant"})
  private static MediaParser createMediaParserInstance(
      OutputConsumer outputConsumer,
      Format format,
      boolean overrideInBandCaptionDeclarations,
      ImmutableList<MediaFormat> muxedCaptionMediaFormats,
      PlayerId playerId,
      String... parserNames) {
    MediaParser mediaParser =
        parserNames.length == 1
            ? MediaParser.createByName(parserNames[0], outputConsumer)
            : MediaParser.create(outputConsumer, parserNames);
    mediaParser.setParameter(PARAMETER_EXPOSE_CAPTION_FORMATS, muxedCaptionMediaFormats);
    mediaParser.setParameter(
        PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS, overrideInBandCaptionDeclarations);
    mediaParser.setParameter(PARAMETER_IN_BAND_CRYPTO_INFO, true);
    mediaParser.setParameter(PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE, true);
    mediaParser.setParameter(PARAMETER_IGNORE_TIMESTAMP_OFFSET, true);
    mediaParser.setParameter(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM, true);
    mediaParser.setParameter(PARAMETER_TS_MODE, "hls");
    @Nullable 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.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
        mediaParser.setParameter(PARAMETER_TS_IGNORE_AAC_STREAM, true);
      }
      if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
        mediaParser.setParameter(PARAMETER_TS_IGNORE_AVC_STREAM, true);
      }
    }
    if (Util.SDK_INT >= 31) {
      MediaParserUtil.setLogSessionIdOnMediaParser(mediaParser, playerId);
    }
    return mediaParser;
  }

  private static final class PeekingInputReader implements MediaParser.SeekableInputReader {

    private final ExtractorInput extractorInput;
    private int totalPeekedBytes;

    private PeekingInputReader(ExtractorInput extractorInput) {
      this.extractorInput = extractorInput;
    }

    @Override
    public int read(byte[] buffer, int offset, int readLength) throws IOException {
      int peekedBytes = extractorInput.peek(buffer, offset, readLength);
      totalPeekedBytes += peekedBytes;
      return peekedBytes;
    }

    @Override
    public long getPosition() {
      return extractorInput.getPeekPosition();
    }

    @Override
    public long getLength() {
      return extractorInput.getLength();
    }

    @Override
    public void seekToPosition(long position) {
      // Seeking is not allowed when sniffing the content.
      throw new UnsupportedOperationException();
    }
  }
}