| /* |
| * 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 com.google.android.exoplayer2.extractor.mp4; |
| |
| import static com.google.android.exoplayer2.util.Assertions.checkNotNull; |
| import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType; |
| import static com.google.android.exoplayer2.util.Util.castNonNull; |
| import static java.lang.Math.max; |
| |
| import android.util.Pair; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.C; |
| import com.google.android.exoplayer2.Format; |
| import com.google.android.exoplayer2.ParserException; |
| import com.google.android.exoplayer2.audio.AacUtil; |
| import com.google.android.exoplayer2.audio.Ac3Util; |
| import com.google.android.exoplayer2.audio.Ac4Util; |
| import com.google.android.exoplayer2.audio.OpusUtil; |
| import com.google.android.exoplayer2.drm.DrmInitData; |
| import com.google.android.exoplayer2.extractor.ExtractorUtil; |
| import com.google.android.exoplayer2.extractor.GaplessInfoHolder; |
| import com.google.android.exoplayer2.metadata.Metadata; |
| import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry; |
| import com.google.android.exoplayer2.util.CodecSpecificDataUtil; |
| import com.google.android.exoplayer2.util.Log; |
| import com.google.android.exoplayer2.util.MimeTypes; |
| import com.google.android.exoplayer2.util.ParsableByteArray; |
| import com.google.android.exoplayer2.util.Util; |
| import com.google.android.exoplayer2.video.AvcConfig; |
| import com.google.android.exoplayer2.video.ColorInfo; |
| import com.google.android.exoplayer2.video.DolbyVisionConfig; |
| import com.google.android.exoplayer2.video.HevcConfig; |
| import com.google.common.base.Function; |
| import com.google.common.collect.ImmutableList; |
| 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; |
| |
| // 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); |
| Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationDataBytes = |
| parseEsdsFromParent(parent, childStartPosition); |
| mimeType = mimeTypeAndInitializationDataBytes.first; |
| @Nullable byte[] initializationDataBytes = mimeTypeAndInitializationDataBytes.second; |
| 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)); |
| } |
| 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; |
| |
| 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) { |
| Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationData = |
| parseEsdsFromParent(parent, esdsAtomPosition); |
| mimeType = mimeTypeAndInitializationData.first; |
| @Nullable byte[] initializationDataBytes = mimeTypeAndInitializationData.second; |
| 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) { |
| out.format = |
| new Format.Builder() |
| .setId(trackId) |
| .setSampleMimeType(mimeType) |
| .setCodecs(codecs) |
| .setChannelCount(channelCount) |
| .setSampleRate(sampleRate) |
| .setPcmEncoding(pcmEncoding) |
| .setInitializationData(initializationData) |
| .setDrmInitData(drmInitData) |
| .setLanguage(language) |
| .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 Pair<@NullableType String, byte @NullableType []> 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.readUnsignedShort()); |
| } |
| 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 Pair.create(mimeType, null); |
| } |
| |
| parent.skipBytes(12); |
| |
| // Start of the DecoderSpecificInfo. |
| parent.skipBytes(1); // DecoderSpecificInfo tag |
| int initializationDataSize = parseExpandableClassSize(parent); |
| byte[] initializationData = new byte[initializationDataSize]; |
| parent.readBytes(initializationData, 0, initializationDataSize); |
| return Pair.create(mimeType, initializationData); |
| } |
| |
| /** |
| * 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; |
| @Track.Transformation public int requiredSampleTransformation; |
| |
| public StsdData(int numberOfEntries) { |
| trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; |
| requiredSampleTransformation = Track.TRANSFORMATION_NONE; |
| } |
| } |
| |
| /** 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; |
| } |
| } |
| } |
| } |
| } |