FragmentedMp4Extractor.java

/*
 * 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 androidx.media3.extractor.mp4;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.nullSafeArrayCopy;
import static androidx.media3.extractor.mp4.AtomParsers.parseTraks;
import static java.lang.Math.max;

import android.util.Pair;
import android.util.SparseArray;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.DrmInitData.SchemeData;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.TimestampAdjuster;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.extractor.Ac4Util;
import androidx.media3.extractor.CeaUtil;
import androidx.media3.extractor.ChunkIndex;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.ExtractorsFactory;
import androidx.media3.extractor.GaplessInfoHolder;
import androidx.media3.extractor.NalUnitUtil;
import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.TrackOutput;
import androidx.media3.extractor.metadata.emsg.EventMessage;
import androidx.media3.extractor.metadata.emsg.EventMessageEncoder;
import androidx.media3.extractor.mp4.Atom.ContainerAtom;
import androidx.media3.extractor.mp4.Atom.LeafAtom;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

/** Extracts data from the FMP4 container format. */
@SuppressWarnings("ConstantField")
@UnstableApi
public class FragmentedMp4Extractor implements Extractor {

  /** Factory for {@link FragmentedMp4Extractor} instances. */
  public static final ExtractorsFactory FACTORY =
      () -> new Extractor[] {new FragmentedMp4Extractor()};

  /**
   * Flags controlling the behavior of the extractor. Possible flag values are {@link
   * #FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME}, {@link #FLAG_WORKAROUND_IGNORE_TFDT_BOX},
   * {@link #FLAG_ENABLE_EMSG_TRACK} and {@link #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}.
   */
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @IntDef(
      flag = true,
      value = {
        FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
        FLAG_WORKAROUND_IGNORE_TFDT_BOX,
        FLAG_ENABLE_EMSG_TRACK,
        FLAG_WORKAROUND_IGNORE_EDIT_LISTS
      })
  public @interface Flags {}
  /**
   * Flag to work around an issue in some video streams where every frame is marked as a sync frame.
   * The workaround overrides the sync frame flags in the stream, forcing them to false except for
   * the first sample in each segment.
   *
   * <p>This flag does nothing if the stream is not a video stream.
   */
  public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
  /** Flag to ignore any tfdt boxes in the stream. */
  public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 1 << 1; // 2
  /**
   * Flag to indicate that the extractor should output an event message metadata track. Any event
   * messages in the stream will be delivered as samples to this track.
   */
  public static final int FLAG_ENABLE_EMSG_TRACK = 1 << 2; // 4

  /** Flag to ignore any edit lists in the stream. */
  public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1 << 4; // 16

  private static final String TAG = "FragmentedMp4Extractor";

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int SAMPLE_GROUP_TYPE_seig = 0x73656967;

