/*
 * 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.MimeTypes.getMimeTypeFromMp4ObjectType;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.max;

import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.ColorInfo;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format;
import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.CodecSpecificDataUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
import androidx.media3.extractor.AacUtil;
import androidx.media3.extractor.Ac3Util;
import androidx.media3.extractor.Ac4Util;
import androidx.media3.extractor.AvcConfig;
import androidx.media3.extractor.DolbyVisionConfig;
import androidx.media3.extractor.ExtractorUtil;
import androidx.media3.extractor.GaplessInfoHolder;
import androidx.media3.extractor.HevcConfig;
import androidx.media3.extractor.OpusUtil;
import androidx.media3.extractor.metadata.mp4.SmtaMetadataEntry;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;

/** Utility methods for parsing MP4 format atom payloads according to ISO/IEC 14496-12. */
@SuppressWarnings("ConstantField")
/* package */ final class AtomParsers {

  private static final String TAG = "AtomParsers";

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_clcp = 0x636c6370;

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_mdta = 0x6d647461;

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_meta = 0x6d657461;

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_nclc = 0x6e636c63;

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_nclx = 0x6e636c78;

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_sbtl = 0x7362746c;

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_soun = 0x736f756e;

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_subt = 0x73756274;

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_text = 0x74657874;

  @SuppressWarnings("ConstantCaseForConstants")
  private static final int TYPE_vide = 0x76696465;

  /**
   * The threshold number of samples to trim from the start/end of an audio track when applying an
   * edit below which gapless info can be used (rather than removing samples from the sample table).
   */
  private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4;

  /** The magic signature for an Opus Identification header, as defined in RFC-7845. */
  private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead");

  /**
   * Parse the trak atoms in a moov atom (defined in ISO/IEC 14496-12).
   *
   * @param moov Moov atom to decode.
   * @param gaplessInfoHolder Holder to populate with gapless playback information.
   * @param duration The duration in units of the timescale declared in the mvhd atom, or {@link
   *     C#TIME_UNSET} if the duration should be parsed from the tkhd atom.
   * @param drmInitData {@link DrmInitData} to be included in the format, or {@code null}.
   * @param ignoreEditLists Whether to ignore any edit lists in the trak boxes.
   * @param isQuickTime True for QuickTime media. False otherwise.
   * @param modifyTrackFunction A function to apply to the {@link Track Tracks} in the result.
   * @return A list of {@link TrackSampleTable} instances.
   * @throws ParserException Thrown if the trak atoms can't be parsed.
   */
  public static List<TrackSampleTable> parseTraks(
      Atom.ContainerAtom moov,
      GaplessInfoHolder gaplessInfoHolder,
      long duration,
      @Nullable DrmInitData drmInitData,
      boolean ignoreEditLists,
      boolean isQuickTime,
      Function<@NullableType Track, @NullableType Track> modifyTrackFunction)
      throws ParserException {
    List<TrackSampleTable> trackSampleTables = new ArrayList<>();
    for (int i = 0; i < moov.containerChildren.size(); i++) {
      Atom.ContainerAtom atom = moov.containerChildren.get(i);
      if (atom.type != Atom.TYPE_trak) {
        continue;
      }
      @Nullable
      Track track =
          modifyTrackFunction.apply(
              parseTrak(
                  atom,
                  checkNotNull(moov.getLeafAtomOfType(Atom.TYPE_mvhd)),
                  duration,
                  drmInitData,
                  ignoreEditLists,
                  isQuickTime));
      if (track == null) {
        continue;
      }
      Atom.ContainerAtom stblAtom =
          checkNotNull(
              checkNotNull(
                      checkNotNull(atom.getContainerAtomOfType(Atom.TYPE_mdia))
                          .getContainerAtomOfType(Atom.TYPE_minf))
                  .getContainerAtomOfType(Atom.TYPE_stbl));
      TrackSampleTable trackSampleTable = parseStbl(track, stblAtom, gaplessInfoHolder);
      trackSampleTables.add(trackSampleTable);
    }
    return trackSampleTables;
  }

  /**
   * Parses a udta atom.
   *
   * @param udtaAtom The udta (user data) atom to decode.
   * @return A {@link Pair} containing the metadata from the meta child atom as first value (if
   *     any), and the metadata from the smta child atom as second value (if any).
   */
  public static Pair<@NullableType Metadata, @NullableType Metadata> parseUdta(
      Atom.LeafAtom udtaAtom) {
    ParsableByteArray udtaData = udtaAtom.data;
    udtaData.setPosition(Atom.HEADER_SIZE);
    @Nullable Metadata metaMetadata = null;
    @Nullable Metadata smtaMetadata = null;
    while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) {
      int atomPosition = udtaData.getPosition();
      int atomSize = udtaData.readInt();
      int atomType = udtaData.readInt();
      if (atomType == Atom.TYPE_meta) {
        udtaData.setPosition(atomPosition);
        metaMetadata = parseUdtaMeta(udtaData, atomPosition + atomSize);
      } else if (atomType == Atom.TYPE_smta) {
        udtaData.setPosition(atomPosition);
        smtaMetadata = parseSmta(udtaData, atomPosition + atomSize);
      }
      udtaData.setPosition(atomPosition + atomSize);
    }
    return Pair.create(metaMetadata, smtaMetadata);
  }

  /**
   * Parses a metadata meta atom if it contains metadata with handler 'mdta'.
   *
   * @param meta The metadata atom to decode.
   * @return Parsed metadata, or null.
   */
  @Nullable
  public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) {
    @Nullable Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr);
    @Nullable Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys);
    @Nullable Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst);
    if (hdlrAtom == null
        || keysAtom == null
        || ilstAtom == null
        || parseHdlr(hdlrAtom.data) != TYPE_mdta) {
      // There isn't enough information to parse the metadata, or the handler type is unexpected.
      return null;
    }

    // Parse metadata keys.
    ParsableByteArray keys = keysAtom.data;
    keys.setPosition(Atom.FULL_HEADER_SIZE);
    int entryCount = keys.readInt();
    String[] keyNames = new String[entryCount];
    for (int i = 0; i < entryCount; i++) {
      int entrySize = keys.readInt();
      keys.skipBytes(4); // keyNamespace
      int keySize = entrySize - 8;
      keyNames[i] = keys.readString(keySize);
    }

    // Parse metadata items.
    ParsableByteArray ilst = ilstAtom.data;
    ilst.setPosition(Atom.HEADER_SIZE);
    ArrayList<Metadata.Entry> entries = new ArrayList<>();
    while (ilst.bytesLeft() > Atom.HEADER_SIZE) {
      int atomPosition = ilst.getPosition();
      int atomSize = ilst.readInt();
      int keyIndex = ilst.readInt() - 1;
      if (keyIndex >= 0 && keyIndex < keyNames.length) {
        String key = keyNames[keyIndex];
        @Nullable
        Metadata.Entry entry =
            MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key);
        if (entry != null) {
          entries.add(entry);
        }
      } else {
        Log.w(TAG, "Skipped metadata with unknown key index: " + keyIndex);
      }
      ilst.setPosition(atomPosition + atomSize);
    }
    return entries.isEmpty() ? null : new Metadata(entries);
  }

  /**
   * Possibly skips the version and flags fields (1+3 byte) of a full meta atom.
   *
   * <p>Atoms of type {@link Atom#TYPE_meta} are defined to be full atoms which have four additional
   * bytes for a version and a flags field (see 4.2 'Object Structure' in ISO/IEC 14496-12:2005).
   * QuickTime do not have such a full box structure. Since some of these files are encoded wrongly,
   * we can't rely on the file type though. Instead we must check the 8 bytes after the common
   * header bytes ourselves.
   *
   * @param meta The 8 or more bytes following the meta atom size and type.
   */
  public static void maybeSkipRemainingMetaAtomHeaderBytes(ParsableByteArray meta) {
    int endPosition = meta.getPosition();
    // The next 8 bytes can be either:
    // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom]
    // (qt)  [4 byte size of next atom      ][4 byte hdlr atom type   ]
    // In case of (iso) we need to skip the next 4 bytes.
    meta.skipBytes(4);
    if (meta.readInt() != Atom.TYPE_hdlr) {
      endPosition += 4;
    }
    meta.setPosition(endPosition);
  }

  /**
   * Parses a trak atom (defined in ISO/IEC 14496-12).
   *
   * @param trak Atom to decode.
   * @param mvhd Movie header atom, used to get the timescale.
   * @param duration The duration in units of the timescale declared in the mvhd atom, or {@link
   *     C#TIME_UNSET} if the duration should be parsed from the tkhd atom.
   * @param drmInitData {@link DrmInitData} to be included in the format, or {@code null}.
   * @param ignoreEditLists Whether to ignore any edit lists in the trak box.
   * @param isQuickTime True for QuickTime media. False otherwise.
   * @return A {@link Track} instance, or {@code null} if the track's type isn't supported.
   * @throws ParserException Thrown if the trak atom can't be parsed.
   */
  @Nullable
  private static Track parseTrak(
      Atom.ContainerAtom trak,
      Atom.LeafAtom mvhd,
      long duration,
      @Nullable DrmInitData drmInitData,
      boolean ignoreEditLists,
      boolean isQuickTime)
      throws ParserException {
    Atom.ContainerAtom mdia = checkNotNull(trak.getContainerAtomOfType(Atom.TYPE_mdia));
    @C.TrackType
    int trackType =
        getTrackTypeForHdlr(parseHdlr(checkNotNull(mdia.getLeafAtomOfType(Atom.TYPE_hdlr)).data));
    if (trackType == C.TRACK_TYPE_UNKNOWN) {
      return null;
    }

    TkhdData tkhdData = parseTkhd(checkNotNull(trak.getLeafAtomOfType(Atom.TYPE_tkhd)).data);
    if (duration == C.TIME_UNSET) {
      duration = tkhdData.duration;
    }
    long movieTimescale = parseMvhd(mvhd.data);
    long durationUs;
    if (duration == C.TIME_UNSET) {
      durationUs = C.TIME_UNSET;
    } else {
      durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale);
    }
    Atom.ContainerAtom stbl =
        checkNotNull(
            checkNotNull(mdia.getContainerAtomOfType(Atom.TYPE_minf))
                .getContainerAtomOfType(Atom.TYPE_stbl));

    Pair<Long, String> mdhdData =
        parseMdhd(checkNotNull(mdia.getLeafAtomOfType(Atom.TYPE_mdhd)).data);
    StsdData stsdData =
        parseStsd(
            checkNotNull(stbl.getLeafAtomOfType(Atom.TYPE_stsd)).data,
            tkhdData.id,
            tkhdData.rotationDegrees,
            mdhdData.second,
            drmInitData,
            isQuickTime);
    @Nullable long[] editListDurations = null;
    @Nullable long[] editListMediaTimes = null;
    if (!ignoreEditLists) {
      @Nullable Atom.ContainerAtom edtsAtom = trak.getContainerAtomOfType(Atom.TYPE_edts);
      if (edtsAtom != null) {
        @Nullable Pair<long[], long[]> edtsData = parseEdts(edtsAtom);
        if (edtsData != null) {
          editListDurations = edtsData.first;
          editListMediaTimes = edtsData.second;
        }
      }
    }
    return stsdData.format == null
        ? null
        : new Track(
            tkhdData.id,
            trackType,
            mdhdData.first,
            movieTimescale,
            durationUs,
            stsdData.format,
            stsdData.requiredSampleTransformation,
            stsdData.trackEncryptionBoxes,
            stsdData.nalUnitLengthFieldLength,
            editListDurations,
            editListMediaTimes);
  }

  /**
   * Parses an stbl atom (defined in ISO/IEC 14496-12).
   *
   * @param track Track to which this sample table corresponds.
   * @param stblAtom stbl (sample table) atom to decode.
   * @param gaplessInfoHolder Holder to populate with gapless playback information.
   * @return Sample table described by the stbl atom.
   * @throws ParserException Thrown if the stbl atom can't be parsed.
   */
  private static TrackSampleTable parseStbl(
      Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder)
      throws ParserException {
    SampleSizeBox sampleSizeBox;
    @Nullable Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);
    if (stszAtom != null) {
      sampleSizeBox = new StszSampleSizeBox(stszAtom, track.format);
    } else {
      @Nullable Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2);
      if (stz2Atom == null) {
        throw ParserException.createForMalformedContainer(
            "Track has no sample table size information", /* cause= */ null);
      }
      sampleSizeBox = new Stz2SampleSizeBox(stz2Atom);
    }

    int sampleCount = sampleSizeBox.getSampleCount();
    if (sampleCount == 0) {
      return new TrackSampleTable(
          track,
          /* offsets= */ new long[0],
          /* sizes= */ new int[0],
          /* maximumSize= */ 0,
          /* timestampsUs= */ new long[0],
          /* flags= */ new int[0],
          /* durationUs= */ 0);
    }

    // Entries are byte offsets of chunks.
    boolean chunkOffsetsAreLongs = false;
    @Nullable Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco);
    if (chunkOffsetsAtom == null) {
      chunkOffsetsAreLongs = true;
      chunkOffsetsAtom = checkNotNull(stblAtom.getLeafAtomOfType(Atom.TYPE_co64));
    }
    ParsableByteArray chunkOffsets = chunkOffsetsAtom.data;
    // Entries are (chunk number, number of samples per chunk, sample description index).
    ParsableByteArray stsc = checkNotNull(stblAtom.getLeafAtomOfType(Atom.TYPE_stsc)).data;
    // Entries are (number of samples, timestamp delta between those samples).
    ParsableByteArray stts = checkNotNull(stblAtom.getLeafAtomOfType(Atom.TYPE_stts)).data;
    // Entries are the indices of samples that are synchronization samples.
    @Nullable Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss);
    @Nullable ParsableByteArray stss = stssAtom != null ? stssAtom.data : null;
    // Entries are (number of samples, timestamp offset).
    @Nullable Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts);
    @Nullable ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;

    // Prepare to read chunk information.
    ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs);

    // Prepare to read sample timestamps.
    stts.setPosition(Atom.FULL_HEADER_SIZE);
    int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
    int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
    int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();

    // Prepare to read sample timestamp offsets, if ctts is present.
    int remainingSamplesAtTimestampOffset = 0;
    int remainingTimestampOffsetChanges = 0;
    int timestampOffset = 0;
    if (ctts != null) {
      ctts.setPosition(Atom.FULL_HEADER_SIZE);
      remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt();
    }

    int nextSynchronizationSampleIndex = C.INDEX_UNSET;
    int remainingSynchronizationSamples = 0;
    if (stss != null) {
      stss.setPosition(Atom.FULL_HEADER_SIZE);
      remainingSynchronizationSamples = stss.readUnsignedIntToInt();
      if (remainingSynchronizationSamples > 0) {
        nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
      } else {
        // Ignore empty stss boxes, which causes all samples to be treated as sync samples.
        stss = null;
      }
    }

    // Fixed sample size raw audio may need to be rechunked.
    int fixedSampleSize = sampleSizeBox.getFixedSampleSize();
    @Nullable String sampleMimeType = track.format.sampleMimeType;
    boolean rechunkFixedSizeSamples =
        fixedSampleSize != C.LENGTH_UNSET
            && (MimeTypes.AUDIO_RAW.equals(sampleMimeType)
                || MimeTypes.AUDIO_MLAW.equals(sampleMimeType)
                || MimeTypes.AUDIO_ALAW.equals(sampleMimeType))
            && remainingTimestampDeltaChanges == 0
            && remainingTimestampOffsetChanges == 0
            && remainingSynchronizationSamples == 0;

    long[] offsets;
    int[] sizes;
    int maximumSize = 0;
    long[] timestamps;
    int[] flags;
    long timestampTimeUnits = 0;
    long duration;

    if (rechunkFixedSizeSamples) {
      long[] chunkOffsetsBytes = new long[chunkIterator.length];
      int[] chunkSampleCounts = new int[chunkIterator.length];
      while (chunkIterator.moveNext()) {
        chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
        chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
      }
      FixedSampleSizeRechunker.Results rechunkedResults =
          FixedSampleSizeRechunker.rechunk(
              fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
      offsets = rechunkedResults.offsets;
      sizes = rechunkedResults.sizes;
      maximumSize = rechunkedResults.maximumSize;
      timestamps = rechunkedResults.timestamps;
      flags = rechunkedResults.flags;
      duration = rechunkedResults.duration;
    } else {
      offsets = new long[sampleCount];
      sizes = new int[sampleCount];
      timestamps = new long[sampleCount];
      flags = new int[sampleCount];
      long offset = 0;
      int remainingSamplesInChunk = 0;

      for (int i = 0; i < sampleCount; i++) {
        // Advance to the next chunk if necessary.
        boolean chunkDataComplete = true;
        while (remainingSamplesInChunk == 0 && (chunkDataComplete = chunkIterator.moveNext())) {
          offset = chunkIterator.offset;
          remainingSamplesInChunk = chunkIterator.numSamples;
        }
        if (!chunkDataComplete) {
          Log.w(TAG, "Unexpected end of chunk data");
          sampleCount = i;
          offsets = Arrays.copyOf(offsets, sampleCount);
          sizes = Arrays.copyOf(sizes, sampleCount);
          timestamps = Arrays.copyOf(timestamps, sampleCount);
          flags = Arrays.copyOf(flags, sampleCount);
          break;
        }

        // Add on the timestamp offset if ctts is present.
        if (ctts != null) {
          while (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {
            remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
            // The BMFF spec (ISO/IEC 14496-12) states that sample offsets should be unsigned
            // integers in version 0 ctts boxes, however some 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).
            timestampOffset = ctts.readInt();
            remainingTimestampOffsetChanges--;
          }
          remainingSamplesAtTimestampOffset--;
        }

        offsets[i] = offset;
        sizes[i] = sampleSizeBox.readNextSampleSize();
        if (sizes[i] > maximumSize) {
          maximumSize = sizes[i];
        }
        timestamps[i] = timestampTimeUnits + timestampOffset;

        // All samples are synchronization samples if the stss is not present.
        flags[i] = stss == null ? C.BUFFER_FLAG_KEY_FRAME : 0;
        if (i == nextSynchronizationSampleIndex) {
          flags[i] = C.BUFFER_FLAG_KEY_FRAME;
          remainingSynchronizationSamples--;
          if (remainingSynchronizationSamples > 0) {
            nextSynchronizationSampleIndex = checkNotNull(stss).readUnsignedIntToInt() - 1;
          }
        }

        // Add on the duration of this sample.
        timestampTimeUnits += timestampDeltaInTimeUnits;
        remainingSamplesAtTimestampDelta--;
        if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) {
          remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
          // The BMFF spec (ISO/IEC 14496-12) states that sample deltas should be unsigned integers
          // in stts boxes, however some streams violate the spec and use signed integers instead.
          // See https://github.com/google/ExoPlayer/issues/3384. It's safe to always decode sample
          // deltas 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
          // deltas are always small).
          timestampDeltaInTimeUnits = stts.readInt();
          remainingTimestampDeltaChanges--;
        }

        offset += sizes[i];
        remainingSamplesInChunk--;
      }
      duration = timestampTimeUnits + timestampOffset;

      // If the stbl's child boxes are not consistent the container is malformed, but the stream may
      // still be playable.
      boolean isCttsValid = true;
      if (ctts != null) {
        while (remainingTimestampOffsetChanges > 0) {
          if (ctts.readUnsignedIntToInt() != 0) {
            isCttsValid = false;
            break;
          }
          ctts.readInt(); // Ignore offset.
          remainingTimestampOffsetChanges--;
        }
      }
      if (remainingSynchronizationSamples != 0
          || remainingSamplesAtTimestampDelta != 0
          || remainingSamplesInChunk != 0
          || remainingTimestampDeltaChanges != 0
          || remainingSamplesAtTimestampOffset != 0
          || !isCttsValid) {
        Log.w(
            TAG,
            "Inconsistent stbl box for track "
                + track.id
                + ": remainingSynchronizationSamples "
                + remainingSynchronizationSamples
                + ", remainingSamplesAtTimestampDelta "
                + remainingSamplesAtTimestampDelta
                + ", remainingSamplesInChunk "
                + remainingSamplesInChunk
                + ", remainingTimestampDeltaChanges "
                + remainingTimestampDeltaChanges
                + ", remainingSamplesAtTimestampOffset "
                + remainingSamplesAtTimestampOffset
                + (!isCttsValid ? ", ctts invalid" : ""));
      }
    }
    long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale);

    if (track.editListDurations == null) {
      Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
      return new TrackSampleTable(
          track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
    }

    // See the BMFF spec (ISO/IEC 14496-12) subsection 8.6.6. Edit lists that require prerolling
    // from a sync sample after reordering are not supported. Partial audio sample truncation is
    // only supported in edit lists with one edit that removes less than
    // MAX_GAPLESS_TRIM_SIZE_SAMPLES samples from the start/end of the track. This implementation
    // handles simple discarding/delaying of samples. The extractor may place further restrictions
    // on what edited streams are playable.

    if (track.editListDurations.length == 1
        && track.type == C.TRACK_TYPE_AUDIO
        && timestamps.length >= 2) {
      long editStartTime = checkNotNull(track.editListMediaTimes)[0];
      long editEndTime =
          editStartTime
              + Util.scaleLargeTimestamp(
                  track.editListDurations[0], track.timescale, track.movieTimescale);
      if (canApplyEditWithGaplessInfo(timestamps, duration, editStartTime, editEndTime)) {
        long paddingTimeUnits = duration - editEndTime;
        long encoderDelay =
            Util.scaleLargeTimestamp(
                editStartTime - timestamps[0], track.format.sampleRate, track.timescale);
        long encoderPadding =
            Util.scaleLargeTimestamp(paddingTimeUnits, track.format.sampleRate, track.timescale);
        if ((encoderDelay != 0 || encoderPadding != 0)
            && encoderDelay <= Integer.MAX_VALUE
            && encoderPadding <= Integer.MAX_VALUE) {
          gaplessInfoHolder.encoderDelay = (int) encoderDelay;
          gaplessInfoHolder.encoderPadding = (int) encoderPadding;
          Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
          long editedDurationUs =
              Util.scaleLargeTimestamp(
                  track.editListDurations[0], C.MICROS_PER_SECOND, track.movieTimescale);
          return new TrackSampleTable(
              track, offsets, sizes, maximumSize, timestamps, flags, editedDurationUs);
        }
      }
    }

    if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) {
      // The current version of the spec leaves handling of an edit with zero segment_duration in
      // unfragmented files open to interpretation. We handle this as a special case and include all
      // samples in the edit.
      long editStartTime = checkNotNull(track.editListMediaTimes)[0];
      for (int i = 0; i < timestamps.length; i++) {
        timestamps[i] =
            Util.scaleLargeTimestamp(
                timestamps[i] - editStartTime, C.MICROS_PER_SECOND, track.timescale);
      }
      durationUs =
          Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale);
      return new TrackSampleTable(
          track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
    }

    // Omit any sample at the end point of an edit for audio tracks.
    boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO;

    // Count the number of samples after applying edits.
    int editedSampleCount = 0;
    int nextSampleIndex = 0;
    boolean copyMetadata = false;
    int[] startIndices = new int[track.editListDurations.length];
    int[] endIndices = new int[track.editListDurations.length];
    long[] editListMediaTimes = checkNotNull(track.editListMediaTimes);
    for (int i = 0; i < track.editListDurations.length; i++) {
      long editMediaTime = editListMediaTimes[i];
      if (editMediaTime != -1) {
        long editDuration =
            Util.scaleLargeTimestamp(
                track.editListDurations[i], track.timescale, track.movieTimescale);
        startIndices[i] =
            Util.binarySearchFloor(
                timestamps, editMediaTime, /* inclusive= */ true, /* stayInBounds= */ true);
        endIndices[i] =
            Util.binarySearchCeil(
                timestamps,
                editMediaTime + editDuration,
                /* inclusive= */ omitClippedSample,
                /* stayInBounds= */ false);
        while (startIndices[i] < endIndices[i]
            && (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) {
          // Applying the edit correctly would require prerolling from the previous sync sample. In
          // the current implementation we advance to the next sync sample instead. Only other
          // tracks (i.e. audio) will be rendered until the time of the first sync sample.
          // See https://github.com/google/ExoPlayer/issues/1659.
          startIndices[i]++;
        }
        editedSampleCount += endIndices[i] - startIndices[i];
        copyMetadata |= nextSampleIndex != startIndices[i];
        nextSampleIndex = endIndices[i];
      }
    }
    copyMetadata |= editedSampleCount != sampleCount;

    // Calculate edited sample timestamps and update the corresponding metadata arrays.
    long[] editedOffsets = copyMetadata ? new long[editedSampleCount] : offsets;
    int[] editedSizes = copyMetadata ? new int[editedSampleCount] : sizes;
    int editedMaximumSize = copyMetadata ? 0 : maximumSize;
    int[] editedFlags = copyMetadata ? new int[editedSampleCount] : flags;
    long[] editedTimestamps = new long[editedSampleCount];
    long pts = 0;
    int sampleIndex = 0;
    for (int i = 0; i < track.editListDurations.length; i++) {
      long editMediaTime = track.editListMediaTimes[i];
      int startIndex = startIndices[i];
      int endIndex = endIndices[i];
      if (copyMetadata) {
        int count = endIndex - startIndex;
        System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);
        System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count);
        System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count);
      }
      for (int j = startIndex; j < endIndex; j++) {
        long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
        long timeInSegmentUs =
            Util.scaleLargeTimestamp(
                max(0, timestamps[j] - editMediaTime), C.MICROS_PER_SECOND, track.timescale);
        editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
        if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
          editedMaximumSize = sizes[j];
        }
        sampleIndex++;
      }
      pts += track.editListDurations[i];
    }
    long editedDurationUs =
        Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
    return new TrackSampleTable(
        track,
        editedOffsets,
        editedSizes,
        editedMaximumSize,
        editedTimestamps,
        editedFlags,
        editedDurationUs);
  }

  @Nullable
  private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) {
    meta.skipBytes(Atom.HEADER_SIZE);
    maybeSkipRemainingMetaAtomHeaderBytes(meta);
    while (meta.getPosition() < limit) {
      int atomPosition = meta.getPosition();
      int atomSize = meta.readInt();
      int atomType = meta.readInt();
      if (atomType == Atom.TYPE_ilst) {
        meta.setPosition(atomPosition);
        return parseIlst(meta, atomPosition + atomSize);
      }
      meta.setPosition(atomPosition + atomSize);
    }
    return null;
  }

  @Nullable
  private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
    ilst.skipBytes(Atom.HEADER_SIZE);
    ArrayList<Metadata.Entry> entries = new ArrayList<>();
    while (ilst.getPosition() < limit) {
      @Nullable Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst);
      if (entry != null) {
        entries.add(entry);
      }
    }
    return entries.isEmpty() ? null : new Metadata(entries);
  }

  /**
   * Parses metadata from a Samsung smta atom.
   *
   * <p>See [Internal: b/150138465#comment76].
   */
  @Nullable
  private static Metadata parseSmta(ParsableByteArray smta, int limit) {
    smta.skipBytes(Atom.FULL_HEADER_SIZE);
    while (smta.getPosition() < limit) {
      int atomPosition = smta.getPosition();
      int atomSize = smta.readInt();
      int atomType = smta.readInt();
      if (atomType == Atom.TYPE_saut) {
        if (atomSize < 14) {
          return null;
        }
        smta.skipBytes(5); // author (4), reserved = 0 (1).
        int recordingMode = smta.readUnsignedByte();
        if (recordingMode != 12 && recordingMode != 13) {
          return null;
        }
        float captureFrameRate = recordingMode == 12 ? 240 : 120;
        smta.skipBytes(1); // reserved = 1 (1).
        int svcTemporalLayerCount = smta.readUnsignedByte();
        return new Metadata(new SmtaMetadataEntry(captureFrameRate, svcTemporalLayerCount));
      }
      smta.setPosition(atomPosition + atomSize);
    }
    return null;
  }

  /**
   * Parses a mvhd atom (defined in ISO/IEC 14496-12), returning the timescale for the movie.
   *
   * @param mvhd Contents of the mvhd atom to be parsed.
   * @return Timescale for the movie.
   */
  private static long parseMvhd(ParsableByteArray mvhd) {
    mvhd.setPosition(Atom.HEADER_SIZE);
    int fullAtom = mvhd.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);
    mvhd.skipBytes(version == 0 ? 8 : 16);
    return mvhd.readUnsignedInt();
  }

  /**
   * Parses a tkhd atom (defined in ISO/IEC 14496-12).
   *
   * @return An object containing the parsed data.
   */
  private static TkhdData parseTkhd(ParsableByteArray tkhd) {
    tkhd.setPosition(Atom.HEADER_SIZE);
    int fullAtom = tkhd.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);

    tkhd.skipBytes(version == 0 ? 8 : 16);
    int trackId = tkhd.readInt();

    tkhd.skipBytes(4);
    boolean durationUnknown = true;
    int durationPosition = tkhd.getPosition();
    int durationByteCount = version == 0 ? 4 : 8;
    for (int i = 0; i < durationByteCount; i++) {
      if (tkhd.getData()[durationPosition + i] != -1) {
        durationUnknown = false;
        break;
      }
    }
    long duration;
    if (durationUnknown) {
      tkhd.skipBytes(durationByteCount);
      duration = C.TIME_UNSET;
    } else {
      duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong();
      if (duration == 0) {
        // 0 duration normally indicates that the file is fully fragmented (i.e. all of the media
        // samples are in fragments). Treat as unknown.
        duration = C.TIME_UNSET;
      }
    }

    tkhd.skipBytes(16);
    int a00 = tkhd.readInt();
    int a01 = tkhd.readInt();
    tkhd.skipBytes(4);
    int a10 = tkhd.readInt();
    int a11 = tkhd.readInt();

    int rotationDegrees;
    int fixedOne = 65536;
    if (a00 == 0 && a01 == fixedOne && a10 == -fixedOne && a11 == 0) {
      rotationDegrees = 90;
    } else if (a00 == 0 && a01 == -fixedOne && a10 == fixedOne && a11 == 0) {
      rotationDegrees = 270;
    } else if (a00 == -fixedOne && a01 == 0 && a10 == 0 && a11 == -fixedOne) {
      rotationDegrees = 180;
    } else {
      // Only 0, 90, 180 and 270 are supported. Treat anything else as 0.
      rotationDegrees = 0;
    }

    return new TkhdData(trackId, duration, rotationDegrees);
  }

  /**
   * Parses an hdlr atom.
   *
   * @param hdlr The hdlr atom to decode.
   * @return The handler value.
   */
  private static int parseHdlr(ParsableByteArray hdlr) {
    hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
    return hdlr.readInt();
  }

  /** Returns the track type for a given handler value. */
  private static @C.TrackType int getTrackTypeForHdlr(int hdlr) {
    if (hdlr == TYPE_soun) {
      return C.TRACK_TYPE_AUDIO;
    } else if (hdlr == TYPE_vide) {
      return C.TRACK_TYPE_VIDEO;
    } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) {
      return C.TRACK_TYPE_TEXT;
    } else if (hdlr == TYPE_meta) {
      return C.TRACK_TYPE_METADATA;
    } else {
      return C.TRACK_TYPE_UNKNOWN;
    }
  }

  /**
   * Parses an mdhd atom (defined in ISO/IEC 14496-12).
   *
   * @param mdhd The mdhd atom to decode.
   * @return A pair consisting of the media timescale defined as the number of time units that pass
   *     in one second, and the language code.
   */
  private static Pair<Long, String> parseMdhd(ParsableByteArray mdhd) {
    mdhd.setPosition(Atom.HEADER_SIZE);
    int fullAtom = mdhd.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);
    mdhd.skipBytes(version == 0 ? 8 : 16);
    long timescale = mdhd.readUnsignedInt();
    mdhd.skipBytes(version == 0 ? 4 : 8);
    int languageCode = mdhd.readUnsignedShort();
    String language =
        ""
            + (char) (((languageCode >> 10) & 0x1F) + 0x60)
            + (char) (((languageCode >> 5) & 0x1F) + 0x60)
            + (char) ((languageCode & 0x1F) + 0x60);
    return Pair.create(timescale, language);
  }

  /**
   * Parses a stsd atom (defined in ISO/IEC 14496-12).
   *
   * @param stsd The stsd atom to decode.
   * @param trackId The track's identifier in its container.
   * @param rotationDegrees The rotation of the track in degrees.
   * @param language The language of the track.
   * @param drmInitData {@link DrmInitData} to be included in the format, or {@code null}.
   * @param isQuickTime True for QuickTime media. False otherwise.
   * @return An object containing the parsed data.
   */
  private static StsdData parseStsd(
      ParsableByteArray stsd,
      int trackId,
      int rotationDegrees,
      String language,
      @Nullable DrmInitData drmInitData,
      boolean isQuickTime)
      throws ParserException {
    stsd.setPosition(Atom.FULL_HEADER_SIZE);
    int numberOfEntries = stsd.readInt();
    StsdData out = new StsdData(numberOfEntries);
    for (int i = 0; i < numberOfEntries; i++) {
      int childStartPosition = stsd.getPosition();
      int childAtomSize = stsd.readInt();
      ExtractorUtil.checkContainerInput(childAtomSize > 0, "childAtomSize must be positive");
      int childAtomType = stsd.readInt();
      if (childAtomType == Atom.TYPE_avc1
          || childAtomType == Atom.TYPE_avc3
          || childAtomType == Atom.TYPE_encv
          || childAtomType == Atom.TYPE_m1v_
          || childAtomType == Atom.TYPE_mp4v
          || childAtomType == Atom.TYPE_hvc1
          || childAtomType == Atom.TYPE_hev1
          || childAtomType == Atom.TYPE_s263
          || childAtomType == Atom.TYPE_H263
          || childAtomType == Atom.TYPE_vp08
          || childAtomType == Atom.TYPE_vp09
          || childAtomType == Atom.TYPE_av01
          || childAtomType == Atom.TYPE_dvav
          || childAtomType == Atom.TYPE_dva1
          || childAtomType == Atom.TYPE_dvhe
          || childAtomType == Atom.TYPE_dvh1) {
        parseVideoSampleEntry(
            stsd,
            childAtomType,
            childStartPosition,
            childAtomSize,
            trackId,
            rotationDegrees,
            drmInitData,
            out,
            i);
      } else if (childAtomType == Atom.TYPE_mp4a
          || childAtomType == Atom.TYPE_enca
          || childAtomType == Atom.TYPE_ac_3
          || childAtomType == Atom.TYPE_ec_3
          || childAtomType == Atom.TYPE_ac_4
          || childAtomType == Atom.TYPE_mlpa
          || childAtomType == Atom.TYPE_dtsc
          || childAtomType == Atom.TYPE_dtse
          || childAtomType == Atom.TYPE_dtsh
          || childAtomType == Atom.TYPE_dtsl
          || childAtomType == Atom.TYPE_dtsx
          || childAtomType == Atom.TYPE_samr
          || childAtomType == Atom.TYPE_sawb
          || childAtomType == Atom.TYPE_lpcm
          || childAtomType == Atom.TYPE_sowt
          || childAtomType == Atom.TYPE_twos
          || childAtomType == Atom.TYPE__mp2
          || childAtomType == Atom.TYPE__mp3
          || childAtomType == Atom.TYPE_mha1
          || childAtomType == Atom.TYPE_mhm1
          || childAtomType == Atom.TYPE_alac
          || childAtomType == Atom.TYPE_alaw
          || childAtomType == Atom.TYPE_ulaw
          || childAtomType == Atom.TYPE_Opus
          || childAtomType == Atom.TYPE_fLaC) {
        parseAudioSampleEntry(
            stsd,
            childAtomType,
            childStartPosition,
            childAtomSize,
            trackId,
            language,
            isQuickTime,
            drmInitData,
            out,
            i);
      } else if (childAtomType == Atom.TYPE_TTML
          || childAtomType == Atom.TYPE_tx3g
          || childAtomType == Atom.TYPE_wvtt
          || childAtomType == Atom.TYPE_stpp
          || childAtomType == Atom.TYPE_c608) {
        parseTextSampleEntry(
            stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, out);
      } else if (childAtomType == Atom.TYPE_mett) {
        parseMetaDataSampleEntry(stsd, childAtomType, childStartPosition, trackId, out);
      } else if (childAtomType == Atom.TYPE_camm) {
        out.format =
            new Format.Builder()
                .setId(trackId)
                .setSampleMimeType(MimeTypes.APPLICATION_CAMERA_MOTION)
                .build();
      }
      stsd.setPosition(childStartPosition + childAtomSize);
    }
    return out;
  }

  private static void parseTextSampleEntry(
      ParsableByteArray parent,
      int atomType,
      int position,
      int atomSize,
      int trackId,
      String language,
      StsdData out) {
    parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);

    // Default values.
    @Nullable ImmutableList<byte[]> initializationData = null;
    long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE;

    String mimeType;
    if (atomType == Atom.TYPE_TTML) {
      mimeType = MimeTypes.APPLICATION_TTML;
    } else if (atomType == Atom.TYPE_tx3g) {
      mimeType = MimeTypes.APPLICATION_TX3G;
      int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8;
      byte[] sampleDescriptionData = new byte[sampleDescriptionLength];
      parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength);
      initializationData = ImmutableList.of(sampleDescriptionData);
    } else if (atomType == Atom.TYPE_wvtt) {
      mimeType = MimeTypes.APPLICATION_MP4VTT;
    } else if (atomType == Atom.TYPE_stpp) {
      mimeType = MimeTypes.APPLICATION_TTML;
      subsampleOffsetUs = 0; // Subsample timing is absolute.
    } else if (atomType == Atom.TYPE_c608) {
      // Defined by the QuickTime File Format specification.
      mimeType = MimeTypes.APPLICATION_MP4CEA608;
      out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT;
    } else {
      // Never happens.
      throw new IllegalStateException();
    }

    out.format =
        new Format.Builder()
            .setId(trackId)
            .setSampleMimeType(mimeType)
            .setLanguage(language)
            .setSubsampleOffsetUs(subsampleOffsetUs)
            .setInitializationData(initializationData)
            .build();
  }

  // hdrStaticInfo is allocated using allocate() in allocateHdrStaticInfo().
  @SuppressWarnings("ByteBufferBackingArray")
  private static void parseVideoSampleEntry(
      ParsableByteArray parent,
      int atomType,
      int position,
      int size,
      int trackId,
      int rotationDegrees,
      @Nullable DrmInitData drmInitData,
      StsdData out,
      int entryIndex)
      throws ParserException {
    parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);

    parent.skipBytes(16);
    int width = parent.readUnsignedShort();
    int height = parent.readUnsignedShort();
    boolean pixelWidthHeightRatioFromPasp = false;
    float pixelWidthHeightRatio = 1;
    parent.skipBytes(50);

    int childPosition = parent.getPosition();
    if (atomType == Atom.TYPE_encv) {
      @Nullable
      Pair<Integer, TrackEncryptionBox> sampleEntryEncryptionData =
          parseSampleEntryEncryptionData(parent, position, size);
      if (sampleEntryEncryptionData != null) {
        atomType = sampleEntryEncryptionData.first;
        drmInitData =
            drmInitData == null
                ? null
                : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);
        out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;
      }
      parent.setPosition(childPosition);
    }
    // TODO: Uncomment when [Internal: b/63092960] is fixed.
    // else {
    //   drmInitData = null;
    // }

    @Nullable String mimeType = null;
    if (atomType == Atom.TYPE_m1v_) {
      mimeType = MimeTypes.VIDEO_MPEG;
    } else if (atomType == Atom.TYPE_H263) {
      mimeType = MimeTypes.VIDEO_H263;
    }

    @Nullable List<byte[]> initializationData = null;
    @Nullable String codecs = null;
    @Nullable byte[] projectionData = null;
    @C.StereoMode int stereoMode = Format.NO_VALUE;
    @Nullable EsdsData esdsData = null;

    // HDR related metadata.
    @C.ColorSpace int colorSpace = Format.NO_VALUE;
    @C.ColorRange int colorRange = Format.NO_VALUE;
    @C.ColorTransfer int colorTransfer = Format.NO_VALUE;
    // The format of HDR static info is defined in CTA-861-G:2017, Table 45.
    @Nullable ByteBuffer hdrStaticInfo = null;

    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childStartPosition = parent.getPosition();
      int childAtomSize = parent.readInt();
      if (childAtomSize == 0 && parent.getPosition() - position == size) {
        // Handle optional terminating four zero bytes in MOV files.
        break;
      }
      ExtractorUtil.checkContainerInput(childAtomSize > 0, "childAtomSize must be positive");
      int childAtomType = parent.readInt();
      if (childAtomType == Atom.TYPE_avcC) {
        ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null);
        mimeType = MimeTypes.VIDEO_H264;
        parent.setPosition(childStartPosition + Atom.HEADER_SIZE);
        AvcConfig avcConfig = AvcConfig.parse(parent);
        initializationData = avcConfig.initializationData;
        out.nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
        if (!pixelWidthHeightRatioFromPasp) {
          pixelWidthHeightRatio = avcConfig.pixelWidthHeightRatio;
        }
        codecs = avcConfig.codecs;
      } else if (childAtomType == Atom.TYPE_hvcC) {
        ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null);
        mimeType = MimeTypes.VIDEO_H265;
        parent.setPosition(childStartPosition + Atom.HEADER_SIZE);
        HevcConfig hevcConfig = HevcConfig.parse(parent);
        initializationData = hevcConfig.initializationData;
        out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength;
        if (!pixelWidthHeightRatioFromPasp) {
          pixelWidthHeightRatio = hevcConfig.pixelWidthHeightRatio;
        }
        codecs = hevcConfig.codecs;
      } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) {
        @Nullable DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent);
        if (dolbyVisionConfig != null) {
          codecs = dolbyVisionConfig.codecs;
          mimeType = MimeTypes.VIDEO_DOLBY_VISION;
        }
      } else if (childAtomType == Atom.TYPE_vpcC) {
        ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null);
        mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9;
      } else if (childAtomType == Atom.TYPE_av1C) {
        ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null);
        mimeType = MimeTypes.VIDEO_AV1;
      } else if (childAtomType == Atom.TYPE_clli) {
        if (hdrStaticInfo == null) {
          hdrStaticInfo = allocateHdrStaticInfo();
        }
        // The contents of the clli box occupy the last 4 bytes of the HDR static info array. Note
        // that each field is read in big endian and written in little endian.
        hdrStaticInfo.position(21);
        hdrStaticInfo.putShort(parent.readShort()); // max_content_light_level.
        hdrStaticInfo.putShort(parent.readShort()); // max_pic_average_light_level.
      } else if (childAtomType == Atom.TYPE_mdcv) {
        if (hdrStaticInfo == null) {
          hdrStaticInfo = allocateHdrStaticInfo();
        }
        // The contents of the mdcv box occupy 20 bytes after the first byte of the HDR static info
        // array. Note that each field is read in big endian and written in little endian.
        short displayPrimariesGX = parent.readShort();
        short displayPrimariesGY = parent.readShort();
        short displayPrimariesBX = parent.readShort();
        short displayPrimariesBY = parent.readShort();
        short displayPrimariesRX = parent.readShort();
        short displayPrimariesRY = parent.readShort();
        short whitePointX = parent.readShort();
        short whitePointY = parent.readShort();
        long maxDisplayMasteringLuminance = parent.readUnsignedInt();
        long minDisplayMasteringLuminance = parent.readUnsignedInt();

        hdrStaticInfo.position(1);
        hdrStaticInfo.putShort(displayPrimariesRX);
        hdrStaticInfo.putShort(displayPrimariesRY);
        hdrStaticInfo.putShort(displayPrimariesGX);
        hdrStaticInfo.putShort(displayPrimariesGY);
        hdrStaticInfo.putShort(displayPrimariesBX);
        hdrStaticInfo.putShort(displayPrimariesBY);
        hdrStaticInfo.putShort(whitePointX);
        hdrStaticInfo.putShort(whitePointY);
        hdrStaticInfo.putShort((short) (maxDisplayMasteringLuminance / 10000));
        hdrStaticInfo.putShort((short) (minDisplayMasteringLuminance / 10000));
      } else if (childAtomType == Atom.TYPE_d263) {
        ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null);
        mimeType = MimeTypes.VIDEO_H263;
      } else if (childAtomType == Atom.TYPE_esds) {
        ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null);
        esdsData = parseEsdsFromParent(parent, childStartPosition);
        mimeType = esdsData.mimeType;
        @Nullable byte[] initializationDataBytes = esdsData.initializationData;
        if (initializationDataBytes != null) {
          initializationData = ImmutableList.of(initializationDataBytes);
        }
      } else if (childAtomType == Atom.TYPE_pasp) {
        pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition);
        pixelWidthHeightRatioFromPasp = true;
      } else if (childAtomType == Atom.TYPE_sv3d) {
        projectionData = parseProjFromParent(parent, childStartPosition, childAtomSize);
      } else if (childAtomType == Atom.TYPE_st3d) {
        int version = parent.readUnsignedByte();
        parent.skipBytes(3); // Flags.
        if (version == 0) {
          int layout = parent.readUnsignedByte();
          switch (layout) {
            case 0:
              stereoMode = C.STEREO_MODE_MONO;
              break;
            case 1:
              stereoMode = C.STEREO_MODE_TOP_BOTTOM;
              break;
            case 2:
              stereoMode = C.STEREO_MODE_LEFT_RIGHT;
              break;
            case 3:
              stereoMode = C.STEREO_MODE_STEREO_MESH;
              break;
            default:
              break;
          }
        }
      } else if (childAtomType == Atom.TYPE_colr) {
        int colorType = parent.readInt();
        if (colorType == TYPE_nclx || colorType == TYPE_nclc) {
          // For more info on syntax, see Section 8.5.2.2 in ISO/IEC 14496-12:2012(E) and
          // https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html.
          int colorPrimaries = parent.readUnsignedShort();
          int transferCharacteristics = parent.readUnsignedShort();
          parent.skipBytes(2); // matrix_coefficients.

          // Only try and read full_range_flag if the box is long enough. It should be present in
          // all colr boxes with type=nclx (Section 8.5.2.2 in ISO/IEC 14496-12:2012(E)) but some
          // device cameras record videos with type=nclx without this final flag (and therefore
          // size=18): https://github.com/google/ExoPlayer/issues/9332
          boolean fullRangeFlag =
              childAtomSize == 19 && (parent.readUnsignedByte() & 0b10000000) != 0;
          colorSpace = ColorInfo.isoColorPrimariesToColorSpace(colorPrimaries);
          colorRange = fullRangeFlag ? C.COLOR_RANGE_FULL : C.COLOR_RANGE_LIMITED;
          colorTransfer =
              ColorInfo.isoTransferCharacteristicsToColorTransfer(transferCharacteristics);
        } else {
          Log.w(TAG, "Unsupported color type: " + Atom.getAtomTypeString(colorType));
        }
      }
      childPosition += childAtomSize;
    }

    // If the media type was not recognized, ignore the track.
    if (mimeType == null) {
      return;
    }

    Format.Builder formatBuilder =
        new Format.Builder()
            .setId(trackId)
            .setSampleMimeType(mimeType)
            .setCodecs(codecs)
            .setWidth(width)
            .setHeight(height)
            .setPixelWidthHeightRatio(pixelWidthHeightRatio)
            .setRotationDegrees(rotationDegrees)
            .setProjectionData(projectionData)
            .setStereoMode(stereoMode)
            .setInitializationData(initializationData)
            .setDrmInitData(drmInitData);
    if (colorSpace != Format.NO_VALUE
        || colorRange != Format.NO_VALUE
        || colorTransfer != Format.NO_VALUE
        || hdrStaticInfo != null) {
      // Note that if either mdcv or clli are missing, we leave the corresponding HDR static
      // metadata bytes with value zero. See [Internal ref: b/194535665].
      formatBuilder.setColorInfo(
          new ColorInfo(
              colorSpace,
              colorRange,
              colorTransfer,
              hdrStaticInfo != null ? hdrStaticInfo.array() : null));
    }

    if (esdsData != null) {
      formatBuilder
          .setAverageBitrate(Ints.saturatedCast(esdsData.bitrate))
          .setPeakBitrate(Ints.saturatedCast(esdsData.peakBitrate));
    }

    out.format = formatBuilder.build();
  }

  private static ByteBuffer allocateHdrStaticInfo() {
    // For HDR static info, Android decoders expect a 25-byte array. The first byte is zero to
    // represent Static Metadata Type 1, as per CTA-861-G:2017, Table 44. The following 24 bytes
    // follow CTA-861-G:2017, Table 45.
    return ByteBuffer.allocate(25).order(ByteOrder.LITTLE_ENDIAN);
  }

  private static void parseMetaDataSampleEntry(
      ParsableByteArray parent, int atomType, int position, int trackId, StsdData out) {
    parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
    if (atomType == Atom.TYPE_mett) {
      parent.readNullTerminatedString(); // Skip optional content_encoding
      @Nullable String mimeType = parent.readNullTerminatedString();
      if (mimeType != null) {
        out.format = new Format.Builder().setId(trackId).setSampleMimeType(mimeType).build();
      }
    }
  }

  /**
   * Parses the edts atom (defined in ISO/IEC 14496-12 subsection 8.6.5).
   *
   * @param edtsAtom edts (edit box) atom to decode.
   * @return Pair of edit list durations and edit list media times, or {@code null} if they are not
   *     present.
   */
  @Nullable
  private static Pair<long[], long[]> parseEdts(Atom.ContainerAtom edtsAtom) {
    @Nullable Atom.LeafAtom elstAtom = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst);
    if (elstAtom == null) {
      return null;
    }
    ParsableByteArray elstData = elstAtom.data;
    elstData.setPosition(Atom.HEADER_SIZE);
    int fullAtom = elstData.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);
    int entryCount = elstData.readUnsignedIntToInt();
    long[] editListDurations = new long[entryCount];
    long[] editListMediaTimes = new long[entryCount];
    for (int i = 0; i < entryCount; i++) {
      editListDurations[i] =
          version == 1 ? elstData.readUnsignedLongToLong() : elstData.readUnsignedInt();
      editListMediaTimes[i] = version == 1 ? elstData.readLong() : elstData.readInt();
      int mediaRateInteger = elstData.readShort();
      if (mediaRateInteger != 1) {
        // The extractor does not handle dwell edits (mediaRateInteger == 0).
        throw new IllegalArgumentException("Unsupported media rate.");
      }
      elstData.skipBytes(2);
    }
    return Pair.create(editListDurations, editListMediaTimes);
  }

  private static float parsePaspFromParent(ParsableByteArray parent, int position) {
    parent.setPosition(position + Atom.HEADER_SIZE);
    int hSpacing = parent.readUnsignedIntToInt();
    int vSpacing = parent.readUnsignedIntToInt();
    return (float) hSpacing / vSpacing;
  }

  private static void parseAudioSampleEntry(
      ParsableByteArray parent,
      int atomType,
      int position,
      int size,
      int trackId,
      String language,
      boolean isQuickTime,
      @Nullable DrmInitData drmInitData,
      StsdData out,
      int entryIndex)
      throws ParserException {
    parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);

    int quickTimeSoundDescriptionVersion = 0;
    if (isQuickTime) {
      quickTimeSoundDescriptionVersion = parent.readUnsignedShort();
      parent.skipBytes(6);
    } else {
      parent.skipBytes(8);
    }

    int channelCount;
    int sampleRate;
    int sampleRateMlp = 0;
    @C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
    @Nullable String codecs = null;
    @Nullable EsdsData esdsData = null;

    if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
      channelCount = parent.readUnsignedShort();
      parent.skipBytes(6); // sampleSize, compressionId, packetSize.

      sampleRate = parent.readUnsignedFixedPoint1616();
      // The sample rate has been redefined as a 32-bit value for Dolby TrueHD (MLP) streams.
      parent.setPosition(parent.getPosition() - 4);
      sampleRateMlp = parent.readInt();

      if (quickTimeSoundDescriptionVersion == 1) {
        parent.skipBytes(16);
      }
    } else if (quickTimeSoundDescriptionVersion == 2) {
      parent.skipBytes(16); // always[3,16,Minus2,0,65536], sizeOfStructOnly

      sampleRate = (int) Math.round(parent.readDouble());
      channelCount = parent.readUnsignedIntToInt();

      // Skip always7F000000, sampleSize, formatSpecificFlags, constBytesPerAudioPacket,
      // constLPCMFramesPerAudioPacket.
      parent.skipBytes(20);
    } else {
      // Unsupported version.
      return;
    }

    int childPosition = parent.getPosition();
    if (atomType == Atom.TYPE_enca) {
      @Nullable
      Pair<Integer, TrackEncryptionBox> sampleEntryEncryptionData =
          parseSampleEntryEncryptionData(parent, position, size);
      if (sampleEntryEncryptionData != null) {
        atomType = sampleEntryEncryptionData.first;
        drmInitData =
            drmInitData == null
                ? null
                : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);
        out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;
      }
      parent.setPosition(childPosition);
    }
    // TODO: Uncomment when [Internal: b/63092960] is fixed.
    // else {
    //   drmInitData = null;
    // }

    // If the atom type determines a MIME type, set it immediately.
    @Nullable String mimeType = null;
    if (atomType == Atom.TYPE_ac_3) {
      mimeType = MimeTypes.AUDIO_AC3;
    } else if (atomType == Atom.TYPE_ec_3) {
      mimeType = MimeTypes.AUDIO_E_AC3;
    } else if (atomType == Atom.TYPE_ac_4) {
      mimeType = MimeTypes.AUDIO_AC4;
    } else if (atomType == Atom.TYPE_dtsc) {
      mimeType = MimeTypes.AUDIO_DTS;
    } else if (atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) {
      mimeType = MimeTypes.AUDIO_DTS_HD;
    } else if (atomType == Atom.TYPE_dtse) {
      mimeType = MimeTypes.AUDIO_DTS_EXPRESS;
    } else if (atomType == Atom.TYPE_dtsx) {
      mimeType = MimeTypes.AUDIO_DTS_X;
    } else if (atomType == Atom.TYPE_samr) {
      mimeType = MimeTypes.AUDIO_AMR_NB;
    } else if (atomType == Atom.TYPE_sawb) {
      mimeType = MimeTypes.AUDIO_AMR_WB;
    } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) {
      mimeType = MimeTypes.AUDIO_RAW;
      pcmEncoding = C.ENCODING_PCM_16BIT;
    } else if (atomType == Atom.TYPE_twos) {
      mimeType = MimeTypes.AUDIO_RAW;
      pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
    } else if (atomType == Atom.TYPE__mp2 || atomType == Atom.TYPE__mp3) {
      mimeType = MimeTypes.AUDIO_MPEG;
    } else if (atomType == Atom.TYPE_mha1) {
      mimeType = MimeTypes.AUDIO_MPEGH_MHA1;
    } else if (atomType == Atom.TYPE_mhm1) {
      mimeType = MimeTypes.AUDIO_MPEGH_MHM1;
    } else if (atomType == Atom.TYPE_alac) {
      mimeType = MimeTypes.AUDIO_ALAC;
    } else if (atomType == Atom.TYPE_alaw) {
      mimeType = MimeTypes.AUDIO_ALAW;
    } else if (atomType == Atom.TYPE_ulaw) {
      mimeType = MimeTypes.AUDIO_MLAW;
    } else if (atomType == Atom.TYPE_Opus) {
      mimeType = MimeTypes.AUDIO_OPUS;
    } else if (atomType == Atom.TYPE_fLaC) {
      mimeType = MimeTypes.AUDIO_FLAC;
    } else if (atomType == Atom.TYPE_mlpa) {
      mimeType = MimeTypes.AUDIO_TRUEHD;
    }

    @Nullable List<byte[]> initializationData = null;
    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childAtomSize = parent.readInt();
      ExtractorUtil.checkContainerInput(childAtomSize > 0, "childAtomSize must be positive");
      int childAtomType = parent.readInt();
      if (childAtomType == Atom.TYPE_mhaC) {
        // See ISO_IEC_23008-3;2019 MHADecoderConfigurationRecord
        // The header consists of: size (4), boxtype 'mhaC' (4), configurationVersion (1),
        // mpegh3daProfileLevelIndication (1), referenceChannelLayout (1), mpegh3daConfigLength (2).
        int mhacHeaderSize = 13;
        int childAtomBodySize = childAtomSize - mhacHeaderSize;
        byte[] initializationDataBytes = new byte[childAtomBodySize];
        parent.setPosition(childPosition + mhacHeaderSize);
        parent.readBytes(initializationDataBytes, 0, childAtomBodySize);
        initializationData = ImmutableList.of(initializationDataBytes);
      } else if (childAtomType == Atom.TYPE_esds
          || (isQuickTime && childAtomType == Atom.TYPE_wave)) {
        int esdsAtomPosition =
            childAtomType == Atom.TYPE_esds
                ? childPosition
                : findBoxPosition(parent, Atom.TYPE_esds, childPosition, childAtomSize);
        if (esdsAtomPosition != C.POSITION_UNSET) {
          esdsData = parseEsdsFromParent(parent, esdsAtomPosition);
          mimeType = esdsData.mimeType;
          @Nullable byte[] initializationDataBytes = esdsData.initializationData;
          if (initializationDataBytes != null) {
            if (MimeTypes.AUDIO_AAC.equals(mimeType)) {
              // Update sampleRate and channelCount from the AudioSpecificConfig initialization
              // data, which is more reliable. See [Internal: b/10903778].
              AacUtil.Config aacConfig = AacUtil.parseAudioSpecificConfig(initializationDataBytes);
              sampleRate = aacConfig.sampleRateHz;
              channelCount = aacConfig.channelCount;
              codecs = aacConfig.codecs;
            }
            initializationData = ImmutableList.of(initializationDataBytes);
          }
        }
      } else if (childAtomType == Atom.TYPE_dac3) {
        parent.setPosition(Atom.HEADER_SIZE + childPosition);
        out.format =
            Ac3Util.parseAc3AnnexFFormat(parent, Integer.toString(trackId), language, drmInitData);
      } else if (childAtomType == Atom.TYPE_dec3) {
        parent.setPosition(Atom.HEADER_SIZE + childPosition);
        out.format =
            Ac3Util.parseEAc3AnnexFFormat(parent, Integer.toString(trackId), language, drmInitData);
      } else if (childAtomType == Atom.TYPE_dac4) {
        parent.setPosition(Atom.HEADER_SIZE + childPosition);
        out.format =
            Ac4Util.parseAc4AnnexEFormat(parent, Integer.toString(trackId), language, drmInitData);
      } else if (childAtomType == Atom.TYPE_dmlp) {
        if (sampleRateMlp <= 0) {
          throw ParserException.createForMalformedContainer(
              "Invalid sample rate for Dolby TrueHD MLP stream: " + sampleRateMlp,
              /* cause= */ null);
        }
        sampleRate = sampleRateMlp;
        // The channel count from the sample entry must be ignored for Dolby TrueHD (MLP) streams
        // because these streams can carry simultaneously multiple representations of the same
        // audio. Use stereo by default.
        channelCount = 2;
      } else if (childAtomType == Atom.TYPE_ddts) {
        out.format =
            new Format.Builder()
                .setId(trackId)
                .setSampleMimeType(mimeType)
                .setChannelCount(channelCount)
                .setSampleRate(sampleRate)
                .setDrmInitData(drmInitData)
                .setLanguage(language)
                .build();
      } else if (childAtomType == Atom.TYPE_dOps) {
        // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic
        // Signature and the body of the dOps atom.
        int childAtomBodySize = childAtomSize - Atom.HEADER_SIZE;
        byte[] headerBytes = Arrays.copyOf(opusMagic, opusMagic.length + childAtomBodySize);
        parent.setPosition(childPosition + Atom.HEADER_SIZE);
        parent.readBytes(headerBytes, opusMagic.length, childAtomBodySize);
        initializationData = OpusUtil.buildInitializationData(headerBytes);
      } else if (childAtomType == Atom.TYPE_dfLa) {
        int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE;
        byte[] initializationDataBytes = new byte[4 + childAtomBodySize];
        initializationDataBytes[0] = 0x66; // f
        initializationDataBytes[1] = 0x4C; // L
        initializationDataBytes[2] = 0x61; // a
        initializationDataBytes[3] = 0x43; // C
        parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);
        parent.readBytes(initializationDataBytes, /* offset= */ 4, childAtomBodySize);
        initializationData = ImmutableList.of(initializationDataBytes);
      } else if (childAtomType == Atom.TYPE_alac) {
        int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE;
        byte[] initializationDataBytes = new byte[childAtomBodySize];
        parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);
        parent.readBytes(initializationDataBytes, /* offset= */ 0, childAtomBodySize);
        // Update sampleRate and channelCount from the AudioSpecificConfig initialization data,
        // which is more reliable. See https://github.com/google/ExoPlayer/pull/6629.
        Pair<Integer, Integer> audioSpecificConfig =
            CodecSpecificDataUtil.parseAlacAudioSpecificConfig(initializationDataBytes);
        sampleRate = audioSpecificConfig.first;
        channelCount = audioSpecificConfig.second;
        initializationData = ImmutableList.of(initializationDataBytes);
      }
      childPosition += childAtomSize;
    }

    if (out.format == null && mimeType != null) {
      Format.Builder formatBuilder =
          new Format.Builder()
              .setId(trackId)
              .setSampleMimeType(mimeType)
              .setCodecs(codecs)
              .setChannelCount(channelCount)
              .setSampleRate(sampleRate)
              .setPcmEncoding(pcmEncoding)
              .setInitializationData(initializationData)
              .setDrmInitData(drmInitData)
              .setLanguage(language);

      if (esdsData != null) {
        formatBuilder
            .setAverageBitrate(Ints.saturatedCast(esdsData.bitrate))
            .setPeakBitrate(Ints.saturatedCast(esdsData.peakBitrate));
      }

      out.format = formatBuilder.build();
    }
  }

  /**
   * Returns the position of the first box with the given {@code boxType} within {@code parent}, or
   * {@link C#POSITION_UNSET} if no such box is found.
   *
   * @param parent The {@link ParsableByteArray} to search. The search will start from the {@link
   *     ParsableByteArray#getPosition() current position}.
   * @param boxType The box type to search for.
   * @param parentBoxPosition The position in {@code parent} of the box we are searching.
   * @param parentBoxSize The size of the parent box we are searching in bytes.
   * @return The position of the first box with the given {@code boxType} within {@code parent}, or
   *     {@link C#POSITION_UNSET} if no such box is found.
   */
  private static int findBoxPosition(
      ParsableByteArray parent, int boxType, int parentBoxPosition, int parentBoxSize)
      throws ParserException {
    int childAtomPosition = parent.getPosition();
    ExtractorUtil.checkContainerInput(childAtomPosition >= parentBoxPosition, /* message= */ null);
    while (childAtomPosition - parentBoxPosition < parentBoxSize) {
      parent.setPosition(childAtomPosition);
      int childAtomSize = parent.readInt();
      ExtractorUtil.checkContainerInput(childAtomSize > 0, "childAtomSize must be positive");
      int childType = parent.readInt();
      if (childType == boxType) {
        return childAtomPosition;
      }
      childAtomPosition += childAtomSize;
    }
    return C.POSITION_UNSET;
  }

  /** Returns codec-specific initialization data contained in an esds box. */
  private static EsdsData parseEsdsFromParent(ParsableByteArray parent, int position) {
    parent.setPosition(position + Atom.HEADER_SIZE + 4);
    // Start of the ES_Descriptor (defined in ISO/IEC 14496-1)
    parent.skipBytes(1); // ES_Descriptor tag
    parseExpandableClassSize(parent);
    parent.skipBytes(2); // ES_ID

    int flags = parent.readUnsignedByte();
    if ((flags & 0x80 /* streamDependenceFlag */) != 0) {
      parent.skipBytes(2);
    }
    if ((flags & 0x40 /* URL_Flag */) != 0) {
      parent.skipBytes(parent.readUnsignedByte());
    }
    if ((flags & 0x20 /* OCRstreamFlag */) != 0) {
      parent.skipBytes(2);
    }

    // Start of the DecoderConfigDescriptor (defined in ISO/IEC 14496-1)
    parent.skipBytes(1); // DecoderConfigDescriptor tag
    parseExpandableClassSize(parent);

    // Set the MIME type based on the object type indication (ISO/IEC 14496-1 table 5).
    int objectTypeIndication = parent.readUnsignedByte();
    @Nullable String mimeType = getMimeTypeFromMp4ObjectType(objectTypeIndication);
    if (MimeTypes.AUDIO_MPEG.equals(mimeType)
        || MimeTypes.AUDIO_DTS.equals(mimeType)
        || MimeTypes.AUDIO_DTS_HD.equals(mimeType)) {
      return new EsdsData(
          mimeType,
          /* initializationData= */ null,
          /* bitrate= */ Format.NO_VALUE,
          /* peakBitrate= */ Format.NO_VALUE);
    }

    parent.skipBytes(4);
    long peakBitrate = parent.readUnsignedInt();
    long bitrate = parent.readUnsignedInt();

    // Start of the DecoderSpecificInfo.
    parent.skipBytes(1); // DecoderSpecificInfo tag
    int initializationDataSize = parseExpandableClassSize(parent);
    byte[] initializationData = new byte[initializationDataSize];
    parent.readBytes(initializationData, 0, initializationDataSize);

    // Skipping zero values as unknown.
    return new EsdsData(
        mimeType,
        /* initializationData= */ initializationData,
        /* bitrate= */ bitrate > 0 ? bitrate : Format.NO_VALUE,
        /* peakBitrate= */ peakBitrate > 0 ? peakBitrate : Format.NO_VALUE);
  }

  /**
   * Parses encryption data from an audio/video sample entry, returning a pair consisting of the
   * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common
   * encryption sinf atom was present.
   */
  @Nullable
  private static Pair<Integer, TrackEncryptionBox> parseSampleEntryEncryptionData(
      ParsableByteArray parent, int position, int size) throws ParserException {
    int childPosition = parent.getPosition();
    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childAtomSize = parent.readInt();
      ExtractorUtil.checkContainerInput(childAtomSize > 0, "childAtomSize must be positive");
      int childAtomType = parent.readInt();
      if (childAtomType == Atom.TYPE_sinf) {
        @Nullable
        Pair<Integer, TrackEncryptionBox> result =
            parseCommonEncryptionSinfFromParent(parent, childPosition, childAtomSize);
        if (result != null) {
          return result;
        }
      }
      childPosition += childAtomSize;
    }
    return null;
  }

  @Nullable
  /* package */ static Pair<Integer, TrackEncryptionBox> parseCommonEncryptionSinfFromParent(
      ParsableByteArray parent, int position, int size) throws ParserException {
    int childPosition = position + Atom.HEADER_SIZE;
    int schemeInformationBoxPosition = C.POSITION_UNSET;
    int schemeInformationBoxSize = 0;
    @Nullable String schemeType = null;
    @Nullable Integer dataFormat = null;
    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childAtomSize = parent.readInt();
      int childAtomType = parent.readInt();
      if (childAtomType == Atom.TYPE_frma) {
        dataFormat = parent.readInt();
      } else if (childAtomType == Atom.TYPE_schm) {
        parent.skipBytes(4);
        // Common encryption scheme_type values are defined in ISO/IEC 23001-7:2016, section 4.1.
        schemeType = parent.readString(4);
      } else if (childAtomType == Atom.TYPE_schi) {
        schemeInformationBoxPosition = childPosition;
        schemeInformationBoxSize = childAtomSize;
      }
      childPosition += childAtomSize;
    }

    if (C.CENC_TYPE_cenc.equals(schemeType)
        || C.CENC_TYPE_cbc1.equals(schemeType)
        || C.CENC_TYPE_cens.equals(schemeType)
        || C.CENC_TYPE_cbcs.equals(schemeType)) {
      ExtractorUtil.checkContainerInput(dataFormat != null, "frma atom is mandatory");
      ExtractorUtil.checkContainerInput(
          schemeInformationBoxPosition != C.POSITION_UNSET, "schi atom is mandatory");
      @Nullable
      TrackEncryptionBox encryptionBox =
          parseSchiFromParent(
              parent, schemeInformationBoxPosition, schemeInformationBoxSize, schemeType);
      ExtractorUtil.checkContainerInput(encryptionBox != null, "tenc atom is mandatory");
      return Pair.create(dataFormat, castNonNull(encryptionBox));
    } else {
      return null;
    }
  }

  @Nullable
  private static TrackEncryptionBox parseSchiFromParent(
      ParsableByteArray parent, int position, int size, String schemeType) {
    int childPosition = position + Atom.HEADER_SIZE;
    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childAtomSize = parent.readInt();
      int childAtomType = parent.readInt();
      if (childAtomType == Atom.TYPE_tenc) {
        int fullAtom = parent.readInt();
        int version = Atom.parseFullAtomVersion(fullAtom);
        parent.skipBytes(1); // reserved = 0.
        int defaultCryptByteBlock = 0;
        int defaultSkipByteBlock = 0;
        if (version == 0) {
          parent.skipBytes(1); // reserved = 0.
        } else /* version 1 or greater */ {
          int patternByte = parent.readUnsignedByte();
          defaultCryptByteBlock = (patternByte & 0xF0) >> 4;
          defaultSkipByteBlock = patternByte & 0x0F;
        }
        boolean defaultIsProtected = parent.readUnsignedByte() == 1;
        int defaultPerSampleIvSize = parent.readUnsignedByte();
        byte[] defaultKeyId = new byte[16];
        parent.readBytes(defaultKeyId, 0, defaultKeyId.length);
        byte[] constantIv = null;
        if (defaultIsProtected && defaultPerSampleIvSize == 0) {
          int constantIvSize = parent.readUnsignedByte();
          constantIv = new byte[constantIvSize];
          parent.readBytes(constantIv, 0, constantIvSize);
        }
        return new TrackEncryptionBox(
            defaultIsProtected,
            schemeType,
            defaultPerSampleIvSize,
            defaultKeyId,
            defaultCryptByteBlock,
            defaultSkipByteBlock,
            constantIv);
      }
      childPosition += childAtomSize;
    }
    return null;
  }

  /** Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media. */
  @Nullable
  private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) {
    int childPosition = position + Atom.HEADER_SIZE;
    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childAtomSize = parent.readInt();
      int childAtomType = parent.readInt();
      if (childAtomType == Atom.TYPE_proj) {
        return Arrays.copyOfRange(parent.getData(), childPosition, childPosition + childAtomSize);
      }
      childPosition += childAtomSize;
    }
    return null;
  }

  /** Parses the size of an expandable class, as specified by ISO/IEC 14496-1 subsection 8.3.3. */
  private static int parseExpandableClassSize(ParsableByteArray data) {
    int currentByte = data.readUnsignedByte();
    int size = currentByte & 0x7F;
    while ((currentByte & 0x80) == 0x80) {
      currentByte = data.readUnsignedByte();
      size = (size << 7) | (currentByte & 0x7F);
    }
    return size;
  }

  /** Returns whether it's possible to apply the specified edit using gapless playback info. */
  private static boolean canApplyEditWithGaplessInfo(
      long[] timestamps, long duration, long editStartTime, long editEndTime) {
    int lastIndex = timestamps.length - 1;
    int latestDelayIndex = Util.constrainValue(MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex);
    int earliestPaddingIndex =
        Util.constrainValue(timestamps.length - MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex);
    return timestamps[0] <= editStartTime
        && editStartTime < timestamps[latestDelayIndex]
        && timestamps[earliestPaddingIndex] < editEndTime
        && editEndTime <= duration;
  }

  private AtomParsers() {
    // Prevent instantiation.
  }

  private static final class ChunkIterator {

    public final int length;

    public int index;
    public int numSamples;
    public long offset;

    private final boolean chunkOffsetsAreLongs;
    private final ParsableByteArray chunkOffsets;
    private final ParsableByteArray stsc;

    private int nextSamplesPerChunkChangeIndex;
    private int remainingSamplesPerChunkChanges;

    public ChunkIterator(
        ParsableByteArray stsc, ParsableByteArray chunkOffsets, boolean chunkOffsetsAreLongs)
        throws ParserException {
      this.stsc = stsc;
      this.chunkOffsets = chunkOffsets;
      this.chunkOffsetsAreLongs = chunkOffsetsAreLongs;
      chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE);
      length = chunkOffsets.readUnsignedIntToInt();
      stsc.setPosition(Atom.FULL_HEADER_SIZE);
      remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt();
      ExtractorUtil.checkContainerInput(stsc.readInt() == 1, "first_chunk must be 1");
      index = -1;
    }

    public boolean moveNext() {
      if (++index == length) {
        return false;
      }
      offset =
          chunkOffsetsAreLongs
              ? chunkOffsets.readUnsignedLongToLong()
              : chunkOffsets.readUnsignedInt();
      if (index == nextSamplesPerChunkChangeIndex) {
        numSamples = stsc.readUnsignedIntToInt();
        stsc.skipBytes(4); // Skip sample_description_index
        nextSamplesPerChunkChangeIndex =
            --remainingSamplesPerChunkChanges > 0
                ? (stsc.readUnsignedIntToInt() - 1)
                : C.INDEX_UNSET;
      }
      return true;
    }
  }

  /** Holds data parsed from a tkhd atom. */
  private static final class TkhdData {

    private final int id;
    private final long duration;
    private final int rotationDegrees;

    public TkhdData(int id, long duration, int rotationDegrees) {
      this.id = id;
      this.duration = duration;
      this.rotationDegrees = rotationDegrees;
    }
  }

  /** Holds data parsed from an stsd atom and its children. */
  private static final class StsdData {

    public static final int STSD_HEADER_SIZE = 8;

    public final TrackEncryptionBox[] trackEncryptionBoxes;

    @Nullable public Format format;
    public int nalUnitLengthFieldLength;
    public @Track.Transformation int requiredSampleTransformation;

    public StsdData(int numberOfEntries) {
      trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
      requiredSampleTransformation = Track.TRANSFORMATION_NONE;
    }
  }

  /** Data parsed from an esds box. */
  private static final class EsdsData {
    private final @NullableType String mimeType;
    private final byte @NullableType [] initializationData;
    private final long bitrate;
    private final long peakBitrate;

    public EsdsData(
        @NullableType String mimeType,
        byte @NullableType [] initializationData,
        long bitrate,
        long peakBitrate) {
      this.mimeType = mimeType;
      this.initializationData = initializationData;
      this.bitrate = bitrate;
      this.peakBitrate = peakBitrate;
    }
  }

  /** A box containing sample sizes (e.g. stsz, stz2). */
  private interface SampleSizeBox {

    /** Returns the number of samples. */
    int getSampleCount();

    /** Returns the size of each sample if fixed, or {@link C#LENGTH_UNSET} otherwise. */
    int getFixedSampleSize();

    /** Returns the size for the next sample. */
    int readNextSampleSize();
  }

  /** An stsz sample size box. */
  /* package */ static final class StszSampleSizeBox implements SampleSizeBox {

    private final int fixedSampleSize;
    private final int sampleCount;
    private final ParsableByteArray data;

    public StszSampleSizeBox(Atom.LeafAtom stszAtom, Format trackFormat) {
      data = stszAtom.data;
      data.setPosition(Atom.FULL_HEADER_SIZE);
      int fixedSampleSize = data.readUnsignedIntToInt();
      if (MimeTypes.AUDIO_RAW.equals(trackFormat.sampleMimeType)) {
        int pcmFrameSize = Util.getPcmFrameSize(trackFormat.pcmEncoding, trackFormat.channelCount);
        if (fixedSampleSize == 0 || fixedSampleSize % pcmFrameSize != 0) {
          // The sample size from the stsz box is inconsistent with the PCM encoding and channel
          // count derived from the stsd box. Choose stsd box as source of truth
          // [Internal ref: b/171627904].
          Log.w(
              TAG,
              "Audio sample size mismatch. stsd sample size: "
                  + pcmFrameSize
                  + ", stsz sample size: "
                  + fixedSampleSize);
          fixedSampleSize = pcmFrameSize;
        }
      }
      this.fixedSampleSize = fixedSampleSize == 0 ? C.LENGTH_UNSET : fixedSampleSize;
      sampleCount = data.readUnsignedIntToInt();
    }

    @Override
    public int getSampleCount() {
      return sampleCount;
    }

    @Override
    public int getFixedSampleSize() {
      return fixedSampleSize;
    }

    @Override
    public int readNextSampleSize() {
      return fixedSampleSize == C.LENGTH_UNSET ? data.readUnsignedIntToInt() : fixedSampleSize;
    }
  }

  /** An stz2 sample size box. */
  /* package */ static final class Stz2SampleSizeBox implements SampleSizeBox {

    private final ParsableByteArray data;
    private final int sampleCount;
    private final int fieldSize; // Can be 4, 8, or 16.

    // Used only if fieldSize == 4.
    private int sampleIndex;
    private int currentByte;

    public Stz2SampleSizeBox(Atom.LeafAtom stz2Atom) {
      data = stz2Atom.data;
      data.setPosition(Atom.FULL_HEADER_SIZE);
      fieldSize = data.readUnsignedIntToInt() & 0x000000FF;
      sampleCount = data.readUnsignedIntToInt();
    }

    @Override
    public int getSampleCount() {
      return sampleCount;
    }

    @Override
    public int getFixedSampleSize() {
      return C.LENGTH_UNSET;
    }

    @Override
    public int readNextSampleSize() {
      if (fieldSize == 8) {
        return data.readUnsignedByte();
      } else if (fieldSize == 16) {
        return data.readUnsignedShort();
      } else {
        // fieldSize == 4.
        if ((sampleIndex++ % 2) == 0) {
          // Read the next byte into our cached byte when we are reading the upper bits.
          currentByte = data.readUnsignedByte();
          // Read the upper bits from the byte and shift them to the lower 4 bits.
          return (currentByte & 0xF0) >> 4;
        } else {
          // Mask out the upper 4 bits of the last byte we read.
          return currentByte & 0x0F;
        }
      }
    }
  }
}