  private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
      new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};

  // Extra tracks constants.
  private static final Format EMSG_FORMAT =
      new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_EMSG).build();
  private static final int EXTRA_TRACKS_BASE_ID = 100;

  // Parser states.
  private static final int STATE_READING_ATOM_HEADER = 0;
  private static final int STATE_READING_ATOM_PAYLOAD = 1;
  private static final int STATE_READING_ENCRYPTION_DATA = 2;
  private static final int STATE_READING_SAMPLE_START = 3;
  private static final int STATE_READING_SAMPLE_CONTINUE = 4;

  // Workarounds.
  @Flags private final int flags;
  @Nullable private final Track sideloadedTrack;

  // Sideloaded data.
  private final List<Format> closedCaptionFormats;

  // Track-linked data bundle, accessible as a whole through trackID.
  private final SparseArray<TrackBundle> trackBundles;

  // Temporary arrays.
  private final ParsableByteArray nalStartCode;
  private final ParsableByteArray nalPrefix;
  private final ParsableByteArray nalBuffer;
  private final byte[] scratchBytes;
  private final ParsableByteArray scratch;

  // Adjusts sample timestamps.
  @Nullable private final TimestampAdjuster timestampAdjuster;

  private final EventMessageEncoder eventMessageEncoder;

  // Parser state.
  private final ParsableByteArray atomHeader;
  private final ArrayDeque<ContainerAtom> containerAtoms;
  private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos;
  @Nullable private final TrackOutput additionalEmsgTrackOutput;

  private int parserState;
  private int atomType;
  private long atomSize;
  private int atomHeaderBytesRead;
  @Nullable private ParsableByteArray atomData;
  private long endOfMdatPosition;
  private int pendingMetadataSampleBytes;
  private long pendingSeekTimeUs;

  private long durationUs;
  private long segmentIndexEarliestPresentationTimeUs;
  @Nullable private TrackBundle currentTrackBundle;
  private int sampleSize;
  private int sampleBytesWritten;
  private int sampleCurrentNalBytesRemaining;
  private boolean processSeiNalUnitPayload;

  // Outputs.
  private ExtractorOutput extractorOutput;
  private TrackOutput[] emsgTrackOutputs;
  private TrackOutput[] ceaTrackOutputs;

  // Whether extractorOutput.seekMap has been called.
  private boolean haveOutputSeekMap;

  public FragmentedMp4Extractor() {
    this(0);
  }

  /** @param flags Flags that control the extractor's behavior. */
  public FragmentedMp4Extractor(@Flags int flags) {
    this(flags, /* timestampAdjuster= */ null);
  }

  /**
   * @param flags Flags that control the extractor's behavior.
   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
   */
  public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) {
    this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList());
  }

  /**
   * @param flags Flags that control the extractor's behavior.
   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
   * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not
   *     receive a moov box in the input data. Null if a moov box is expected.
   */
  public FragmentedMp4Extractor(
      @Flags int flags,
      @Nullable TimestampAdjuster timestampAdjuster,
      @Nullable Track sideloadedTrack) {
    this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList());
  }

  /**
   * @param flags Flags that control the extractor's behavior.
   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
   * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not
   *     receive a moov box in the input data. Null if a moov box is expected.
   * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed
   *     caption channels to expose.
   */
  public FragmentedMp4Extractor(
      @Flags int flags,
      @Nullable TimestampAdjuster timestampAdjuster,
      @Nullable Track sideloadedTrack,
      List<Format> closedCaptionFormats) {
    this(
        flags,
        timestampAdjuster,
        sideloadedTrack,
        closedCaptionFormats,
        /* additionalEmsgTrackOutput= */ null);
  }

  /**
   * @param flags Flags that control the extractor's behavior.
   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
   * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not
   *     receive a moov box in the input data. Null if a moov box is expected.
   * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed
   *     caption channels to expose.
   * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages
   *     targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special
   *     handling of emsg messages for players is not required.
   */
  public FragmentedMp4Extractor(
      @Flags int flags,
      @Nullable TimestampAdjuster timestampAdjuster,
      @Nullable Track sideloadedTrack,
      List<Format> closedCaptionFormats,
      @Nullable TrackOutput additionalEmsgTrackOutput) {
    this.flags = flags;
    this.timestampAdjuster = timestampAdjuster;
    this.sideloadedTrack = sideloadedTrack;
    this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats);
    this.additionalEmsgTrackOutput = additionalEmsgTrackOutput;
    eventMessageEncoder = new EventMessageEncoder();
    atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
    nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
    nalPrefix = new ParsableByteArray(5);
    nalBuffer = new ParsableByteArray();
    scratchBytes = new byte[16];
    scratch = new ParsableByteArray(scratchBytes);
    containerAtoms = new ArrayDeque<>();
    pendingMetadataSampleInfos = new ArrayDeque<>();
    trackBundles = new SparseArray<>();
    durationUs = C.TIME_UNSET;
    pendingSeekTimeUs = C.TIME_UNSET;
    segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
    extractorOutput = ExtractorOutput.PLACEHOLDER;
    emsgTrackOutputs = new TrackOutput[0];
    ceaTrackOutputs = new TrackOutput[0];
  }

  @Override
  public boolean sniff(ExtractorInput input) throws IOException {
    return Sniffer.sniffFragmented(input);
  }

  @Override
  public void init(ExtractorOutput output) {
    extractorOutput = output;
    enterReadingAtomHeaderState();
    initExtraTracks();
    if (sideloadedTrack != null) {
      TrackBundle bundle =
          new TrackBundle(
              output.track(0, sideloadedTrack.type),
              new TrackSampleTable(
                  sideloadedTrack,
                  /* offsets= */ new long[0],
                  /* sizes= */ new int[0],
                  /* maximumSize= */ 0,
                  /* timestampsUs= */ new long[0],
                  /* flags= */ new int[0],
                  /* durationUs= */ 0),
              new DefaultSampleValues(
                  /* sampleDescriptionIndex= */ 0,
                  /* duration= */ 0,
                  /* size= */ 0,
                  /* flags= */ 0));
      trackBundles.put(0, bundle);
      extractorOutput.endTracks();
    }
  }

  @Override
  public void seek(long position, long timeUs) {
    int trackCount = trackBundles.size();
    for (int i = 0; i < trackCount; i++) {
      trackBundles.valueAt(i).resetFragmentInfo();
    }
    pendingMetadataSampleInfos.clear();
    pendingMetadataSampleBytes = 0;
    pendingSeekTimeUs = timeUs;
    containerAtoms.clear();
    enterReadingAtomHeaderState();
  }

  @Override
  public void release() {
    // Do nothing
  }

  @Override
  public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
    while (true) {
      switch (parserState) {
        case STATE_READING_ATOM_HEADER:
          if (!readAtomHeader(input)) {
            return Extractor.RESULT_END_OF_INPUT;
          }
          break;
        case STATE_READING_ATOM_PAYLOAD:
          readAtomPayload(input);
          break;
        case STATE_READING_ENCRYPTION_DATA:
          readEncryptionData(input);
          break;
        default:
          if (readSample(input)) {
            return RESULT_CONTINUE;
          }
      }
    }
  }

  private void enterReadingAtomHeaderState() {
    parserState = STATE_READING_ATOM_HEADER;
    atomHeaderBytesRead = 0;
  }

  private boolean readAtomHeader(ExtractorInput input) throws IOException {
    if (atomHeaderBytesRead == 0) {
      // Read the standard length atom header.
      if (!input.readFully(atomHeader.getData(), 0, Atom.HEADER_SIZE, true)) {
        return false;
      }
      atomHeaderBytesRead = Atom.HEADER_SIZE;
      atomHeader.setPosition(0);
      atomSize = atomHeader.readUnsignedInt();
      atomType = atomHeader.readInt();
    }

    if (atomSize == Atom.DEFINES_LARGE_SIZE) {
      // Read the large size.
      int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;
      input.readFully(atomHeader.getData(), Atom.HEADER_SIZE, headerBytesRemaining);
      atomHeaderBytesRead += headerBytesRemaining;
      atomSize = atomHeader.readUnsignedLongToLong();
    } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
      // The atom extends to the end of the file. Note that if the atom is within a container we can
      // work out its size even if the input length is unknown.
      long endPosition = input.getLength();
      if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) {
        endPosition = containerAtoms.peek().endPosition;
      }
      if (endPosition != C.LENGTH_UNSET) {
        atomSize = endPosition - input.getPosition() + atomHeaderBytesRead;
      }
    }

    if (atomSize < atomHeaderBytesRead) {
      throw ParserException.createForUnsupportedContainerFeature(
          "Atom size less than header length (unsupported).");
    }

    long atomPosition = input.getPosition() - atomHeaderBytesRead;
    if (atomType == Atom.TYPE_moof || atomType == Atom.TYPE_mdat) {
      if (!haveOutputSeekMap) {
        // This must be the first moof or mdat in the stream.
        extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition));
        haveOutputSeekMap = true;
      }
    }

    if (atomType == Atom.TYPE_moof) {
      // The data positions may be updated when parsing the tfhd/trun.
      int trackCount = trackBundles.size();
      for (int i = 0; i < trackCount; i++) {
        TrackFragment fragment = trackBundles.valueAt(i).fragment;
        fragment.atomPosition = atomPosition;
        fragment.auxiliaryDataPosition = atomPosition;
        fragment.dataPosition = atomPosition;
      }
    }

    if (atomType == Atom.TYPE_mdat) {
      currentTrackBundle = null;
      endOfMdatPosition = atomPosition + atomSize;
      parserState = STATE_READING_ENCRYPTION_DATA;
      return true;
    }

    if (shouldParseContainerAtom(atomType)) {
      long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;
      containerAtoms.push(new ContainerAtom(atomType, endPosition));
      if (atomSize == atomHeaderBytesRead) {
        processAtomEnded(endPosition);
      } else {
        // Start reading the first child atom.
        enterReadingAtomHeaderState();
      }
    } else if (shouldParseLeafAtom(atomType)) {
      if (atomHeaderBytesRead != Atom.HEADER_SIZE) {
        throw ParserException.createForUnsupportedContainerFeature(
            "Leaf atom defines extended atom size (unsupported).");
      }
      if (atomSize > Integer.MAX_VALUE) {
        throw ParserException.createForUnsupportedContainerFeature(
            "Leaf atom with length > 2147483647 (unsupported).");
      }
      ParsableByteArray atomData = new ParsableByteArray((int) atomSize);
      System.arraycopy(atomHeader.getData(), 0, atomData.getData(), 0, Atom.HEADER_SIZE);
      this.atomData = atomData;
      parserState = STATE_READING_ATOM_PAYLOAD;
    } else {
      if (atomSize > Integer.MAX_VALUE) {
        throw ParserException.createForUnsupportedContainerFeature(
            "Skipping atom with length > 2147483647 (unsupported).");
      }
      atomData = null;
      parserState = STATE_READING_ATOM_PAYLOAD;
    }

    return true;
  }

  private void readAtomPayload(ExtractorInput input) throws IOException {
    int atomPayloadSize = (int) atomSize - atomHeaderBytesRead;
    @Nullable ParsableByteArray atomData = this.atomData;
    if (atomData != null) {
      input.readFully(atomData.getData(), Atom.HEADER_SIZE, atomPayloadSize);
      onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());
    } else {
      input.skipFully(atomPayloadSize);
    }
    processAtomEnded(input.getPosition());
  }

  private void processAtomEnded(long atomEndPosition) throws ParserException {
    while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) {
      onContainerAtomRead(containerAtoms.pop());
    }
    enterReadingAtomHeaderState();
  }

  private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserException {
    if (!containerAtoms.isEmpty()) {
      containerAtoms.peek().add(leaf);
    } else if (leaf.type == Atom.TYPE_sidx) {
      Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition);
      segmentIndexEarliestPresentationTimeUs = result.first;
      extractorOutput.seekMap(result.second);
      haveOutputSeekMap = true;
    } else if (leaf.type == Atom.TYPE_emsg) {
      onEmsgLeafAtomRead(leaf.data);
    }
  }

  private void onContainerAtomRead(ContainerAtom container) throws ParserException {
    if (container.type == Atom.TYPE_moov) {
      onMoovContainerAtomRead(container);
    } else if (container.type == Atom.TYPE_moof) {
      onMoofContainerAtomRead(container);
    } else if (!containerAtoms.isEmpty()) {
      containerAtoms.peek().add(container);
    }
  }

  private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException {
    checkState(sideloadedTrack == null, "Unexpected moov box.");

    @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren);

    // Read declaration of track fragments in the moov box.
    ContainerAtom mvex = checkNotNull(moov.getContainerAtomOfType(Atom.TYPE_mvex));
    SparseArray<DefaultSampleValues> defaultSampleValuesArray = new SparseArray<>();
    long duration = C.TIME_UNSET;
    int mvexChildrenSize = mvex.leafChildren.size();
    for (int i = 0; i < mvexChildrenSize; i++) {
      Atom.LeafAtom atom = mvex.leafChildren.get(i);
      if (atom.type == Atom.TYPE_trex) {
        Pair<Integer, DefaultSampleValues> trexData = parseTrex(atom.data);
        defaultSampleValuesArray.put(trexData.first, trexData.second);
      } else if (atom.type == Atom.TYPE_mehd) {
        duration = parseMehd(atom.data);
      }
    }

    // Construction of tracks and sample tables.
    List<TrackSampleTable> sampleTables =
        parseTraks(
            moov,
            new GaplessInfoHolder(),
            duration,
            drmInitData,
            /* ignoreEditLists= */ (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0,
            /* isQuickTime= */ false,
            this::modifyTrack);

    int trackCount = sampleTables.size();
    if (trackBundles.size() == 0) {
      // We need to create the track bundles.
      for (int i = 0; i < trackCount; i++) {
        TrackSampleTable sampleTable = sampleTables.get(i);
        Track track = sampleTable.track;
        TrackBundle trackBundle =
            new TrackBundle(
                extractorOutput.track(i, track.type),
                sampleTable,
                getDefaultSampleValues(defaultSampleValuesArray, track.id));
        trackBundles.put(track.id, trackBundle);
        durationUs = max(durationUs, track.durationUs);
      }
      extractorOutput.endTracks();
    } else {
      checkState(trackBundles.size() == trackCount);
      for (int i = 0; i < trackCount; i++) {
        TrackSampleTable sampleTable = sampleTables.get(i);
        Track track = sampleTable.track;
        trackBundles
            .get(track.id)
            .reset(sampleTable, getDefaultSampleValues(defaultSampleValuesArray, track.id));
      }
    }
  }

  @Nullable
  protected Track modifyTrack(@Nullable Track track) {
    return track;
  }

  private DefaultSampleValues getDefaultSampleValues(
      SparseArray<DefaultSampleValues> defaultSampleValuesArray, int trackId) {
    if (defaultSampleValuesArray.size() == 1) {
      // Ignore track id if there is only one track to cope with non-matching track indices.
      // See https://github.com/google/ExoPlayer/issues/4477.
      return defaultSampleValuesArray.valueAt(/* index= */ 0);
    }
    return checkNotNull(defaultSampleValuesArray.get(trackId));
  }

  private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
    parseMoof(moof, trackBundles, sideloadedTrack != null, flags, scratchBytes);

    @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren);
    if (drmInitData != null) {
      int trackCount = trackBundles.size();
      for (int i = 0; i < trackCount; i++) {
        trackBundles.valueAt(i).updateDrmInitData(drmInitData);
      }
    }
    // If we have a pending seek, advance tracks to their preceding sync frames.
    if (pendingSeekTimeUs != C.TIME_UNSET) {
      int trackCount = trackBundles.size();
      for (int i = 0; i < trackCount; i++) {
        trackBundles.valueAt(i).seek(pendingSeekTimeUs);
      }
      pendingSeekTimeUs = C.TIME_UNSET;
    }
  }

  private void initExtraTracks() {
    int nextExtraTrackId = EXTRA_TRACKS_BASE_ID;

    emsgTrackOutputs = new TrackOutput[2];
    int emsgTrackOutputCount = 0;
    if (additionalEmsgTrackOutput != null) {
      emsgTrackOutputs[emsgTrackOutputCount++] = additionalEmsgTrackOutput;
    }
    if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0) {
      emsgTrackOutputs[emsgTrackOutputCount++] =
          extractorOutput.track(nextExtraTrackId++, C.TRACK_TYPE_METADATA);
    }
    emsgTrackOutputs = nullSafeArrayCopy(emsgTrackOutputs, emsgTrackOutputCount);
    for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) {
      eventMessageTrackOutput.format(EMSG_FORMAT);
    }

    ceaTrackOutputs = new TrackOutput[closedCaptionFormats.size()];
    for (int i = 0; i < ceaTrackOutputs.length; i++) {
      TrackOutput output = extractorOutput.track(nextExtraTrackId++, C.TRACK_TYPE_TEXT);
      output.format(closedCaptionFormats.get(i));
      ceaTrackOutputs[i] = output;
    }
  }

  /** Handles an emsg atom (defined in 23009-1). */
  private void onEmsgLeafAtomRead(ParsableByteArray atom) {
    if (emsgTrackOutputs.length == 0) {
      return;
    }
    atom.setPosition(Atom.HEADER_SIZE);
    int fullAtom = atom.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);
    String schemeIdUri;
    String value;
    long timescale;
    long presentationTimeDeltaUs = C.TIME_UNSET; // Only set if version == 0
    long sampleTimeUs = C.TIME_UNSET;
    long durationMs;
    long id;
    switch (version) {
      case 0:
        schemeIdUri = checkNotNull(atom.readNullTerminatedString());
        value = checkNotNull(atom.readNullTerminatedString());
        timescale = atom.readUnsignedInt();
        presentationTimeDeltaUs =
            Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);
        if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) {
          sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs;
        }
        durationMs =
            Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale);
        id = atom.readUnsignedInt();
        break;
      case 1:
        timescale = atom.readUnsignedInt();
        sampleTimeUs =
            Util.scaleLargeTimestamp(atom.readUnsignedLongToLong(), C.MICROS_PER_SECOND, timescale);
        durationMs =
            Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale);
        id = atom.readUnsignedInt();
        schemeIdUri = checkNotNull(atom.readNullTerminatedString());
        value = checkNotNull(atom.readNullTerminatedString());
        break;
      default:
        Log.w(TAG, "Skipping unsupported emsg version: " + version);
        return;
    }

    byte[] messageData = new byte[atom.bytesLeft()];
    atom.readBytes(messageData, /*offset=*/ 0, atom.bytesLeft());
    EventMessage eventMessage = new EventMessage(schemeIdUri, value, durationMs, id, messageData);
    ParsableByteArray encodedEventMessage =
        new ParsableByteArray(eventMessageEncoder.encode(eventMessage));
    int sampleSize = encodedEventMessage.bytesLeft();

    // Output the sample data.
    for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {
      encodedEventMessage.setPosition(0);
      emsgTrackOutput.sampleData(encodedEventMessage, sampleSize);
    }

    // Output the sample metadata. This is made a little complicated because emsg-v0 atoms
    // have presentation time *delta* while v1 atoms have absolute presentation time.
    if (sampleTimeUs == C.TIME_UNSET) {
      // We need the first sample timestamp in the segment before we can output the metadata.
      pendingMetadataSampleInfos.addLast(
          new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize));
      pendingMetadataSampleBytes += sampleSize;
    } else {
      if (timestampAdjuster != null) {
        sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
      }
      for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {
        emsgTrackOutput.sampleMetadata(
            sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null);
      }
    }
  }

  /** Parses a trex atom (defined in 14496-12). */
  private static Pair<Integer, DefaultSampleValues> parseTrex(ParsableByteArray trex) {
    trex.setPosition(Atom.FULL_HEADER_SIZE);
    int trackId = trex.readInt();
    int defaultSampleDescriptionIndex = trex.readInt() - 1;
    int defaultSampleDuration = trex.readInt();
    int defaultSampleSize = trex.readInt();
    int defaultSampleFlags = trex.readInt();

    return Pair.create(
        trackId,
        new DefaultSampleValues(
            defaultSampleDescriptionIndex,
            defaultSampleDuration,
            defaultSampleSize,
            defaultSampleFlags));
  }

  /** Parses an mehd atom (defined in 14496-12). */
  private static long parseMehd(ParsableByteArray mehd) {
    mehd.setPosition(Atom.HEADER_SIZE);
    int fullAtom = mehd.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);
    return version == 0 ? mehd.readUnsignedInt() : mehd.readUnsignedLongToLong();
  }

  private static void parseMoof(
      ContainerAtom moof,
      SparseArray<TrackBundle> trackBundles,
      boolean haveSideloadedTrack,
      @Flags int flags,
      byte[] extendedTypeScratch)
      throws ParserException {
    int moofContainerChildrenSize = moof.containerChildren.size();
    for (int i = 0; i < moofContainerChildrenSize; i++) {
      Atom.ContainerAtom child = moof.containerChildren.get(i);
      // TODO: Support multiple traf boxes per track in a single moof.
      if (child.type == Atom.TYPE_traf) {
        parseTraf(child, trackBundles, haveSideloadedTrack, flags, extendedTypeScratch);
      }
    }
  }

  /** Parses a traf atom (defined in 14496-12). */
  private static void parseTraf(
      ContainerAtom traf,
      SparseArray<TrackBundle> trackBundles,
      boolean haveSideloadedTrack,
      @Flags int flags,
      byte[] extendedTypeScratch)
      throws ParserException {
    LeafAtom tfhd = checkNotNull(traf.getLeafAtomOfType(Atom.TYPE_tfhd));
    @Nullable TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundles, haveSideloadedTrack);
    if (trackBundle == null) {
      return;
    }

    TrackFragment fragment = trackBundle.fragment;
    long fragmentDecodeTime = fragment.nextFragmentDecodeTime;
    boolean fragmentDecodeTimeIncludesMoov = fragment.nextFragmentDecodeTimeIncludesMoov;
    trackBundle.resetFragmentInfo();
    trackBundle.currentlyInFragment = true;
    @Nullable LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
    if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) {
      fragment.nextFragmentDecodeTime = parseTfdt(tfdtAtom.data);
      fragment.nextFragmentDecodeTimeIncludesMoov = true;
    } else {
      fragment.nextFragmentDecodeTime = fragmentDecodeTime;
      fragment.nextFragmentDecodeTimeIncludesMoov = fragmentDecodeTimeIncludesMoov;
    }

    parseTruns(traf, trackBundle, flags);

    @Nullable
    TrackEncryptionBox encryptionBox =
        trackBundle.moovSampleTable.track.getSampleDescriptionEncryptionBox(
            checkNotNull(fragment.header).sampleDescriptionIndex);

    @Nullable LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
    if (saiz != null) {
      parseSaiz(checkNotNull(encryptionBox), saiz.data, fragment);
    }

    @Nullable LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio);
    if (saio != null) {
      parseSaio(saio.data, fragment);
    }

    @Nullable LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
    if (senc != null) {
      parseSenc(senc.data, fragment);
    }

    parseSampleGroups(traf, encryptionBox != null ? encryptionBox.schemeType : null, fragment);

    int leafChildrenSize = traf.leafChildren.size();
    for (int i = 0; i < leafChildrenSize; i++) {
      LeafAtom atom = traf.leafChildren.get(i);
      if (atom.type == Atom.TYPE_uuid) {
        parseUuid(atom.data, fragment, extendedTypeScratch);
      }
    }
  }

  private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, @Flags int flags)
      throws ParserException {
    int trunCount = 0;
    int totalSampleCount = 0;
    List<LeafAtom> leafChildren = traf.leafChildren;
    int leafChildrenSize = leafChildren.size();
    for (int i = 0; i < leafChildrenSize; i++) {
      LeafAtom atom = leafChildren.get(i);
      if (atom.type == Atom.TYPE_trun) {
        ParsableByteArray trunData = atom.data;
        trunData.setPosition(Atom.FULL_HEADER_SIZE);
        int trunSampleCount = trunData.readUnsignedIntToInt();
        if (trunSampleCount > 0) {
          totalSampleCount += trunSampleCount;
          trunCount++;
        }
      }
    }
    trackBundle.currentTrackRunIndex = 0;
    trackBundle.currentSampleInTrackRun = 0;
    trackBundle.currentSampleIndex = 0;
    trackBundle.fragment.initTables(trunCount, totalSampleCount);

    int trunIndex = 0;
    int trunStartPosition = 0;
    for (int i = 0; i < leafChildrenSize; i++) {
      LeafAtom trun = leafChildren.get(i);
      if (trun.type == Atom.TYPE_trun) {
        trunStartPosition =
            parseTrun(trackBundle, trunIndex++, flags, trun.data, trunStartPosition);
      }
    }
  }

  private static void parseSaiz(
      TrackEncryptionBox encryptionBox, ParsableByteArray saiz, TrackFragment out)
      throws ParserException {
    int vectorSize = encryptionBox.perSampleIvSize;
    saiz.setPosition(Atom.HEADER_SIZE);
    int fullAtom = saiz.readInt();
    int flags = Atom.parseFullAtomFlags(fullAtom);
    if ((flags & 0x01) == 1) {
      saiz.skipBytes(8);
    }
    int defaultSampleInfoSize = saiz.readUnsignedByte();

    int sampleCount = saiz.readUnsignedIntToInt();
    if (sampleCount > out.sampleCount) {
      throw ParserException.createForMalformedContainer(
          "Saiz sample count "
              + sampleCount
              + " is greater than fragment sample count"
              + out.sampleCount,
          /* cause= */ null);
    }

    int totalSize = 0;
    if (defaultSampleInfoSize == 0) {
      boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
      for (int i = 0; i < sampleCount; i++) {
        int sampleInfoSize = saiz.readUnsignedByte();
        totalSize += sampleInfoSize;
        sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
      }
    } else {
      boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
      totalSize += defaultSampleInfoSize * sampleCount;
      Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
    }
    Arrays.fill(out.sampleHasSubsampleEncryptionTable, sampleCount, out.sampleCount, false);
    if (totalSize > 0) {
      out.initEncryptionData(totalSize);
    }
  }

  /**
   * Parses a saio atom (defined in 14496-12).
   *
   * @param saio The saio atom to decode.
   * @param out The {@link TrackFragment} to populate with data from the saio atom.
   */
  private static void parseSaio(ParsableByteArray saio, TrackFragment out) throws ParserException {
    saio.setPosition(Atom.HEADER_SIZE);
    int fullAtom = saio.readInt();
    int flags = Atom.parseFullAtomFlags(fullAtom);
    if ((flags & 0x01) == 1) {
      saio.skipBytes(8);
    }

    int entryCount = saio.readUnsignedIntToInt();
    if (entryCount != 1) {
      // We only support one trun element currently, so always expect one entry.
      throw ParserException.createForMalformedContainer(
          "Unexpected saio entry count: " + entryCount, /* cause= */ null);
    }

    int version = Atom.parseFullAtomVersion(fullAtom);
    out.auxiliaryDataPosition +=
        version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong();
  }

  /**
   * Parses a tfhd atom (defined in 14496-12), updates the corresponding {@link TrackFragment} and
   * returns the {@link TrackBundle} of the corresponding {@link Track}. If the tfhd does not refer
   * to any {@link TrackBundle}, {@code null} is returned and no changes are made.
   *
   * @param tfhd The tfhd atom to decode.
   * @param trackBundles The track bundles, one of which corresponds to the tfhd atom being parsed.
   * @param haveSideloadedTrack Whether {@code trackBundles} contains a single bundle corresponding
   *     to a side-loaded track.
   * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd
   *     does not refer to any {@link TrackBundle}.
   */
  @Nullable
  private static TrackBundle parseTfhd(
      ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles, boolean haveSideloadedTrack) {
    tfhd.setPosition(Atom.HEADER_SIZE);
    int fullAtom = tfhd.readInt();
    int atomFlags = Atom.parseFullAtomFlags(fullAtom);
    int trackId = tfhd.readInt();
    @Nullable
    TrackBundle trackBundle =
        haveSideloadedTrack ? trackBundles.valueAt(0) : trackBundles.get(trackId);
    if (trackBundle == null) {
      return null;
    }
    if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) {
      long baseDataPosition = tfhd.readUnsignedLongToLong();
      trackBundle.fragment.dataPosition = baseDataPosition;
      trackBundle.fragment.auxiliaryDataPosition = baseDataPosition;
    }

    DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;
    int defaultSampleDescriptionIndex =
        ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0)
            ? tfhd.readInt() - 1
            : defaultSampleValues.sampleDescriptionIndex;
    int defaultSampleDuration =
        ((atomFlags & 0x08 /* default_sample_duration_present */) != 0)
            ? tfhd.readInt()
            : defaultSampleValues.duration;
    int defaultSampleSize =
        ((atomFlags & 0x10 /* default_sample_size_present */) != 0)
            ? tfhd.readInt()
            : defaultSampleValues.size;
    int defaultSampleFlags =
        ((atomFlags & 0x20 /* default_sample_flags_present */) != 0)
            ? tfhd.readInt()
            : defaultSampleValues.flags;
    trackBundle.fragment.header =
        new DefaultSampleValues(
            defaultSampleDescriptionIndex,
            defaultSampleDuration,
            defaultSampleSize,
            defaultSampleFlags);
    return trackBundle;
  }

  /**
   * Parses a tfdt atom (defined in 14496-12).
   *
   * @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the
   *     media, expressed in the media's timescale.
   */
  private static long parseTfdt(ParsableByteArray tfdt) {
    tfdt.setPosition(Atom.HEADER_SIZE);
    int fullAtom = tfdt.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);
    return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
  }

  /**
   * Parses a trun atom (defined in 14496-12).
   *
   * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into which
   *     parsed data should be placed.
   * @param index Index of the track run in the fragment.
   * @param flags Flags to allow any required workaround to be executed.
   * @param trun The trun atom to decode.
   * @return The starting position of samples for the next run.
   */
  private static int parseTrun(
      TrackBundle trackBundle,
      int index,
      @Flags int flags,
      ParsableByteArray trun,
      int trackRunStart)
      throws ParserException {
    trun.setPosition(Atom.HEADER_SIZE);
    int fullAtom = trun.readInt();
    int atomFlags = Atom.parseFullAtomFlags(fullAtom);

    Track track = trackBundle.moovSampleTable.track;
    TrackFragment fragment = trackBundle.fragment;
    DefaultSampleValues defaultSampleValues = castNonNull(fragment.header);

    fragment.trunLength[index] = trun.readUnsignedIntToInt();
    fragment.trunDataPosition[index] = fragment.dataPosition;
    if ((atomFlags & 0x01 /* data_offset_present */) != 0) {
      fragment.trunDataPosition[index] += trun.readInt();
    }

    boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0;
    int firstSampleFlags = defaultSampleValues.flags;
    if (firstSampleFlagsPresent) {
      firstSampleFlags = trun.readInt();
    }

    boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0;
    boolean sampleSizesPresent = (atomFlags & 0x200 /* sample_size_present */) != 0;
    boolean sampleFlagsPresent = (atomFlags & 0x400 /* sample_flags_present */) != 0;
    boolean sampleCompositionTimeOffsetsPresent =
        (atomFlags & 0x800 /* sample_composition_time_offsets_present */) != 0;

    // Offset to the entire video timeline. In the presence of B-frames this is usually used to
    // ensure that the first frame's presentation timestamp is zero.
    long edtsOffsetUs = 0;

    // Currently we only support a single edit that moves the entire media timeline (indicated by
    // duration == 0). Other uses of edit lists are uncommon and unsupported.
    if (track.editListDurations != null
        && track.editListDurations.length == 1
        && track.editListDurations[0] == 0) {
      edtsOffsetUs =
          Util.scaleLargeTimestamp(
              castNonNull(track.editListMediaTimes)[0], C.MICROS_PER_SECOND, track.timescale);
    }

    int[] sampleSizeTable = fragment.sampleSizeTable;
    int[] sampleCompositionTimeOffsetUsTable = fragment.sampleCompositionTimeOffsetUsTable;
    long[] sampleDecodingTimeUsTable = fragment.sampleDecodingTimeUsTable;
    boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable;

    boolean workaroundEveryVideoFrameIsSyncFrame =
        track.type == C.TRACK_TYPE_VIDEO
            && (flags & FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0;

    int trackRunEnd = trackRunStart + fragment.trunLength[index];
    long timescale = track.timescale;
    long cumulativeTime = fragment.nextFragmentDecodeTime;
    for (int i = trackRunStart; i < trackRunEnd; i++) {
      // Use trun values if present, otherwise tfhd, otherwise trex.
      int sampleDuration =
          checkNonNegative(sampleDurationsPresent ? trun.readInt() : defaultSampleValues.duration);
      int sampleSize =
          checkNonNegative(sampleSizesPresent ? trun.readInt() : defaultSampleValues.size);
      int sampleFlags =
          sampleFlagsPresent
              ? trun.readInt()
              : (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags : defaultSampleValues.flags;
      if (sampleCompositionTimeOffsetsPresent) {
        // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
        // version 0 trun boxes, however a significant number of streams violate the spec and use
        // signed integers instead. It's safe to always decode sample offsets as signed integers
        // here, because unsigned integers will still be parsed correctly (unless their top bit is
        // set, which is never true in practice because sample offsets are always small).
        int sampleOffset = trun.readInt();
        sampleCompositionTimeOffsetUsTable[i] =
            (int) ((sampleOffset * C.MICROS_PER_SECOND) / timescale);
      } else {
        sampleCompositionTimeOffsetUsTable[i] = 0;
      }
      sampleDecodingTimeUsTable[i] =
          Util.scaleLargeTimestamp(cumulativeTime, C.MICROS_PER_SECOND, timescale) - edtsOffsetUs;
      if (!fragment.nextFragmentDecodeTimeIncludesMoov) {
        sampleDecodingTimeUsTable[i] += trackBundle.moovSampleTable.durationUs;
      }
      sampleSizeTable[i] = sampleSize;
      sampleIsSyncFrameTable[i] =
          ((sampleFlags >> 16) & 0x1) == 0 && (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
      cumulativeTime += sampleDuration;
    }
    fragment.nextFragmentDecodeTime = cumulativeTime;
    return trackRunEnd;
  }

  private static int checkNonNegative(int value) throws ParserException {
    if (value < 0) {
      throw ParserException.createForMalformedContainer(
          "Unexpected negative value: " + value, /* cause= */ null);
    }
    return value;
  }

  private static void parseUuid(
      ParsableByteArray uuid, TrackFragment out, byte[] extendedTypeScratch)
      throws ParserException {
    uuid.setPosition(Atom.HEADER_SIZE);
    uuid.readBytes(extendedTypeScratch, 0, 16);

    // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
    if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
      return;
    }

    // Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
    // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
    // Section 5.3.2.1."
    parseSenc(uuid, 16, out);
  }

  private static void parseSenc(ParsableByteArray senc, TrackFragment out) throws ParserException {
    parseSenc(senc, 0, out);
  }

  private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out)
      throws ParserException {
    senc.setPosition(Atom.HEADER_SIZE + offset);
    int fullAtom = senc.readInt();
    int flags = Atom.parseFullAtomFlags(fullAtom);

    if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
      // TODO: Implement this.
      throw ParserException.createForUnsupportedContainerFeature(
          "Overriding TrackEncryptionBox parameters is unsupported.");
    }

    boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
    int sampleCount = senc.readUnsignedIntToInt();
    if (sampleCount == 0) {
      // Samples are unencrypted.
      Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, out.sampleCount, false);
      return;
    } else if (sampleCount != out.sampleCount) {
      throw ParserException.createForMalformedContainer(
          "Senc sample count "
              + sampleCount
              + " is different from fragment sample count"
              + out.sampleCount,
          /* cause= */ null);
    }

    Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
    out.initEncryptionData(senc.bytesLeft());
    out.fillEncryptionData(senc);
  }

  private static void parseSampleGroups(
      ContainerAtom traf, @Nullable String schemeType, TrackFragment out) throws ParserException {
    // Find sbgp and sgpd boxes with grouping_type == seig.
    @Nullable ParsableByteArray sbgp = null;
    @Nullable ParsableByteArray sgpd = null;
    for (int i = 0; i < traf.leafChildren.size(); i++) {
      LeafAtom leafAtom = traf.leafChildren.get(i);
      ParsableByteArray leafAtomData = leafAtom.data;
      if (leafAtom.type == Atom.TYPE_sbgp) {
        leafAtomData.setPosition(Atom.FULL_HEADER_SIZE);
        if (leafAtomData.readInt() == SAMPLE_GROUP_TYPE_seig) {
          sbgp = leafAtomData;
        }
      } else if (leafAtom.type == Atom.TYPE_sgpd) {
        leafAtomData.setPosition(Atom.FULL_HEADER_SIZE);
        if (leafAtomData.readInt() == SAMPLE_GROUP_TYPE_seig) {
          sgpd = leafAtomData;
        }
      }
    }
    if (sbgp == null || sgpd == null) {
      return;
    }

    sbgp.setPosition(Atom.HEADER_SIZE);
    int sbgpVersion = Atom.parseFullAtomVersion(sbgp.readInt());
    sbgp.skipBytes(4); // grouping_type == seig.
    if (sbgpVersion == 1) {
      sbgp.skipBytes(4); // grouping_type_parameter.
    }
    if (sbgp.readInt() != 1) { // entry_count.
      throw ParserException.createForUnsupportedContainerFeature(
          "Entry count in sbgp != 1 (unsupported).");
    }

    sgpd.setPosition(Atom.HEADER_SIZE);
    int sgpdVersion = Atom.parseFullAtomVersion(sgpd.readInt());
    sgpd.skipBytes(4); // grouping_type == seig.
    if (sgpdVersion == 1) {
      if (sgpd.readUnsignedInt() == 0) {
        throw ParserException.createForUnsupportedContainerFeature(
            "Variable length description in sgpd found (unsupported)");
      }
    } else if (sgpdVersion >= 2) {
      sgpd.skipBytes(4); // default_sample_description_index.
    }
    if (sgpd.readUnsignedInt() != 1) { // entry_count.
      throw ParserException.createForUnsupportedContainerFeature(
          "Entry count in sgpd != 1 (unsupported).");
    }

    // CencSampleEncryptionInformationGroupEntry
    sgpd.skipBytes(1); // reserved = 0.
    int patternByte = sgpd.readUnsignedByte();
    int cryptByteBlock = (patternByte & 0xF0) >> 4;
    int skipByteBlock = patternByte & 0x0F;
    boolean isProtected = sgpd.readUnsignedByte() == 1;
    if (!isProtected) {
      return;
    }
    int perSampleIvSize = sgpd.readUnsignedByte();
    byte[] keyId = new byte[16];
    sgpd.readBytes(keyId, 0, keyId.length);
    @Nullable byte[] constantIv = null;
    if (perSampleIvSize == 0) {
      int constantIvSize = sgpd.readUnsignedByte();
      constantIv = new byte[constantIvSize];
      sgpd.readBytes(constantIv, 0, constantIvSize);
    }
    out.definesEncryptionData = true;
    out.trackEncryptionBox =
        new TrackEncryptionBox(
            isProtected,
            schemeType,
            perSampleIvSize,
            keyId,
            cryptByteBlock,
            skipByteBlock,
            constantIv);
  }

  /**
   * Parses a sidx atom (defined in 14496-12).
   *
   * @param atom The atom data.
   * @param inputPosition The input position of the first byte after the atom.
   * @return A pair consisting of the earliest presentation time in microseconds, and the parsed
   *     {@link ChunkIndex}.
   */
  private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition)
      throws ParserException {
    atom.setPosition(Atom.HEADER_SIZE);
    int fullAtom = atom.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);

    atom.skipBytes(4);
    long timescale = atom.readUnsignedInt();
    long earliestPresentationTime;
    long offset = inputPosition;
    if (version == 0) {
      earliestPresentationTime = atom.readUnsignedInt();
      offset += atom.readUnsignedInt();
    } else {
      earliestPresentationTime = atom.readUnsignedLongToLong();
      offset += atom.readUnsignedLongToLong();
    }
    long earliestPresentationTimeUs =
        Util.scaleLargeTimestamp(earliestPresentationTime, C.MICROS_PER_SECOND, timescale);

    atom.skipBytes(2);

    int referenceCount = atom.readUnsignedShort();
    int[] sizes = new int[referenceCount];
    long[] offsets = new long[referenceCount];
    long[] durationsUs = new long[referenceCount];
    long[] timesUs = new long[referenceCount];

    long time = earliestPresentationTime;
    long timeUs = earliestPresentationTimeUs;
    for (int i = 0; i < referenceCount; i++) {
      int firstInt = atom.readInt();

      int type = 0x80000000 & firstInt;
      if (type != 0) {
        throw ParserException.createForMalformedContainer(
            "Unhandled indirect reference", /* cause= */ null);
      }
      long referenceDuration = atom.readUnsignedInt();

      sizes[i] = 0x7FFFFFFF & firstInt;
      offsets[i] = offset;

      // Calculate time and duration values such that any rounding errors are consistent. i.e. That
      // timesUs[i] + durationsUs[i] == timesUs[i + 1].
      timesUs[i] = timeUs;
      time += referenceDuration;
      timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
      durationsUs[i] = timeUs - timesUs[i];

      atom.skipBytes(4);
      offset += sizes[i];
    }

    return Pair.create(
        earliestPresentationTimeUs, new ChunkIndex(sizes, offsets, durationsUs, timesUs));
  }

  private void readEncryptionData(ExtractorInput input) throws IOException {
    @Nullable TrackBundle nextTrackBundle = null;
    long nextDataOffset = Long.MAX_VALUE;
    int trackBundlesSize = trackBundles.size();
    for (int i = 0; i < trackBundlesSize; i++) {
      TrackFragment trackFragment = trackBundles.valueAt(i).fragment;
      if (trackFragment.sampleEncryptionDataNeedsFill
          && trackFragment.auxiliaryDataPosition < nextDataOffset) {
        nextDataOffset = trackFragment.auxiliaryDataPosition;
        nextTrackBundle = trackBundles.valueAt(i);
      }
    }
    if (nextTrackBundle == null) {
      parserState = STATE_READING_SAMPLE_START;
      return;
    }
    int bytesToSkip = (int) (nextDataOffset - input.getPosition());
    if (bytesToSkip < 0) {
      throw ParserException.createForMalformedContainer(
          "Offset to encryption data was negative.", /* cause= */ null);
    }
    input.skipFully(bytesToSkip);
    nextTrackBundle.fragment.fillEncryptionData(input);
  }

  /**
   * Attempts to read the next sample in the current mdat atom. The read sample may be output or
   * skipped.
   *
   * <p>If there are no more samples in the current mdat atom then the parser state is transitioned
   * to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned.
   *
   * <p>It is possible for a sample to be partially read in the case that an exception is thrown. In
   * this case the method can be called again to read the remainder of the sample.
   *
   * @param input The {@link ExtractorInput} from which to read data.
   * @return Whether a sample was read. The read sample may have been output or skipped. False
   *     indicates that there are no samples left to read in the current mdat.
   * @throws IOException If an error occurs reading from the input.
   */
  private boolean readSample(ExtractorInput input) throws IOException {
    @Nullable TrackBundle trackBundle = currentTrackBundle;
    if (trackBundle == null) {
      trackBundle = getNextTrackBundle(trackBundles);
      if (trackBundle == null) {
        // We've run out of samples in the current mdat. Discard any trailing data and prepare to
        // read the header of the next atom.
        int bytesToSkip = (int) (endOfMdatPosition - input.getPosition());
        if (bytesToSkip < 0) {
          throw ParserException.createForMalformedContainer(
              "Offset to end of mdat was negative.", /* cause= */ null);
        }
        input.skipFully(bytesToSkip);
        enterReadingAtomHeaderState();
        return false;
      }

      long nextDataPosition = trackBundle.getCurrentSampleOffset();
      // We skip bytes preceding the next sample to read.
      int bytesToSkip = (int) (nextDataPosition - input.getPosition());
      if (bytesToSkip < 0) {
        // Assume the sample data must be contiguous in the mdat with no preceding data.
        Log.w(TAG, "Ignoring negative offset to sample data.");
        bytesToSkip = 0;
      }
      input.skipFully(bytesToSkip);
      currentTrackBundle = trackBundle;
    }
    if (parserState == STATE_READING_SAMPLE_START) {
      sampleSize = trackBundle.getCurrentSampleSize();

      if (trackBundle.currentSampleIndex < trackBundle.firstSampleToOutputIndex) {
        input.skipFully(sampleSize);
        trackBundle.skipSampleEncryptionData();
        if (!trackBundle.next()) {
          currentTrackBundle = null;
        }
        parserState = STATE_READING_SAMPLE_START;
        return true;
      }

      if (trackBundle.moovSampleTable.track.sampleTransformation
          == Track.TRANSFORMATION_CEA608_CDAT) {
        sampleSize -= Atom.HEADER_SIZE;
        input.skipFully(Atom.HEADER_SIZE);
      }

      if (MimeTypes.AUDIO_AC4.equals(trackBundle.moovSampleTable.track.format.sampleMimeType)) {
        // AC4 samples need to be prefixed with a clear sample header.
        sampleBytesWritten =
            trackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE);
        Ac4Util.getAc4SampleHeader(sampleSize, scratch);
        trackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
        sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE;
      } else {
        sampleBytesWritten =
            trackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0);
      }
      sampleSize += sampleBytesWritten;
      parserState = STATE_READING_SAMPLE_CONTINUE;
      sampleCurrentNalBytesRemaining = 0;
    }

    Track track = trackBundle.moovSampleTable.track;
    TrackOutput output = trackBundle.output;
    long sampleTimeUs = trackBundle.getCurrentSamplePresentationTimeUs();
    if (timestampAdjuster != null) {
      sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
    }
    if (track.nalUnitLengthFieldLength != 0) {
      // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
      // they're only 1 or 2 bytes long.
      byte[] nalPrefixData = nalPrefix.getData();
      nalPrefixData[0] = 0;
      nalPrefixData[1] = 0;
      nalPrefixData[2] = 0;
      int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1;
      int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength;
      // NAL units are length delimited, but the decoder requires start code delimited units.
      // Loop until we've written the sample to the track output, replacing length delimiters with
      // start codes as we encounter them.
      while (sampleBytesWritten < sampleSize) {
        if (sampleCurrentNalBytesRemaining == 0) {
          // Read the NAL length so that we know where we find the next one, and its type.
          input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength);
          nalPrefix.setPosition(0);
          int nalLengthInt = nalPrefix.readInt();
          if (nalLengthInt < 1) {
            throw ParserException.createForMalformedContainer(
                "Invalid NAL length", /* cause= */ null);
          }
          sampleCurrentNalBytesRemaining = nalLengthInt - 1;
          // Write a start code for the current NAL unit.
          nalStartCode.setPosition(0);
          output.sampleData(nalStartCode, 4);
          // Write the NAL unit type byte.
          output.sampleData(nalPrefix, 1);
          processSeiNalUnitPayload =
              ceaTrackOutputs.length > 0
                  && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
          sampleBytesWritten += 5;
          sampleSize += nalUnitLengthFieldLengthDiff;
        } else {
          int writtenBytes;
          if (processSeiNalUnitPayload) {
            // Read and write the payload of the SEI NAL unit.
            nalBuffer.reset(sampleCurrentNalBytesRemaining);
            input.readFully(nalBuffer.getData(), 0, sampleCurrentNalBytesRemaining);
            output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining);
            writtenBytes = sampleCurrentNalBytesRemaining;
            // Unescape and process the SEI NAL unit.
            int unescapedLength =
                NalUnitUtil.unescapeStream(nalBuffer.getData(), nalBuffer.limit());
            // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte.
            nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0);
            nalBuffer.setLimit(unescapedLength);
            CeaUtil.consume(sampleTimeUs, nalBuffer, ceaTrackOutputs);
          } else {
            // Write the payload of the NAL unit.
            writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
          }
          sampleBytesWritten += writtenBytes;
          sampleCurrentNalBytesRemaining -= writtenBytes;
        }
      }
    } else {
      while (sampleBytesWritten < sampleSize) {
        int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false);
        sampleBytesWritten += writtenBytes;
      }
    }

    @C.BufferFlags int sampleFlags = trackBundle.getCurrentSampleFlags();

    // Encryption data.
    @Nullable TrackOutput.CryptoData cryptoData = null;
    @Nullable TrackEncryptionBox encryptionBox = trackBundle.getEncryptionBoxIfEncrypted();
    if (encryptionBox != null) {
      cryptoData = encryptionBox.cryptoData;
    }

    output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData);

    // After we have the sampleTimeUs, we can commit all the pending metadata samples
    outputPendingMetadataSamples(sampleTimeUs);
    if (!trackBundle.next()) {
      currentTrackBundle = null;
    }
    parserState = STATE_READING_SAMPLE_START;
    return true;
  }

  private void outputPendingMetadataSamples(long sampleTimeUs) {
    while (!pendingMetadataSampleInfos.isEmpty()) {
      MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
      pendingMetadataSampleBytes -= sampleInfo.size;
      long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs;
      if (timestampAdjuster != null) {
        metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs);
      }
      for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {
        emsgTrackOutput.sampleMetadata(
            metadataTimeUs,
            C.BUFFER_FLAG_KEY_FRAME,
            sampleInfo.size,
            pendingMetadataSampleBytes,
            null);
      }
    }
  }

  /**
   * Returns the {@link TrackBundle} whose sample has the earliest file position out of those yet to
   * be consumed, or null if all have been consumed.
   */
  @Nullable
  private static TrackBundle getNextTrackBundle(SparseArray<TrackBundle> trackBundles) {
    @Nullable TrackBundle nextTrackBundle = null;
    long nextSampleOffset = Long.MAX_VALUE;

    int trackBundlesSize = trackBundles.size();
    for (int i = 0; i < trackBundlesSize; i++) {
      TrackBundle trackBundle = trackBundles.valueAt(i);
      if ((!trackBundle.currentlyInFragment
              && trackBundle.currentSampleIndex == trackBundle.moovSampleTable.sampleCount)
          || (trackBundle.currentlyInFragment
              && trackBundle.currentTrackRunIndex == trackBundle.fragment.trunCount)) {
        // This track sample table or fragment contains no more runs in the next mdat box.
      } else {
        long sampleOffset = trackBundle.getCurrentSampleOffset();
        if (sampleOffset < nextSampleOffset) {
          nextTrackBundle = trackBundle;
          nextSampleOffset = sampleOffset;
        }
      }
    }
    return nextTrackBundle;
  }

  /** Returns DrmInitData from leaf atoms. */
  @Nullable
  private static DrmInitData getDrmInitDataFromAtoms(List<Atom.LeafAtom> leafChildren) {
    @Nullable ArrayList<SchemeData> schemeDatas = null;
    int leafChildrenSize = leafChildren.size();
    for (int i = 0; i < leafChildrenSize; i++) {
      LeafAtom child = leafChildren.get(i);
      if (child.type == Atom.TYPE_pssh) {
        if (schemeDatas == null) {
          schemeDatas = new ArrayList<>();
        }
        byte[] psshData = child.data.getData();
        @Nullable UUID uuid = PsshAtomUtil.parseUuid(psshData);
        if (uuid == null) {
          Log.w(TAG, "Skipped pssh atom (failed to extract uuid)");
        } else {
          schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData));
        }
      }
    }
    return schemeDatas == null ? null : new DrmInitData(schemeDatas);
  }

  /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */
  private static boolean shouldParseLeafAtom(int atom) {
    return atom == Atom.TYPE_hdlr
        || atom == Atom.TYPE_mdhd
        || atom == Atom.TYPE_mvhd
        || atom == Atom.TYPE_sidx
        || atom == Atom.TYPE_stsd
        || atom == Atom.TYPE_stts
        || atom == Atom.TYPE_ctts
        || atom == Atom.TYPE_stsc
        || atom == Atom.TYPE_stsz
        || atom == Atom.TYPE_stz2
        || atom == Atom.TYPE_stco
        || atom == Atom.TYPE_co64
        || atom == Atom.TYPE_stss
        || atom == Atom.TYPE_tfdt
        || atom == Atom.TYPE_tfhd
        || atom == Atom.TYPE_tkhd
        || atom == Atom.TYPE_trex
        || atom == Atom.TYPE_trun
        || atom == Atom.TYPE_pssh
        || atom == Atom.TYPE_saiz
        || atom == Atom.TYPE_saio
        || atom == Atom.TYPE_senc
        || atom == Atom.TYPE_uuid
        || atom == Atom.TYPE_sbgp
        || atom == Atom.TYPE_sgpd
        || atom == Atom.TYPE_elst
        || atom == Atom.TYPE_mehd
        || atom == Atom.TYPE_emsg;
  }

  /** Returns whether the extractor should decode a container atom with type {@code atom}. */
  private static boolean shouldParseContainerAtom(int atom) {
    return atom == Atom.TYPE_moov
        || atom == Atom.TYPE_trak
        || atom == Atom.TYPE_mdia
        || atom == Atom.TYPE_minf
        || atom == Atom.TYPE_stbl
        || atom == Atom.TYPE_moof
        || atom == Atom.TYPE_traf
        || atom == Atom.TYPE_mvex
        || atom == Atom.TYPE_edts;
  }

  /** Holds data corresponding to a metadata sample. */
  private static final class MetadataSampleInfo {

    public final long presentationTimeDeltaUs;
    public final int size;

    public MetadataSampleInfo(long presentationTimeDeltaUs, int size) {
      this.presentationTimeDeltaUs = presentationTimeDeltaUs;
      this.size = size;
    }
  }

  /** Holds data corresponding to a single track. */
  private static final class TrackBundle {

    private static final int SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH = 8;

    public final TrackOutput output;
    public final TrackFragment fragment;
    public final ParsableByteArray scratch;

    public TrackSampleTable moovSampleTable;
    public DefaultSampleValues defaultSampleValues;
    public int currentSampleIndex;
    public int currentSampleInTrackRun;
    public int currentTrackRunIndex;
    public int firstSampleToOutputIndex;

    private final ParsableByteArray encryptionSignalByte;
    private final ParsableByteArray defaultInitializationVector;

    private boolean currentlyInFragment;

    public TrackBundle(
        TrackOutput output,
        TrackSampleTable moovSampleTable,
        DefaultSampleValues defaultSampleValues) {
      this.output = output;
      this.moovSampleTable = moovSampleTable;
      this.defaultSampleValues = defaultSampleValues;
      fragment = new TrackFragment();
      scratch = new ParsableByteArray();
      encryptionSignalByte = new ParsableByteArray(1);
      defaultInitializationVector = new ParsableByteArray();
      reset(moovSampleTable, defaultSampleValues);
    }

    public void reset(TrackSampleTable moovSampleTable, DefaultSampleValues defaultSampleValues) {
      this.moovSampleTable = moovSampleTable;
      this.defaultSampleValues = defaultSampleValues;
      output.format(moovSampleTable.track.format);
      resetFragmentInfo();
    }

    public void updateDrmInitData(DrmInitData drmInitData) {
      @Nullable
      TrackEncryptionBox encryptionBox =
          moovSampleTable.track.getSampleDescriptionEncryptionBox(
              castNonNull(fragment.header).sampleDescriptionIndex);
      @Nullable String schemeType = encryptionBox != null ? encryptionBox.schemeType : null;
      DrmInitData updatedDrmInitData = drmInitData.copyWithSchemeType(schemeType);
      Format format =
          moovSampleTable.track.format.buildUpon().setDrmInitData(updatedDrmInitData).build();
      output.format(format);
    }

    /** Resets the current fragment, sample indices and {@link #currentlyInFragment} boolean. */
    public void resetFragmentInfo() {
      fragment.reset();
      currentSampleIndex = 0;
      currentTrackRunIndex = 0;
      currentSampleInTrackRun = 0;
      firstSampleToOutputIndex = 0;
      currentlyInFragment = false;
    }

    /**
     * Advances {@link #firstSampleToOutputIndex} to point to the sync sample before the specified
     * seek time in the current fragment.
     *
     * @param timeUs The seek time, in microseconds.
     */
    public void seek(long timeUs) {
      int searchIndex = currentSampleIndex;
      while (searchIndex < fragment.sampleCount
          && fragment.getSamplePresentationTimeUs(searchIndex) < timeUs) {
        if (fragment.sampleIsSyncFrameTable[searchIndex]) {
          firstSampleToOutputIndex = searchIndex;
        }
        searchIndex++;
      }
    }

    /** Returns the presentation time of the current sample in microseconds. */
    public long getCurrentSamplePresentationTimeUs() {
      return !currentlyInFragment
          ? moovSampleTable.timestampsUs[currentSampleIndex]
          : fragment.getSamplePresentationTimeUs(currentSampleIndex);
    }

    /** Returns the byte offset of the current sample. */
    public long getCurrentSampleOffset() {
      return !currentlyInFragment
          ? moovSampleTable.offsets[currentSampleIndex]
          : fragment.trunDataPosition[currentTrackRunIndex];
    }

    /** Returns the size of the current sample in bytes. */
    public int getCurrentSampleSize() {
      return !currentlyInFragment
          ? moovSampleTable.sizes[currentSampleIndex]
          : fragment.sampleSizeTable[currentSampleIndex];
    }

    /** Returns the {@link C.BufferFlags} corresponding to the current sample. */
    @C.BufferFlags
    public int getCurrentSampleFlags() {
      int flags =
          !currentlyInFragment
              ? moovSampleTable.flags[currentSampleIndex]
              : (fragment.sampleIsSyncFrameTable[currentSampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0);
      if (getEncryptionBoxIfEncrypted() != null) {
        flags |= C.BUFFER_FLAG_ENCRYPTED;
      }
      return flags;
    }

    /**
     * Advances the indices in the bundle to point to the next sample in the sample table (if it has
     * not reached the fragments yet) or in the current fragment.
     *
     * <p>If the current sample is the last one in the sample table, then the advanced state will be
     * {@code currentSampleIndex == moovSampleTable.sampleCount}. If the current sample is the last
     * one in the current fragment, then the advanced state will be {@code currentSampleIndex ==
     * fragment.sampleCount}, {@code currentTrackRunIndex == fragment.trunCount} and {@code
     * #currentSampleInTrackRun == 0}.
     *
     * @return Whether this {@link TrackBundle} can be used to read the next sample without
     *     recomputing the next {@link TrackBundle}.
     */
    public boolean next() {
      currentSampleIndex++;
      if (!currentlyInFragment) {
        return false;
      }
      currentSampleInTrackRun++;
      if (currentSampleInTrackRun == fragment.trunLength[currentTrackRunIndex]) {
        currentTrackRunIndex++;
        currentSampleInTrackRun = 0;
        return false;
      }
      return true;
    }

    /**
     * Outputs the encryption data for the current sample.
     *
     * <p>This is not supported yet for samples specified in the sample table.
     *
     * @param sampleSize The size of the current sample in bytes, excluding any additional clear
     *     header that will be prefixed to the sample by the extractor.
     * @param clearHeaderSize The size of a clear header that will be prefixed to the sample by the
     *     extractor, or 0.
     * @return The number of written bytes.
     */
    public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) {
      @Nullable TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();
      if (encryptionBox == null) {
        return 0;
      }

      ParsableByteArray initializationVectorData;
      int vectorSize;
      if (encryptionBox.perSampleIvSize != 0) {
        initializationVectorData = fragment.sampleEncryptionData;
        vectorSize = encryptionBox.perSampleIvSize;
      } else {
        // The default initialization vector should be used.
        byte[] initVectorData = castNonNull(encryptionBox.defaultInitializationVector);
        defaultInitializationVector.reset(initVectorData, initVectorData.length);
        initializationVectorData = defaultInitializationVector;
        vectorSize = initVectorData.length;
      }

      boolean haveSubsampleEncryptionTable =
          fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex);
      boolean writeSubsampleEncryptionData = haveSubsampleEncryptionTable || clearHeaderSize != 0;

      // Write the signal byte, containing the vector size and the subsample encryption flag.
      encryptionSignalByte.getData()[0] =
          (byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0));
      encryptionSignalByte.setPosition(0);
      output.sampleData(encryptionSignalByte, 1, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
      // Write the vector.
      output.sampleData(
          initializationVectorData, vectorSize, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);

      if (!writeSubsampleEncryptionData) {
        return 1 + vectorSize;
      }

      if (!haveSubsampleEncryptionTable) {
        // The sample is fully encrypted, except for the additional clear header that the extractor
        // is going to prefix. We need to synthesize subsample encryption data that takes the header
        // into account.
        scratch.reset(SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH);
        // subsampleCount = 1 (unsigned short)
        byte[] data = scratch.getData();
        data[0] = (byte) 0;
        data[1] = (byte) 1;
        // clearDataSize = clearHeaderSize (unsigned short)
        data[2] = (byte) ((clearHeaderSize >> 8) & 0xFF);
        data[3] = (byte) (clearHeaderSize & 0xFF);
        // encryptedDataSize = sampleSize (unsigned int)
        data[4] = (byte) ((sampleSize >> 24) & 0xFF);
        data[5] = (byte) ((sampleSize >> 16) & 0xFF);
        data[6] = (byte) ((sampleSize >> 8) & 0xFF);
        data[7] = (byte) (sampleSize & 0xFF);
        output.sampleData(
            scratch,
            SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH,
            TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
        return 1 + vectorSize + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH;
      }

      ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData;
      int subsampleCount = subsampleEncryptionData.readUnsignedShort();
      subsampleEncryptionData.skipBytes(-2);
      int subsampleDataLength = 2 + 6 * subsampleCount;

      if (clearHeaderSize != 0) {
        // We need to account for the additional clear header by adding clearHeaderSize to
        // clearDataSize for the first subsample specified in the subsample encryption data.
        scratch.reset(subsampleDataLength);
        byte[] scratchData = scratch.getData();
        subsampleEncryptionData.readBytes(scratchData, /* offset= */ 0, subsampleDataLength);

        int clearDataSize = (scratchData[2] & 0xFF) << 8 | (scratchData[3] & 0xFF);
        int adjustedClearDataSize = clearDataSize + clearHeaderSize;
        scratchData[2] = (byte) ((adjustedClearDataSize >> 8) & 0xFF);
        scratchData[3] = (byte) (adjustedClearDataSize & 0xFF);
        subsampleEncryptionData = scratch;
      }

      output.sampleData(
          subsampleEncryptionData, subsampleDataLength, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
      return 1 + vectorSize + subsampleDataLength;
    }

    /**
     * Skips the encryption data for the current sample.
     *
     * <p>This is not supported yet for samples specified in the sample table.
     */
    public void skipSampleEncryptionData() {
      @Nullable TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();
      if (encryptionBox == null) {
        return;
      }

      ParsableByteArray sampleEncryptionData = fragment.sampleEncryptionData;
      if (encryptionBox.perSampleIvSize != 0) {
        sampleEncryptionData.skipBytes(encryptionBox.perSampleIvSize);
      }
      if (fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex)) {
        sampleEncryptionData.skipBytes(6 * sampleEncryptionData.readUnsignedShort());
      }
    }

    @Nullable
    public TrackEncryptionBox getEncryptionBoxIfEncrypted() {
      if (!currentlyInFragment) {
        // Encryption is not supported yet for samples specified in the sample table.
        return null;
      }
      int sampleDescriptionIndex = castNonNull(fragment.header).sampleDescriptionIndex;
      @Nullable
      TrackEncryptionBox encryptionBox =
          fragment.trackEncryptionBox != null
              ? fragment.trackEncryptionBox
              : moovSampleTable.track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex);
      return encryptionBox != null && encryptionBox.isEncrypted ? encryptionBox : null;
    }
  }
}