diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4')
14 files changed, 6463 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java new file mode 100644 index 0000000000..56f0eab1cd --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -0,0 +1,558 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings("ConstantField") +/* package */ abstract class Atom { + + /** + * Size of an atom header, in bytes. + */ + public static final int HEADER_SIZE = 8; + + /** + * Size of a full atom header, in bytes. + */ + public static final int FULL_HEADER_SIZE = 12; + + /** + * Size of a long atom header, in bytes. + */ + public static final int LONG_HEADER_SIZE = 16; + + /** + * Value for the size field in an atom that defines its size in the largesize field. + */ + public static final int DEFINES_LARGE_SIZE = 1; + + /** + * Value for the size field in an atom that extends to the end of the file. + */ + public static final int EXTENDS_TO_END_SIZE = 0; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ftyp = 0x66747970; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_avc1 = 0x61766331; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_avc3 = 0x61766333; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_avcC = 0x61766343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hvc1 = 0x68766331; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hev1 = 0x68657631; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hvcC = 0x68766343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vp08 = 0x76703038; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vp09 = 0x76703039; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vpcC = 0x76706343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_av01 = 0x61763031; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_av1C = 0x61763143; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvav = 0x64766176; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dva1 = 0x64766131; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvhe = 0x64766865; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvh1 = 0x64766831; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvcC = 0x64766343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvvC = 0x64767643; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_s263 = 0x73323633; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_d263 = 0x64323633; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mdat = 0x6d646174; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mp4a = 0x6d703461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE__mp3 = 0x2e6d7033; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_wave = 0x77617665; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_lpcm = 0x6c70636d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sowt = 0x736f7774; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ac_3 = 0x61632d33; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dac3 = 0x64616333; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ec_3 = 0x65632d33; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dec3 = 0x64656333; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ac_4 = 0x61632d34; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dac4 = 0x64616334; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtsc = 0x64747363; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtsh = 0x64747368; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtsl = 0x6474736c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtse = 0x64747365; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ddts = 0x64647473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tfdt = 0x74666474; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tfhd = 0x74666864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_trex = 0x74726578; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_trun = 0x7472756e; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sidx = 0x73696478; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_moov = 0x6d6f6f76; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mvhd = 0x6d766864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_trak = 0x7472616b; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mdia = 0x6d646961; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_minf = 0x6d696e66; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stbl = 0x7374626c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_esds = 0x65736473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_moof = 0x6d6f6f66; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_traf = 0x74726166; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mvex = 0x6d766578; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mehd = 0x6d656864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tkhd = 0x746b6864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_edts = 0x65647473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_elst = 0x656c7374; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mdhd = 0x6d646864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hdlr = 0x68646c72; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stsd = 0x73747364; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_pssh = 0x70737368; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sinf = 0x73696e66; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_schm = 0x7363686d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_schi = 0x73636869; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tenc = 0x74656e63; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_encv = 0x656e6376; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_enca = 0x656e6361; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_frma = 0x66726d61; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_saiz = 0x7361697a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_saio = 0x7361696f; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sbgp = 0x73626770; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sgpd = 0x73677064; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_uuid = 0x75756964; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_senc = 0x73656e63; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_pasp = 0x70617370; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_TTML = 0x54544d4c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vmhd = 0x766d6864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mp4v = 0x6d703476; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stts = 0x73747473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stss = 0x73747373; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ctts = 0x63747473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stsc = 0x73747363; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stsz = 0x7374737a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stz2 = 0x73747a32; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stco = 0x7374636f; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_co64 = 0x636f3634; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tx3g = 0x74783367; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_wvtt = 0x77767474; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stpp = 0x73747070; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_c608 = 0x63363038; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_samr = 0x73616d72; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sawb = 0x73617762; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_udta = 0x75647461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_meta = 0x6d657461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_keys = 0x6b657973; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ilst = 0x696c7374; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mean = 0x6d65616e; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_name = 0x6e616d65; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_data = 0x64617461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_emsg = 0x656d7367; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_st3d = 0x73743364; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sv3d = 0x73763364; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_proj = 0x70726f6a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_camm = 0x63616d6d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_alac = 0x616c6163; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_alaw = 0x616c6177; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ulaw = 0x756c6177; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_Opus = 0x4f707573; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dOps = 0x644f7073; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_fLaC = 0x664c6143; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dfLa = 0x64664c61; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_twos = 0x74776f73; + + public final int type; + + public Atom(int type) { + this.type = type; + } + + @Override + public String toString() { + return getAtomTypeString(type); + } + + /** + * An MP4 atom that is a leaf. + */ + /* package */ static final class LeafAtom extends Atom { + + /** + * The atom data. + */ + public final ParsableByteArray data; + + /** + * @param type The type of the atom. + * @param data The atom data. + */ + public LeafAtom(int type, ParsableByteArray data) { + super(type); + this.data = data; + } + + } + + /** + * An MP4 atom that has child atoms. + */ + /* package */ static final class ContainerAtom extends Atom { + + public final long endPosition; + public final List<LeafAtom> leafChildren; + public final List<ContainerAtom> containerChildren; + + /** + * @param type The type of the atom. + * @param endPosition The position of the first byte after the end of the atom. + */ + public ContainerAtom(int type, long endPosition) { + super(type); + this.endPosition = endPosition; + leafChildren = new ArrayList<>(); + containerChildren = new ArrayList<>(); + } + + /** + * Adds a child leaf to this container. + * + * @param atom The child to add. + */ + public void add(LeafAtom atom) { + leafChildren.add(atom); + } + + /** + * Adds a child container to this container. + * + * @param atom The child to add. + */ + public void add(ContainerAtom atom) { + containerChildren.add(atom); + } + + /** + * Returns the child leaf of the given type. + * + * <p>If no child exists with the given type then null is returned. If multiple children exist + * with the given type then the first one to have been added is returned. + * + * @param type The leaf type. + * @return The child leaf of the given type, or null if no such child exists. + */ + @Nullable + public LeafAtom getLeafAtomOfType(int type) { + int childrenSize = leafChildren.size(); + for (int i = 0; i < childrenSize; i++) { + LeafAtom atom = leafChildren.get(i); + if (atom.type == type) { + return atom; + } + } + return null; + } + + /** + * Returns the child container of the given type. + * + * <p>If no child exists with the given type then null is returned. If multiple children exist + * with the given type then the first one to have been added is returned. + * + * @param type The container type. + * @return The child container of the given type, or null if no such child exists. + */ + @Nullable + public ContainerAtom getContainerAtomOfType(int type) { + int childrenSize = containerChildren.size(); + for (int i = 0; i < childrenSize; i++) { + ContainerAtom atom = containerChildren.get(i); + if (atom.type == type) { + return atom; + } + } + return null; + } + + /** + * Returns the total number of leaf/container children of this atom with the given type. + * + * @param type The type of child atoms to count. + * @return The total number of leaf/container children of this atom with the given type. + */ + public int getChildAtomOfTypeCount(int type) { + int count = 0; + int size = leafChildren.size(); + for (int i = 0; i < size; i++) { + LeafAtom atom = leafChildren.get(i); + if (atom.type == type) { + count++; + } + } + size = containerChildren.size(); + for (int i = 0; i < size; i++) { + ContainerAtom atom = containerChildren.get(i); + if (atom.type == type) { + count++; + } + } + return count; + } + + @Override + public String toString() { + return getAtomTypeString(type) + + " leaves: " + Arrays.toString(leafChildren.toArray()) + + " containers: " + Arrays.toString(containerChildren.toArray()); + } + + } + + /** + * Parses the version number out of the additional integer component of a full atom. + */ + public static int parseFullAtomVersion(int fullAtomInt) { + return 0x000000FF & (fullAtomInt >> 24); + } + + /** + * Parses the atom flags out of the additional integer component of a full atom. + */ + public static int parseFullAtomFlags(int fullAtomInt) { + return 0x00FFFFFF & fullAtomInt; + } + + /** + * Converts a numeric atom type to the corresponding four character string. + * + * @param type The numeric atom type. + * @return The corresponding four character string. + */ + public static String getAtomTypeString(int type) { + return "" + (char) ((type >> 24) & 0xFF) + + (char) ((type >> 16) & 0xFF) + + (char) ((type >> 8) & 0xFF) + + (char) (type & 0xFF); + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java new file mode 100644 index 0000000000..93ee2d6810 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -0,0 +1,1607 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType; + +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.DolbyVisionConfig; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.HevcConfig; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ +@SuppressWarnings({"ConstantField"}) +/* package */ final class AtomParsers { + + private static final String TAG = "AtomParsers"; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_vide = 0x76696465; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_soun = 0x736f756e; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_text = 0x74657874; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_sbtl = 0x7362746c; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_subt = 0x73756274; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_clcp = 0x636c6370; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_meta = 0x6d657461; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_mdta = 0x6d647461; + + /** + * 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"); + + /** + * Parses a trak atom (defined in 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. + * @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. + */ + public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration, + DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime) + throws ParserException { + Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); + int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data)); + if (trackType == C.TRACK_TYPE_UNKNOWN) { + return null; + } + + TkhdData tkhdData = parseTkhd(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 = mdia.getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); + + Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); + StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id, + tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime); + long[] editListDurations = null; + long[] editListMediaTimes = null; + if (!ignoreEditLists) { + Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); + 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 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. + */ + public static TrackSampleTable parseStbl( + Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) + throws ParserException { + SampleSizeBox sampleSizeBox; + Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); + if (stszAtom != null) { + sampleSizeBox = new StszSampleSizeBox(stszAtom); + } else { + Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2); + if (stz2Atom == null) { + throw new ParserException("Track has no sample table size information"); + } + 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= */ C.TIME_UNSET); + } + + // Entries are byte offsets of chunks. + boolean chunkOffsetsAreLongs = false; + Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco); + if (chunkOffsetsAtom == null) { + chunkOffsetsAreLongs = true; + chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64); + } + ParsableByteArray chunkOffsets = chunkOffsetsAtom.data; + // Entries are (chunk number, number of samples per chunk, sample description index). + ParsableByteArray stsc = stblAtom.getLeafAtomOfType(Atom.TYPE_stsc).data; + // Entries are (number of samples, timestamp delta between those samples). + ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data; + // Entries are the indices of samples that are synchronization samples. + Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss); + ParsableByteArray stss = stssAtom != null ? stssAtom.data : null; + // Entries are (number of samples, timestamp offset). + Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts); + 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. + boolean isFixedSampleSizeRawAudio = + sampleSizeBox.isFixedSampleSize() + && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) + && remainingTimestampDeltaChanges == 0 + && remainingTimestampOffsetChanges == 0 + && remainingSynchronizationSamples == 0; + + long[] offsets; + int[] sizes; + int maximumSize = 0; + long[] timestamps; + int[] flags; + long timestampTimeUnits = 0; + long duration; + + if (!isFixedSampleSizeRawAudio) { + 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 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 = 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 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; + 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" : "")); + } + } else { + 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; + } + int fixedSampleSize = + Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount); + 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; + } + 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 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 = 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 = 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]; + for (int i = 0; i < track.editListDurations.length; i++) { + long editMediaTime = track.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( + Math.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); + } + + /** + * Parses a udta atom. + * + * @param udtaAtom The udta (user data) atom to decode. + * @param isQuickTime True for QuickTime media. False otherwise. + * @return Parsed metadata, or null. + */ + @Nullable + public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { + if (isQuickTime) { + // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and + // decode one. + return null; + } + ParsableByteArray udtaData = udtaAtom.data; + udtaData.setPosition(Atom.HEADER_SIZE); + 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); + return parseUdtaMeta(udtaData, atomPosition + atomSize); + } + udtaData.setPosition(atomPosition + atomSize); + } + return null; + } + + /** + * 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) { + Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr); + Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys); + Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst); + if (hdlrAtom == null + || keysAtom == null + || ilstAtom == null + || AtomParsers.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]; + 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); + } + + @Nullable + private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) { + meta.skipBytes(Atom.FULL_HEADER_SIZE); + 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) { + Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst); + if (entry != null) { + entries.add(entry); + } + } + return entries.isEmpty() ? null : new Metadata(entries); + } + + /** + * Parses a mvhd atom (defined in 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 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.data[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 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 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 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. + * @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, 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(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = stsd.readInt(); + if (childAtomType == Atom.TYPE_avc1 + || childAtomType == Atom.TYPE_avc3 + || childAtomType == Atom.TYPE_encv + || childAtomType == Atom.TYPE_mp4v + || childAtomType == Atom.TYPE_hvc1 + || childAtomType == Atom.TYPE_hev1 + || childAtomType == Atom.TYPE_s263 + || 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_dtsc + || childAtomType == Atom.TYPE_dtse + || childAtomType == Atom.TYPE_dtsh + || childAtomType == Atom.TYPE_dtsl + || childAtomType == Atom.TYPE_samr + || childAtomType == Atom.TYPE_sawb + || childAtomType == Atom.TYPE_lpcm + || childAtomType == Atom.TYPE_sowt + || childAtomType == Atom.TYPE_twos + || childAtomType == Atom.TYPE__mp3 + || 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_camm) { + out.format = Format.createSampleFormat(Integer.toString(trackId), + MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null); + } + stsd.setPosition(childStartPosition + childAtomSize); + } + return out; + } + + private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, + int atomSize, int trackId, String language, StsdData out) throws ParserException { + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); + + // Default values. + List<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 = Collections.singletonList(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 = + Format.createTextSampleFormat( + Integer.toString(trackId), + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + language, + /* accessibilityChannel= */ Format.NO_VALUE, + /* drmInitData= */ null, + subsampleOffsetUs, + initializationData); + } + + private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, + int size, int trackId, int rotationDegrees, 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) { + 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; + // } + + List<byte[]> initializationData = null; + String mimeType = null; + String codecs = null; + byte[] projectionData = null; + @C.StereoMode + int stereoMode = Format.NO_VALUE; + 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; + } + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_avcC) { + Assertions.checkState(mimeType == 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.pixelWidthAspectRatio; + } + } else if (childAtomType == Atom.TYPE_hvcC) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H265; + parent.setPosition(childStartPosition + Atom.HEADER_SIZE); + HevcConfig hevcConfig = HevcConfig.parse(parent); + initializationData = hevcConfig.initializationData; + out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; + } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { + DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); + if (dolbyVisionConfig != null) { + codecs = dolbyVisionConfig.codecs; + mimeType = MimeTypes.VIDEO_DOLBY_VISION; + } + } else if (childAtomType == Atom.TYPE_vpcC) { + Assertions.checkState(mimeType == null); + mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9; + } else if (childAtomType == Atom.TYPE_av1C) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_AV1; + } else if (childAtomType == Atom.TYPE_d263) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H263; + } else if (childAtomType == Atom.TYPE_esds) { + Assertions.checkState(mimeType == null); + Pair<String, byte[]> mimeTypeAndInitializationData = + parseEsdsFromParent(parent, childStartPosition); + mimeType = mimeTypeAndInitializationData.first; + initializationData = Collections.singletonList(mimeTypeAndInitializationData.second); + } 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; + } + } + } + childPosition += childAtomSize; + } + + // If the media type was not recognized, ignore the track. + if (mimeType == null) { + return; + } + + out.format = + Format.createVideoSampleFormat( + Integer.toString(trackId), + mimeType, + codecs, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + width, + height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + /* colorInfo= */ null, + drmInitData); + } + + /** + * Parses the edts atom (defined in 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 a pair of nulls if they are + * not present. + */ + private static Pair<long[], long[]> parseEdts(Atom.ContainerAtom edtsAtom) { + Atom.LeafAtom elst; + if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) { + return Pair.create(null, null); + } + ParsableByteArray elstData = elst.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, 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; + @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; + + if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { + channelCount = parent.readUnsignedShort(); + parent.skipBytes(6); // sampleSize, compressionId, packetSize. + sampleRate = parent.readUnsignedFixedPoint1616(); + + 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) { + 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. + 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_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__mp3) { + mimeType = MimeTypes.AUDIO_MPEG; + } 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; + } + + byte[] initializationData = null; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) { + int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition + : findEsdsPosition(parent, childPosition, childAtomSize); + if (esdsAtomPosition != C.POSITION_UNSET) { + Pair<String, byte[]> mimeTypeAndInitializationData = + parseEsdsFromParent(parent, esdsAtomPosition); + mimeType = mimeTypeAndInitializationData.first; + initializationData = mimeTypeAndInitializationData.second; + if (MimeTypes.AUDIO_AAC.equals(mimeType)) { + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, + // which is more reliable. See [Internal: b/10903778]. + Pair<Integer, Integer> audioSpecificConfig = + CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; + } + } + } 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_ddts) { + out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, + language); + } 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; + initializationData = new byte[opusMagic.length + childAtomBodySize]; + System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length); + parent.setPosition(childPosition + Atom.HEADER_SIZE); + parent.readBytes(initializationData, opusMagic.length, childAtomBodySize); + } else if (childAtomType == Atom.TYPE_dfLa) { + int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; + initializationData = new byte[4 + childAtomBodySize]; + initializationData[0] = 0x66; // f + initializationData[1] = 0x4C; // L + initializationData[2] = 0x61; // a + initializationData[3] = 0x43; // C + parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); + parent.readBytes(initializationData, /* offset= */ 4, childAtomBodySize); + } else if (childAtomType == Atom.TYPE_alac) { + int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; + initializationData = new byte[childAtomBodySize]; + parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); + parent.readBytes(initializationData, /* 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(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; + } + childPosition += childAtomSize; + } + + if (out.format == null && mimeType != null) { + out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, + initializationData == null ? null : Collections.singletonList(initializationData), + drmInitData, 0, language); + } + } + + /** + * Returns the position of the esds box within a parent, or {@link C#POSITION_UNSET} if no esds + * box is found + */ + private static int findEsdsPosition(ParsableByteArray parent, int position, int size) { + int childAtomPosition = parent.getPosition(); + while (childAtomPosition - position < size) { + parent.setPosition(childAtomPosition); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childType = parent.readInt(); + if (childType == Atom.TYPE_esds) { + return childAtomPosition; + } + childAtomPosition += childAtomSize; + } + return C.POSITION_UNSET; + } + + /** + * Returns codec-specific initialization data contained in an esds box. + */ + private static Pair<String, byte[]> parseEsdsFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + Atom.HEADER_SIZE + 4); + // Start of the ES_Descriptor (defined in 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 14496-1) + parent.skipBytes(1); // DecoderConfigDescriptor tag + parseExpandableClassSize(parent); + + // Set the MIME type based on the object type indication (14496-1 table 5). + int objectTypeIndication = parent.readUnsignedByte(); + 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. + */ + private static Pair<Integer, TrackEncryptionBox> parseSampleEntryEncryptionData( + ParsableByteArray parent, int position, int size) { + int childPosition = parent.getPosition(); + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_sinf) { + Pair<Integer, TrackEncryptionBox> result = parseCommonEncryptionSinfFromParent(parent, + childPosition, childAtomSize); + if (result != null) { + return result; + } + } + childPosition += childAtomSize; + } + return null; + } + + /* package */ static Pair<Integer, TrackEncryptionBox> parseCommonEncryptionSinfFromParent( + ParsableByteArray parent, int position, int size) { + int childPosition = position + Atom.HEADER_SIZE; + int schemeInformationBoxPosition = C.POSITION_UNSET; + int schemeInformationBoxSize = 0; + String schemeType = null; + 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)) { + Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); + Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET, + "schi atom is mandatory"); + TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition, + schemeInformationBoxSize, schemeType); + Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory"); + return Pair.create(dataFormat, encryptionBox); + } else { + return null; + } + } + + 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. + */ + 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.data, childPosition, childPosition + childAtomSize); + } + childPosition += childAtomSize; + } + return null; + } + + /** + * Parses the size of an expandable class, as specified by ISO 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) { + 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(); + Assertions.checkState(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; + + 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 for the next sample. + */ + int readNextSampleSize(); + + /** + * Returns whether samples have a fixed size. + */ + boolean isFixedSampleSize(); + + } + + /** + * 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) { + data = stszAtom.data; + data.setPosition(Atom.FULL_HEADER_SIZE); + fixedSampleSize = data.readUnsignedIntToInt(); + sampleCount = data.readUnsignedIntToInt(); + } + + @Override + public int getSampleCount() { + return sampleCount; + } + + @Override + public int readNextSampleSize() { + return fixedSampleSize == 0 ? data.readUnsignedIntToInt() : fixedSampleSize; + } + + @Override + public boolean isFixedSampleSize() { + return fixedSampleSize != 0; + } + + } + + /** + * 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 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; + } + } + } + + @Override + public boolean isFixedSampleSize() { + return false; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java new file mode 100644 index 0000000000..0942673435 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java @@ -0,0 +1,32 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +/* package */ final class DefaultSampleValues { + + public final int sampleDescriptionIndex; + public final int duration; + public final int size; + public final int flags; + + public DefaultSampleValues(int sampleDescriptionIndex, int duration, int size, int flags) { + this.sampleDescriptionIndex = sampleDescriptionIndex; + this.duration = duration; + this.size = size; + this.flags = flags; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java new file mode 100644 index 0000000000..78d30ba582 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java @@ -0,0 +1,114 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Rechunks fixed sample size media in which every sample is a key frame (e.g. uncompressed audio). + */ +/* package */ final class FixedSampleSizeRechunker { + + /** + * The result of a rechunking operation. + */ + public static final class Results { + + public final long[] offsets; + public final int[] sizes; + public final int maximumSize; + public final long[] timestamps; + public final int[] flags; + public final long duration; + + private Results( + long[] offsets, + int[] sizes, + int maximumSize, + long[] timestamps, + int[] flags, + long duration) { + this.offsets = offsets; + this.sizes = sizes; + this.maximumSize = maximumSize; + this.timestamps = timestamps; + this.flags = flags; + this.duration = duration; + } + + } + + /** + * Maximum number of bytes for each buffer in rechunked output. + */ + private static final int MAX_SAMPLE_SIZE = 8 * 1024; + + /** + * Rechunk the given fixed sample size input to produce a new sequence of samples. + * + * @param fixedSampleSize Size in bytes of each sample. + * @param chunkOffsets Chunk offsets in the MP4 stream to rechunk. + * @param chunkSampleCounts Sample counts for each of the MP4 stream's chunks. + * @param timestampDeltaInTimeUnits Timestamp delta between each sample in time units. + */ + public static Results rechunk(int fixedSampleSize, long[] chunkOffsets, int[] chunkSampleCounts, + long timestampDeltaInTimeUnits) { + int maxSampleCount = MAX_SAMPLE_SIZE / fixedSampleSize; + + // Count the number of new, rechunked buffers. + int rechunkedSampleCount = 0; + for (int chunkSampleCount : chunkSampleCounts) { + rechunkedSampleCount += Util.ceilDivide(chunkSampleCount, maxSampleCount); + } + + long[] offsets = new long[rechunkedSampleCount]; + int[] sizes = new int[rechunkedSampleCount]; + int maximumSize = 0; + long[] timestamps = new long[rechunkedSampleCount]; + int[] flags = new int[rechunkedSampleCount]; + + int originalSampleIndex = 0; + int newSampleIndex = 0; + for (int chunkIndex = 0; chunkIndex < chunkSampleCounts.length; chunkIndex++) { + int chunkSamplesRemaining = chunkSampleCounts[chunkIndex]; + long sampleOffset = chunkOffsets[chunkIndex]; + + while (chunkSamplesRemaining > 0) { + int bufferSampleCount = Math.min(maxSampleCount, chunkSamplesRemaining); + + offsets[newSampleIndex] = sampleOffset; + sizes[newSampleIndex] = fixedSampleSize * bufferSampleCount; + maximumSize = Math.max(maximumSize, sizes[newSampleIndex]); + timestamps[newSampleIndex] = (timestampDeltaInTimeUnits * originalSampleIndex); + flags[newSampleIndex] = C.BUFFER_FLAG_KEY_FRAME; + + sampleOffset += sizes[newSampleIndex]; + originalSampleIndex += bufferSampleCount; + + chunkSamplesRemaining -= bufferSampleCount; + newSampleIndex++; + } + } + long duration = timestampDeltaInTimeUnits * originalSampleIndex; + + return new Results(offsets, sizes, maximumSize, timestamps, flags, duration); + } + + private FixedSampleSizeRechunker() { + // Prevent instantiation. + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java new file mode 100644 index 0000000000..291a9ade27 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -0,0 +1,1660 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import android.util.Pair; +import android.util.SparseArray; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ChunkIndex; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessage; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.CeaUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +/** Extracts data from the FMP4 container format. */ +@SuppressWarnings("ConstantField") +public class FragmentedMp4Extractor implements Extractor { + + /** Factory for {@link FragmentedMp4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = + () -> new Extractor[] {new FragmentedMp4Extractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag values are {@link + * #FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME}, {@link #FLAG_WORKAROUND_IGNORE_TFDT_BOX}, + * {@link #FLAG_ENABLE_EMSG_TRACK}, {@link #FLAG_SIDELOADED} and {@link + * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, + FLAG_WORKAROUND_IGNORE_TFDT_BOX, + FLAG_ENABLE_EMSG_TRACK, + FLAG_SIDELOADED, + FLAG_WORKAROUND_IGNORE_EDIT_LISTS + }) + public @interface Flags {} + /** + * Flag to work around an issue in some video streams where every frame is marked as a sync frame. + * The workaround overrides the sync frame flags in the stream, forcing them to false except for + * the first sample in each segment. + * <p> + * This flag does nothing if the stream is not a video stream. + */ + public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; + /** Flag to ignore any tfdt boxes in the stream. */ + public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 1 << 1; // 2 + /** + * Flag to indicate that the extractor should output an event message metadata track. Any event + * messages in the stream will be delivered as samples to this track. + */ + public static final int FLAG_ENABLE_EMSG_TRACK = 1 << 2; // 4 + /** + * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 + * container. + */ + private static final int FLAG_SIDELOADED = 1 << 3; // 8 + /** Flag to ignore any edit lists in the stream. */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1 << 4; // 16 + + private static final String TAG = "FragmentedMp4Extractor"; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int SAMPLE_GROUP_TYPE_seig = 0x73656967; + + private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = + new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; + private static final Format EMSG_FORMAT = + Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); + + // Parser states. + private static final int STATE_READING_ATOM_HEADER = 0; + private static final int STATE_READING_ATOM_PAYLOAD = 1; + private static final int STATE_READING_ENCRYPTION_DATA = 2; + private static final int STATE_READING_SAMPLE_START = 3; + private static final int STATE_READING_SAMPLE_CONTINUE = 4; + + // Workarounds. + @Flags private final int flags; + @Nullable private final Track sideloadedTrack; + + // Sideloaded data. + private final List<Format> closedCaptionFormats; + + // Track-linked data bundle, accessible as a whole through trackID. + private final SparseArray<TrackBundle> trackBundles; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalPrefix; + private final ParsableByteArray nalBuffer; + private final byte[] scratchBytes; + private final ParsableByteArray scratch; + + // Adjusts sample timestamps. + @Nullable private final TimestampAdjuster timestampAdjuster; + + private final EventMessageEncoder eventMessageEncoder; + + // Parser state. + private final ParsableByteArray atomHeader; + private final ArrayDeque<ContainerAtom> containerAtoms; + private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos; + @Nullable private final TrackOutput additionalEmsgTrackOutput; + + private int parserState; + private int atomType; + private long atomSize; + private int atomHeaderBytesRead; + private ParsableByteArray atomData; + private long endOfMdatPosition; + private int pendingMetadataSampleBytes; + private long pendingSeekTimeUs; + + private long durationUs; + private long segmentIndexEarliestPresentationTimeUs; + private TrackBundle currentTrackBundle; + private int sampleSize; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; + private boolean processSeiNalUnitPayload; + + // Extractor output. + private ExtractorOutput extractorOutput; + private TrackOutput[] emsgTrackOutputs; + private TrackOutput[] cea608TrackOutputs; + + // Whether extractorOutput.seekMap has been called. + private boolean haveOutputSeekMap; + + public FragmentedMp4Extractor() { + this(0); + } + + /** + * @param flags Flags that control the extractor's behavior. + */ + public FragmentedMp4Extractor(@Flags int flags) { + this(flags, /* timestampAdjuster= */ null); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + */ + public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) { + this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList()); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. + */ + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack) { + this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList()); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + */ + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack, + List<Format> closedCaptionFormats) { + this( + flags, + timestampAdjuster, + sideloadedTrack, + closedCaptionFormats, + /* additionalEmsgTrackOutput= */ null); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages + * targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special + * handling of emsg messages for players is not required. + */ + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack, + List<Format> closedCaptionFormats, + @Nullable TrackOutput additionalEmsgTrackOutput) { + this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); + this.timestampAdjuster = timestampAdjuster; + this.sideloadedTrack = sideloadedTrack; + this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); + this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; + eventMessageEncoder = new EventMessageEncoder(); + atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalPrefix = new ParsableByteArray(5); + nalBuffer = new ParsableByteArray(); + scratchBytes = new byte[16]; + scratch = new ParsableByteArray(scratchBytes); + containerAtoms = new ArrayDeque<>(); + pendingMetadataSampleInfos = new ArrayDeque<>(); + trackBundles = new SparseArray<>(); + durationUs = C.TIME_UNSET; + pendingSeekTimeUs = C.TIME_UNSET; + segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; + enterReadingAtomHeaderState(); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return Sniffer.sniffFragmented(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + if (sideloadedTrack != null) { + TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type)); + bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); + trackBundles.put(0, bundle); + maybeInitExtraTracks(); + extractorOutput.endTracks(); + } + } + + @Override + public void seek(long position, long timeUs) { + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + trackBundles.valueAt(i).reset(); + } + pendingMetadataSampleInfos.clear(); + pendingMetadataSampleBytes = 0; + pendingSeekTimeUs = timeUs; + containerAtoms.clear(); + enterReadingAtomHeaderState(); + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + while (true) { + switch (parserState) { + case STATE_READING_ATOM_HEADER: + if (!readAtomHeader(input)) { + return Extractor.RESULT_END_OF_INPUT; + } + break; + case STATE_READING_ATOM_PAYLOAD: + readAtomPayload(input); + break; + case STATE_READING_ENCRYPTION_DATA: + readEncryptionData(input); + break; + default: + if (readSample(input)) { + return RESULT_CONTINUE; + } + } + } + } + + private void enterReadingAtomHeaderState() { + parserState = STATE_READING_ATOM_HEADER; + atomHeaderBytesRead = 0; + } + + private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { + if (atomHeaderBytesRead == 0) { + // Read the standard length atom header. + if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { + return false; + } + atomHeaderBytesRead = Atom.HEADER_SIZE; + atomHeader.setPosition(0); + atomSize = atomHeader.readUnsignedInt(); + atomType = atomHeader.readInt(); + } + + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large size. + int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; + input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); + atomHeaderBytesRead += headerBytesRemaining; + atomSize = atomHeader.readUnsignedLongToLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. Note that if the atom is within a container we can + // work out its size even if the input length is unknown. + long endPosition = input.getLength(); + if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { + endPosition = containerAtoms.peek().endPosition; + } + if (endPosition != C.LENGTH_UNSET) { + atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; + } + } + + if (atomSize < atomHeaderBytesRead) { + throw new ParserException("Atom size less than header length (unsupported)."); + } + + long atomPosition = input.getPosition() - atomHeaderBytesRead; + if (atomType == Atom.TYPE_moof) { + // The data positions may be updated when parsing the tfhd/trun. + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + TrackFragment fragment = trackBundles.valueAt(i).fragment; + fragment.atomPosition = atomPosition; + fragment.auxiliaryDataPosition = atomPosition; + fragment.dataPosition = atomPosition; + } + } + + if (atomType == Atom.TYPE_mdat) { + currentTrackBundle = null; + endOfMdatPosition = atomPosition + atomSize; + if (!haveOutputSeekMap) { + // This must be the first mdat in the stream. + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition)); + haveOutputSeekMap = true; + } + parserState = STATE_READING_ENCRYPTION_DATA; + return true; + } + + if (shouldParseContainerAtom(atomType)) { + long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE; + containerAtoms.push(new ContainerAtom(atomType, endPosition)); + if (atomSize == atomHeaderBytesRead) { + processAtomEnded(endPosition); + } else { + // Start reading the first child atom. + enterReadingAtomHeaderState(); + } + } else if (shouldParseLeafAtom(atomType)) { + if (atomHeaderBytesRead != Atom.HEADER_SIZE) { + throw new ParserException("Leaf atom defines extended atom size (unsupported)."); + } + if (atomSize > Integer.MAX_VALUE) { + throw new ParserException("Leaf atom with length > 2147483647 (unsupported)."); + } + atomData = new ParsableByteArray((int) atomSize); + System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE); + parserState = STATE_READING_ATOM_PAYLOAD; + } else { + if (atomSize > Integer.MAX_VALUE) { + throw new ParserException("Skipping atom with length > 2147483647 (unsupported)."); + } + atomData = null; + parserState = STATE_READING_ATOM_PAYLOAD; + } + + return true; + } + + private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException { + int atomPayloadSize = (int) atomSize - atomHeaderBytesRead; + if (atomData != null) { + input.readFully(atomData.data, Atom.HEADER_SIZE, atomPayloadSize); + onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition()); + } else { + input.skipFully(atomPayloadSize); + } + processAtomEnded(input.getPosition()); + } + + private void processAtomEnded(long atomEndPosition) throws ParserException { + while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) { + onContainerAtomRead(containerAtoms.pop()); + } + enterReadingAtomHeaderState(); + } + + private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserException { + if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(leaf); + } else if (leaf.type == Atom.TYPE_sidx) { + Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition); + segmentIndexEarliestPresentationTimeUs = result.first; + extractorOutput.seekMap(result.second); + haveOutputSeekMap = true; + } else if (leaf.type == Atom.TYPE_emsg) { + onEmsgLeafAtomRead(leaf.data); + } + } + + private void onContainerAtomRead(ContainerAtom container) throws ParserException { + if (container.type == Atom.TYPE_moov) { + onMoovContainerAtomRead(container); + } else if (container.type == Atom.TYPE_moof) { + onMoofContainerAtomRead(container); + } else if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(container); + } + } + + private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { + Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); + + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); + + // Read declaration of track fragments in the Moov box. + ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); + SparseArray<DefaultSampleValues> defaultSampleValuesArray = new SparseArray<>(); + long duration = C.TIME_UNSET; + int mvexChildrenSize = mvex.leafChildren.size(); + for (int i = 0; i < mvexChildrenSize; i++) { + Atom.LeafAtom atom = mvex.leafChildren.get(i); + if (atom.type == Atom.TYPE_trex) { + Pair<Integer, DefaultSampleValues> trexData = parseTrex(atom.data); + defaultSampleValuesArray.put(trexData.first, trexData.second); + } else if (atom.type == Atom.TYPE_mehd) { + duration = parseMehd(atom.data); + } + } + + // Construction of tracks. + SparseArray<Track> tracks = new SparseArray<>(); + int moovContainerChildrenSize = moov.containerChildren.size(); + for (int i = 0; i < moovContainerChildrenSize; i++) { + Atom.ContainerAtom atom = moov.containerChildren.get(i); + if (atom.type == Atom.TYPE_trak) { + Track track = + modifyTrack( + AtomParsers.parseTrak( + atom, + moov.getLeafAtomOfType(Atom.TYPE_mvhd), + duration, + drmInitData, + (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, + false)); + if (track != null) { + tracks.put(track.id, track); + } + } + } + + int trackCount = tracks.size(); + if (trackBundles.size() == 0) { + // We need to create the track bundles. + for (int i = 0; i < trackCount; i++) { + Track track = tracks.valueAt(i); + TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); + trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); + trackBundles.put(track.id, trackBundle); + durationUs = Math.max(durationUs, track.durationUs); + } + maybeInitExtraTracks(); + extractorOutput.endTracks(); + } else { + Assertions.checkState(trackBundles.size() == trackCount); + for (int i = 0; i < trackCount; i++) { + Track track = tracks.valueAt(i); + trackBundles + .get(track.id) + .init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); + } + } + } + + @Nullable + protected Track modifyTrack(@Nullable Track track) { + return track; + } + + private DefaultSampleValues getDefaultSampleValues( + SparseArray<DefaultSampleValues> defaultSampleValuesArray, int trackId) { + if (defaultSampleValuesArray.size() == 1) { + // Ignore track id if there is only one track to cope with non-matching track indices. + // See https://github.com/google/ExoPlayer/issues/4477. + return defaultSampleValuesArray.valueAt(/* index= */ 0); + } + return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId)); + } + + private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { + parseMoof(moof, trackBundles, flags, scratchBytes); + + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); + if (drmInitData != null) { + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + trackBundles.valueAt(i).updateDrmInitData(drmInitData); + } + } + // If we have a pending seek, advance tracks to their preceding sync frames. + if (pendingSeekTimeUs != C.TIME_UNSET) { + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + trackBundles.valueAt(i).seek(pendingSeekTimeUs); + } + pendingSeekTimeUs = C.TIME_UNSET; + } + } + + private void maybeInitExtraTracks() { + if (emsgTrackOutputs == null) { + emsgTrackOutputs = new TrackOutput[2]; + int emsgTrackOutputCount = 0; + if (additionalEmsgTrackOutput != null) { + emsgTrackOutputs[emsgTrackOutputCount++] = additionalEmsgTrackOutput; + } + if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0) { + emsgTrackOutputs[emsgTrackOutputCount++] = + extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); + } + emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount); + + for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) { + eventMessageTrackOutput.format(EMSG_FORMAT); + } + } + if (cea608TrackOutputs == null) { + cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()]; + for (int i = 0; i < cea608TrackOutputs.length; i++) { + TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT); + output.format(closedCaptionFormats.get(i)); + cea608TrackOutputs[i] = output; + } + } + } + + /** Handles an emsg atom (defined in 23009-1). */ + private void onEmsgLeafAtomRead(ParsableByteArray atom) { + if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) { + return; + } + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + String schemeIdUri; + String value; + long timescale; + long presentationTimeDeltaUs = C.TIME_UNSET; // Only set if version == 0 + long sampleTimeUs = C.TIME_UNSET; + long durationMs; + long id; + switch (version) { + case 0: + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + timescale = atom.readUnsignedInt(); + presentationTimeDeltaUs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { + sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + } + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + break; + case 1: + timescale = atom.readUnsignedInt(); + sampleTimeUs = + Util.scaleLargeTimestamp(atom.readUnsignedLongToLong(), C.MICROS_PER_SECOND, timescale); + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + break; + default: + Log.w(TAG, "Skipping unsupported emsg version: " + version); + return; + } + + byte[] messageData = new byte[atom.bytesLeft()]; + atom.readBytes(messageData, /*offset=*/ 0, atom.bytesLeft()); + EventMessage eventMessage = new EventMessage(schemeIdUri, value, durationMs, id, messageData); + ParsableByteArray encodedEventMessage = + new ParsableByteArray(eventMessageEncoder.encode(eventMessage)); + int sampleSize = encodedEventMessage.bytesLeft(); + + // Output the sample data. + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + encodedEventMessage.setPosition(0); + emsgTrackOutput.sampleData(encodedEventMessage, sampleSize); + } + + // Output the sample metadata. This is made a little complicated because emsg-v0 atoms + // have presentation time *delta* while v1 atoms have absolute presentation time. + if (sampleTimeUs == C.TIME_UNSET) { + // We need the first sample timestamp in the segment before we can output the metadata. + pendingMetadataSampleInfos.addLast( + new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); + pendingMetadataSampleBytes += sampleSize; + } else { + if (timestampAdjuster != null) { + sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); + } + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null); + } + } + } + + /** Parses a trex atom (defined in 14496-12). */ + private static Pair<Integer, DefaultSampleValues> parseTrex(ParsableByteArray trex) { + trex.setPosition(Atom.FULL_HEADER_SIZE); + int trackId = trex.readInt(); + int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1; + int defaultSampleDuration = trex.readUnsignedIntToInt(); + int defaultSampleSize = trex.readUnsignedIntToInt(); + int defaultSampleFlags = trex.readInt(); + + return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex, + defaultSampleDuration, defaultSampleSize, defaultSampleFlags)); + } + + /** + * Parses an mehd atom (defined in 14496-12). + */ + private static long parseMehd(ParsableByteArray mehd) { + mehd.setPosition(Atom.HEADER_SIZE); + int fullAtom = mehd.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + return version == 0 ? mehd.readUnsignedInt() : mehd.readUnsignedLongToLong(); + } + + private static void parseMoof(ContainerAtom moof, SparseArray<TrackBundle> trackBundleArray, + @Flags int flags, byte[] extendedTypeScratch) throws ParserException { + int moofContainerChildrenSize = moof.containerChildren.size(); + for (int i = 0; i < moofContainerChildrenSize; i++) { + Atom.ContainerAtom child = moof.containerChildren.get(i); + // TODO: Support multiple traf boxes per track in a single moof. + if (child.type == Atom.TYPE_traf) { + parseTraf(child, trackBundleArray, flags, extendedTypeScratch); + } + } + } + + /** + * Parses a traf atom (defined in 14496-12). + */ + private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray, + @Flags int flags, byte[] extendedTypeScratch) throws ParserException { + LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); + TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); + if (trackBundle == null) { + return; + } + + TrackFragment fragment = trackBundle.fragment; + long decodeTime = fragment.nextFragmentDecodeTime; + trackBundle.reset(); + + LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); + if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) { + decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); + } + + parseTruns(traf, trackBundle, decodeTime, flags); + + TrackEncryptionBox encryptionBox = trackBundle.track + .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + + LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); + if (saiz != null) { + parseSaiz(encryptionBox, saiz.data, fragment); + } + + LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); + if (saio != null) { + parseSaio(saio.data, fragment); + } + + LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); + if (senc != null) { + parseSenc(senc.data, fragment); + } + + LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); + LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); + if (sbgp != null && sgpd != null) { + parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null, + fragment); + } + + int leafChildrenSize = traf.leafChildren.size(); + for (int i = 0; i < leafChildrenSize; i++) { + LeafAtom atom = traf.leafChildren.get(i); + if (atom.type == Atom.TYPE_uuid) { + parseUuid(atom.data, fragment, extendedTypeScratch); + } + } + } + + private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime, + @Flags int flags) { + int trunCount = 0; + int totalSampleCount = 0; + List<LeafAtom> leafChildren = traf.leafChildren; + int leafChildrenSize = leafChildren.size(); + for (int i = 0; i < leafChildrenSize; i++) { + LeafAtom atom = leafChildren.get(i); + if (atom.type == Atom.TYPE_trun) { + ParsableByteArray trunData = atom.data; + trunData.setPosition(Atom.FULL_HEADER_SIZE); + int trunSampleCount = trunData.readUnsignedIntToInt(); + if (trunSampleCount > 0) { + totalSampleCount += trunSampleCount; + trunCount++; + } + } + } + trackBundle.currentTrackRunIndex = 0; + trackBundle.currentSampleInTrackRun = 0; + trackBundle.currentSampleIndex = 0; + trackBundle.fragment.initTables(trunCount, totalSampleCount); + + int trunIndex = 0; + int trunStartPosition = 0; + for (int i = 0; i < leafChildrenSize; i++) { + LeafAtom trun = leafChildren.get(i); + if (trun.type == Atom.TYPE_trun) { + trunStartPosition = parseTrun(trackBundle, trunIndex++, decodeTime, flags, trun.data, + trunStartPosition); + } + } + } + + private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, + TrackFragment out) throws ParserException { + int vectorSize = encryptionBox.perSampleIvSize; + saiz.setPosition(Atom.HEADER_SIZE); + int fullAtom = saiz.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + if ((flags & 0x01) == 1) { + saiz.skipBytes(8); + } + int defaultSampleInfoSize = saiz.readUnsignedByte(); + + int sampleCount = saiz.readUnsignedIntToInt(); + if (sampleCount != out.sampleCount) { + throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); + } + + int totalSize = 0; + if (defaultSampleInfoSize == 0) { + boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable; + for (int i = 0; i < sampleCount; i++) { + int sampleInfoSize = saiz.readUnsignedByte(); + totalSize += sampleInfoSize; + sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize; + } + } else { + boolean subsampleEncryption = defaultSampleInfoSize > vectorSize; + totalSize += defaultSampleInfoSize * sampleCount; + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); + } + out.initEncryptionData(totalSize); + } + + /** + * Parses a saio atom (defined in 14496-12). + * + * @param saio The saio atom to decode. + * @param out The {@link TrackFragment} to populate with data from the saio atom. + */ + private static void parseSaio(ParsableByteArray saio, TrackFragment out) throws ParserException { + saio.setPosition(Atom.HEADER_SIZE); + int fullAtom = saio.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + if ((flags & 0x01) == 1) { + saio.skipBytes(8); + } + + int entryCount = saio.readUnsignedIntToInt(); + if (entryCount != 1) { + // We only support one trun element currently, so always expect one entry. + throw new ParserException("Unexpected saio entry count: " + entryCount); + } + + int version = Atom.parseFullAtomVersion(fullAtom); + out.auxiliaryDataPosition += + version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong(); + } + + /** + * Parses a tfhd atom (defined in 14496-12), updates the corresponding {@link TrackFragment} and + * returns the {@link TrackBundle} of the corresponding {@link Track}. If the tfhd does not refer + * to any {@link TrackBundle}, {@code null} is returned and no changes are made. + * + * @param tfhd The tfhd atom to decode. + * @param trackBundles The track bundles, one of which corresponds to the tfhd atom being parsed. + * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd + * does not refer to any {@link TrackBundle}. + */ + private static TrackBundle parseTfhd( + ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles) { + tfhd.setPosition(Atom.HEADER_SIZE); + int fullAtom = tfhd.readInt(); + int atomFlags = Atom.parseFullAtomFlags(fullAtom); + int trackId = tfhd.readInt(); + TrackBundle trackBundle = getTrackBundle(trackBundles, trackId); + if (trackBundle == null) { + return null; + } + if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) { + long baseDataPosition = tfhd.readUnsignedLongToLong(); + trackBundle.fragment.dataPosition = baseDataPosition; + trackBundle.fragment.auxiliaryDataPosition = baseDataPosition; + } + + DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; + int defaultSampleDescriptionIndex = + ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) + ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; + int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; + int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size; + int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags; + trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex, + defaultSampleDuration, defaultSampleSize, defaultSampleFlags); + return trackBundle; + } + + private static @Nullable TrackBundle getTrackBundle( + SparseArray<TrackBundle> trackBundles, int trackId) { + if (trackBundles.size() == 1) { + // Ignore track id if there is only one track. This is either because we have a side-loaded + // track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see + // https://github.com/google/ExoPlayer/issues/4083). + return trackBundles.valueAt(/* index= */ 0); + } + return trackBundles.get(trackId); + } + + /** + * Parses a tfdt atom (defined in 14496-12). + * + * @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the + * media, expressed in the media's timescale. + */ + private static long parseTfdt(ParsableByteArray tfdt) { + tfdt.setPosition(Atom.HEADER_SIZE); + int fullAtom = tfdt.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt(); + } + + /** + * Parses a trun atom (defined in 14496-12). + * + * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into + * which parsed data should be placed. + * @param index Index of the track run in the fragment. + * @param decodeTime The decode time of the first sample in the fragment run. + * @param flags Flags to allow any required workaround to be executed. + * @param trun The trun atom to decode. + * @return The starting position of samples for the next run. + */ + private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime, + @Flags int flags, ParsableByteArray trun, int trackRunStart) { + trun.setPosition(Atom.HEADER_SIZE); + int fullAtom = trun.readInt(); + int atomFlags = Atom.parseFullAtomFlags(fullAtom); + + Track track = trackBundle.track; + TrackFragment fragment = trackBundle.fragment; + DefaultSampleValues defaultSampleValues = fragment.header; + + fragment.trunLength[index] = trun.readUnsignedIntToInt(); + fragment.trunDataPosition[index] = fragment.dataPosition; + if ((atomFlags & 0x01 /* data_offset_present */) != 0) { + fragment.trunDataPosition[index] += trun.readInt(); + } + + boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0; + int firstSampleFlags = defaultSampleValues.flags; + if (firstSampleFlagsPresent) { + firstSampleFlags = trun.readUnsignedIntToInt(); + } + + boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0; + boolean sampleSizesPresent = (atomFlags & 0x200 /* sample_size_present */) != 0; + boolean sampleFlagsPresent = (atomFlags & 0x400 /* sample_flags_present */) != 0; + boolean sampleCompositionTimeOffsetsPresent = + (atomFlags & 0x800 /* sample_composition_time_offsets_present */) != 0; + + // Offset to the entire video timeline. In the presence of B-frames this is usually used to + // ensure that the first frame's presentation timestamp is zero. + long edtsOffset = 0; + + // Currently we only support a single edit that moves the entire media timeline (indicated by + // duration == 0). Other uses of edit lists are uncommon and unsupported. + if (track.editListDurations != null && track.editListDurations.length == 1 + && track.editListDurations[0] == 0) { + edtsOffset = + Util.scaleLargeTimestamp( + track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); + } + + int[] sampleSizeTable = fragment.sampleSizeTable; + int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable; + long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable; + boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable; + + boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO + && (flags & FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0; + + int trackRunEnd = trackRunStart + fragment.trunLength[index]; + long timescale = track.timescale; + long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime; + for (int i = trackRunStart; i < trackRunEnd; i++) { + // Use trun values if present, otherwise tfhd, otherwise trex. + int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() + : defaultSampleValues.duration; + int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size; + int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags + : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; + if (sampleCompositionTimeOffsetsPresent) { + // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in + // version 0 trun boxes, however a significant number of streams violate the spec and use + // signed integers instead. It's safe to always decode sample offsets as signed integers + // here, because unsigned integers will still be parsed correctly (unless their top bit is + // set, which is never true in practice because sample offsets are always small). + int sampleOffset = trun.readInt(); + sampleCompositionTimeOffsetTable[i] = + (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale); + } else { + sampleCompositionTimeOffsetTable[i] = 0; + } + sampleDecodingTimeTable[i] = + Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; + sampleSizeTable[i] = sampleSize; + sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 + && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); + cumulativeTime += sampleDuration; + } + fragment.nextFragmentDecodeTime = cumulativeTime; + return trackRunEnd; + } + + private static void parseUuid(ParsableByteArray uuid, TrackFragment out, + byte[] extendedTypeScratch) throws ParserException { + uuid.setPosition(Atom.HEADER_SIZE); + uuid.readBytes(extendedTypeScratch, 0, 16); + + // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox. + if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) { + return; + } + + // Except for the extended type, this box is identical to a SENC box. See "Portable encoding of + // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al, + // Section 5.3.2.1." + parseSenc(uuid, 16, out); + } + + private static void parseSenc(ParsableByteArray senc, TrackFragment out) throws ParserException { + parseSenc(senc, 0, out); + } + + private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) + throws ParserException { + senc.setPosition(Atom.HEADER_SIZE + offset); + int fullAtom = senc.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + + if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) { + // TODO: Implement this. + throw new ParserException("Overriding TrackEncryptionBox parameters is unsupported."); + } + + boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0; + int sampleCount = senc.readUnsignedIntToInt(); + if (sampleCount != out.sampleCount) { + throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); + } + + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); + out.initEncryptionData(senc.bytesLeft()); + out.fillEncryptionData(senc); + } + + private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType, + TrackFragment out) throws ParserException { + sbgp.setPosition(Atom.HEADER_SIZE); + int sbgpFullAtom = sbgp.readInt(); + if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { + // Only seig grouping type is supported. + return; + } + if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) { + sbgp.skipBytes(4); // default_length. + } + if (sbgp.readInt() != 1) { // entry_count. + throw new ParserException("Entry count in sbgp != 1 (unsupported)."); + } + + sgpd.setPosition(Atom.HEADER_SIZE); + int sgpdFullAtom = sgpd.readInt(); + if (sgpd.readInt() != SAMPLE_GROUP_TYPE_seig) { + // Only seig grouping type is supported. + return; + } + int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom); + if (sgpdVersion == 1) { + if (sgpd.readUnsignedInt() == 0) { + throw new ParserException("Variable length description in sgpd found (unsupported)"); + } + } else if (sgpdVersion >= 2) { + sgpd.skipBytes(4); // default_sample_description_index. + } + if (sgpd.readUnsignedInt() != 1) { // entry_count. + throw new ParserException("Entry count in sgpd != 1 (unsupported)."); + } + // CencSampleEncryptionInformationGroupEntry + sgpd.skipBytes(1); // reserved = 0. + int patternByte = sgpd.readUnsignedByte(); + int cryptByteBlock = (patternByte & 0xF0) >> 4; + int skipByteBlock = patternByte & 0x0F; + boolean isProtected = sgpd.readUnsignedByte() == 1; + if (!isProtected) { + return; + } + int perSampleIvSize = sgpd.readUnsignedByte(); + byte[] keyId = new byte[16]; + sgpd.readBytes(keyId, 0, keyId.length); + byte[] constantIv = null; + if (perSampleIvSize == 0) { + int constantIvSize = sgpd.readUnsignedByte(); + constantIv = new byte[constantIvSize]; + sgpd.readBytes(constantIv, 0, constantIvSize); + } + out.definesEncryptionData = true; + out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, perSampleIvSize, keyId, + cryptByteBlock, skipByteBlock, constantIv); + } + + /** + * Parses a sidx atom (defined in 14496-12). + * + * @param atom The atom data. + * @param inputPosition The input position of the first byte after the atom. + * @return A pair consisting of the earliest presentation time in microseconds, and the parsed + * {@link ChunkIndex}. + */ + private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition) + throws ParserException { + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + + atom.skipBytes(4); + long timescale = atom.readUnsignedInt(); + long earliestPresentationTime; + long offset = inputPosition; + if (version == 0) { + earliestPresentationTime = atom.readUnsignedInt(); + offset += atom.readUnsignedInt(); + } else { + earliestPresentationTime = atom.readUnsignedLongToLong(); + offset += atom.readUnsignedLongToLong(); + } + long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime, + C.MICROS_PER_SECOND, timescale); + + atom.skipBytes(2); + + int referenceCount = atom.readUnsignedShort(); + int[] sizes = new int[referenceCount]; + long[] offsets = new long[referenceCount]; + long[] durationsUs = new long[referenceCount]; + long[] timesUs = new long[referenceCount]; + + long time = earliestPresentationTime; + long timeUs = earliestPresentationTimeUs; + for (int i = 0; i < referenceCount; i++) { + int firstInt = atom.readInt(); + + int type = 0x80000000 & firstInt; + if (type != 0) { + throw new ParserException("Unhandled indirect reference"); + } + long referenceDuration = atom.readUnsignedInt(); + + sizes[i] = 0x7FFFFFFF & firstInt; + offsets[i] = offset; + + // Calculate time and duration values such that any rounding errors are consistent. i.e. That + // timesUs[i] + durationsUs[i] == timesUs[i + 1]. + timesUs[i] = timeUs; + time += referenceDuration; + timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); + durationsUs[i] = timeUs - timesUs[i]; + + atom.skipBytes(4); + offset += sizes[i]; + } + + return Pair.create(earliestPresentationTimeUs, + new ChunkIndex(sizes, offsets, durationsUs, timesUs)); + } + + private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { + TrackBundle nextTrackBundle = null; + long nextDataOffset = Long.MAX_VALUE; + int trackBundlesSize = trackBundles.size(); + for (int i = 0; i < trackBundlesSize; i++) { + TrackFragment trackFragment = trackBundles.valueAt(i).fragment; + if (trackFragment.sampleEncryptionDataNeedsFill + && trackFragment.auxiliaryDataPosition < nextDataOffset) { + nextDataOffset = trackFragment.auxiliaryDataPosition; + nextTrackBundle = trackBundles.valueAt(i); + } + } + if (nextTrackBundle == null) { + parserState = STATE_READING_SAMPLE_START; + return; + } + int bytesToSkip = (int) (nextDataOffset - input.getPosition()); + if (bytesToSkip < 0) { + throw new ParserException("Offset to encryption data was negative."); + } + input.skipFully(bytesToSkip); + nextTrackBundle.fragment.fillEncryptionData(input); + } + + /** + * Attempts to read the next sample in the current mdat atom. The read sample may be output or + * skipped. + * + * <p>If there are no more samples in the current mdat atom then the parser state is transitioned + * to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned. + * + * <p>It is possible for a sample to be partially read in the case that an exception is thrown. In + * this case the method can be called again to read the remainder of the sample. + * + * @param input The {@link ExtractorInput} from which to read data. + * @return Whether a sample was read. The read sample may have been output or skipped. False + * indicates that there are no samples left to read in the current mdat. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { + if (parserState == STATE_READING_SAMPLE_START) { + if (currentTrackBundle == null) { + TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles); + if (currentTrackBundle == null) { + // We've run out of samples in the current mdat. Discard any trailing data and prepare to + // read the header of the next atom. + int bytesToSkip = (int) (endOfMdatPosition - input.getPosition()); + if (bytesToSkip < 0) { + throw new ParserException("Offset to end of mdat was negative."); + } + input.skipFully(bytesToSkip); + enterReadingAtomHeaderState(); + return false; + } + + long nextDataPosition = currentTrackBundle.fragment + .trunDataPosition[currentTrackBundle.currentTrackRunIndex]; + // We skip bytes preceding the next sample to read. + int bytesToSkip = (int) (nextDataPosition - input.getPosition()); + if (bytesToSkip < 0) { + // Assume the sample data must be contiguous in the mdat with no preceding data. + Log.w(TAG, "Ignoring negative offset to sample data."); + bytesToSkip = 0; + } + input.skipFully(bytesToSkip); + this.currentTrackBundle = currentTrackBundle; + } + + sampleSize = currentTrackBundle.fragment + .sampleSizeTable[currentTrackBundle.currentSampleIndex]; + + if (currentTrackBundle.currentSampleIndex < currentTrackBundle.firstSampleToOutputIndex) { + input.skipFully(sampleSize); + currentTrackBundle.skipSampleEncryptionData(); + if (!currentTrackBundle.next()) { + currentTrackBundle = null; + } + parserState = STATE_READING_SAMPLE_START; + return true; + } + + if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { + sampleSize -= Atom.HEADER_SIZE; + input.skipFully(Atom.HEADER_SIZE); + } + + if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) { + // AC4 samples need to be prefixed with a clear sample header. + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE); + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } else { + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0); + } + sampleSize += sampleBytesWritten; + parserState = STATE_READING_SAMPLE_CONTINUE; + sampleCurrentNalBytesRemaining = 0; + } + + TrackFragment fragment = currentTrackBundle.fragment; + Track track = currentTrackBundle.track; + TrackOutput output = currentTrackBundle.output; + int sampleIndex = currentTrackBundle.currentSampleIndex; + long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; + if (timestampAdjuster != null) { + sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); + } + if (track.nalUnitLengthFieldLength != 0) { + // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalPrefixData = nalPrefix.data; + nalPrefixData[0] = 0; + nalPrefixData[1] = 0; + nalPrefixData[2] = 0; + int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1; + int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + while (sampleBytesWritten < sampleSize) { + if (sampleCurrentNalBytesRemaining == 0) { + // Read the NAL length so that we know where we find the next one, and its type. + input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength); + nalPrefix.setPosition(0); + int nalLengthInt = nalPrefix.readInt(); + if (nalLengthInt < 1) { + throw new ParserException("Invalid NAL length"); + } + sampleCurrentNalBytesRemaining = nalLengthInt - 1; + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + output.sampleData(nalStartCode, 4); + // Write the NAL unit type byte. + output.sampleData(nalPrefix, 1); + processSeiNalUnitPayload = cea608TrackOutputs.length > 0 + && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); + sampleBytesWritten += 5; + sampleSize += nalUnitLengthFieldLengthDiff; + } else { + int writtenBytes; + if (processSeiNalUnitPayload) { + // Read and write the payload of the SEI NAL unit. + nalBuffer.reset(sampleCurrentNalBytesRemaining); + input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining); + output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining); + writtenBytes = sampleCurrentNalBytesRemaining; + // Unescape and process the SEI NAL unit. + int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); + // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. + nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); + nalBuffer.setLimit(unescapedLength); + CeaUtil.consume(sampleTimeUs, nalBuffer, cea608TrackOutputs); + } else { + // Write the payload of the NAL unit. + writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + } + sampleBytesWritten += writtenBytes; + sampleCurrentNalBytesRemaining -= writtenBytes; + } + } + } else { + while (sampleBytesWritten < sampleSize) { + int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesWritten += writtenBytes; + } + } + + @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex] + ? C.BUFFER_FLAG_KEY_FRAME : 0; + + // Encryption data. + TrackOutput.CryptoData cryptoData = null; + TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted(); + if (encryptionBox != null) { + sampleFlags |= C.BUFFER_FLAG_ENCRYPTED; + cryptoData = encryptionBox.cryptoData; + } + + output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData); + + // After we have the sampleTimeUs, we can commit all the pending metadata samples + outputPendingMetadataSamples(sampleTimeUs); + if (!currentTrackBundle.next()) { + currentTrackBundle = null; + } + parserState = STATE_READING_SAMPLE_START; + return true; + } + + private void outputPendingMetadataSamples(long sampleTimeUs) { + while (!pendingMetadataSampleInfos.isEmpty()) { + MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); + pendingMetadataSampleBytes -= sampleInfo.size; + long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs; + if (timestampAdjuster != null) { + metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs); + } + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + metadataTimeUs, + C.BUFFER_FLAG_KEY_FRAME, + sampleInfo.size, + pendingMetadataSampleBytes, + null); + } + } + } + + /** + * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those + * yet to be consumed, or null if all have been consumed. + */ + private static TrackBundle getNextFragmentRun(SparseArray<TrackBundle> trackBundles) { + TrackBundle nextTrackBundle = null; + long nextTrackRunOffset = Long.MAX_VALUE; + + int trackBundlesSize = trackBundles.size(); + for (int i = 0; i < trackBundlesSize; i++) { + TrackBundle trackBundle = trackBundles.valueAt(i); + if (trackBundle.currentTrackRunIndex == trackBundle.fragment.trunCount) { + // This track fragment contains no more runs in the next mdat box. + } else { + long trunOffset = trackBundle.fragment.trunDataPosition[trackBundle.currentTrackRunIndex]; + if (trunOffset < nextTrackRunOffset) { + nextTrackBundle = trackBundle; + nextTrackRunOffset = trunOffset; + } + } + } + return nextTrackBundle; + } + + /** Returns DrmInitData from leaf atoms. */ + @Nullable + private static DrmInitData getDrmInitDataFromAtoms(List<Atom.LeafAtom> leafChildren) { + ArrayList<SchemeData> schemeDatas = null; + int leafChildrenSize = leafChildren.size(); + for (int i = 0; i < leafChildrenSize; i++) { + LeafAtom child = leafChildren.get(i); + if (child.type == Atom.TYPE_pssh) { + if (schemeDatas == null) { + schemeDatas = new ArrayList<>(); + } + byte[] psshData = child.data.data; + UUID uuid = PsshAtomUtil.parseUuid(psshData); + if (uuid == null) { + Log.w(TAG, "Skipped pssh atom (failed to extract uuid)"); + } else { + schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData)); + } + } + } + return schemeDatas == null ? null : new DrmInitData(schemeDatas); + } + + /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */ + private static boolean shouldParseLeafAtom(int atom) { + return atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd + || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd || atom == Atom.TYPE_tfdt + || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_trex + || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz + || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid + || atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst + || atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg; + } + + /** Returns whether the extractor should decode a container atom with type {@code atom}. */ + private static boolean shouldParseContainerAtom(int atom) { + return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia + || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_moof + || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts; + } + + /** + * Holds data corresponding to a metadata sample. + */ + private static final class MetadataSampleInfo { + + public final long presentationTimeDeltaUs; + public final int size; + + public MetadataSampleInfo(long presentationTimeDeltaUs, int size) { + this.presentationTimeDeltaUs = presentationTimeDeltaUs; + this.size = size; + } + + } + + /** + * Holds data corresponding to a single track. + */ + private static final class TrackBundle { + + private static final int SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH = 8; + + public final TrackOutput output; + public final TrackFragment fragment; + public final ParsableByteArray scratch; + + public Track track; + public DefaultSampleValues defaultSampleValues; + public int currentSampleIndex; + public int currentSampleInTrackRun; + public int currentTrackRunIndex; + public int firstSampleToOutputIndex; + + private final ParsableByteArray encryptionSignalByte; + private final ParsableByteArray defaultInitializationVector; + + public TrackBundle(TrackOutput output) { + this.output = output; + fragment = new TrackFragment(); + scratch = new ParsableByteArray(); + encryptionSignalByte = new ParsableByteArray(1); + defaultInitializationVector = new ParsableByteArray(); + } + + public void init(Track track, DefaultSampleValues defaultSampleValues) { + this.track = Assertions.checkNotNull(track); + this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues); + output.format(track.format); + reset(); + } + + public void updateDrmInitData(DrmInitData drmInitData) { + TrackEncryptionBox encryptionBox = + track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; + output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType))); + } + + /** Resets the current fragment and sample indices. */ + public void reset() { + fragment.reset(); + currentSampleIndex = 0; + currentTrackRunIndex = 0; + currentSampleInTrackRun = 0; + firstSampleToOutputIndex = 0; + } + + /** + * Advances {@link #firstSampleToOutputIndex} to point to the sync sample before the specified + * seek time in the current fragment. + * + * @param timeUs The seek time, in microseconds. + */ + public void seek(long timeUs) { + long timeMs = C.usToMs(timeUs); + int searchIndex = currentSampleIndex; + while (searchIndex < fragment.sampleCount + && fragment.getSamplePresentationTime(searchIndex) < timeMs) { + if (fragment.sampleIsSyncFrameTable[searchIndex]) { + firstSampleToOutputIndex = searchIndex; + } + searchIndex++; + } + } + + /** + * Advances the indices in the bundle to point to the next sample in the current fragment. If + * the current sample is the last one in the current fragment, then the advanced state will be + * {@code currentSampleIndex == fragment.sampleCount}, {@code currentTrackRunIndex == + * fragment.trunCount} and {@code #currentSampleInTrackRun == 0}. + * + * @return Whether the next sample is in the same track run as the previous one. + */ + public boolean next() { + currentSampleIndex++; + currentSampleInTrackRun++; + if (currentSampleInTrackRun == fragment.trunLength[currentTrackRunIndex]) { + currentTrackRunIndex++; + currentSampleInTrackRun = 0; + return false; + } + return true; + } + + /** + * Outputs the encryption data for the current sample. + * + * @param sampleSize The size of the current sample in bytes, excluding any additional clear + * header that will be prefixed to the sample by the extractor. + * @param clearHeaderSize The size of a clear header that will be prefixed to the sample by the + * extractor, or 0. + * @return The number of written bytes. + */ + public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) { + TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + if (encryptionBox == null) { + return 0; + } + + ParsableByteArray initializationVectorData; + int vectorSize; + if (encryptionBox.perSampleIvSize != 0) { + initializationVectorData = fragment.sampleEncryptionData; + vectorSize = encryptionBox.perSampleIvSize; + } else { + // The default initialization vector should be used. + byte[] initVectorData = encryptionBox.defaultInitializationVector; + defaultInitializationVector.reset(initVectorData, initVectorData.length); + initializationVectorData = defaultInitializationVector; + vectorSize = initVectorData.length; + } + + boolean haveSubsampleEncryptionTable = + fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean writeSubsampleEncryptionData = haveSubsampleEncryptionTable || clearHeaderSize != 0; + + // Write the signal byte, containing the vector size and the subsample encryption flag. + encryptionSignalByte.data[0] = + (byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0)); + encryptionSignalByte.setPosition(0); + output.sampleData(encryptionSignalByte, 1); + // Write the vector. + output.sampleData(initializationVectorData, vectorSize); + + if (!writeSubsampleEncryptionData) { + return 1 + vectorSize; + } + + if (!haveSubsampleEncryptionTable) { + // The sample is fully encrypted, except for the additional clear header that the extractor + // is going to prefix. We need to synthesize subsample encryption data that takes the header + // into account. + scratch.reset(SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + // subsampleCount = 1 (unsigned short) + scratch.data[0] = (byte) 0; + scratch.data[1] = (byte) 1; + // clearDataSize = clearHeaderSize (unsigned short) + scratch.data[2] = (byte) ((clearHeaderSize >> 8) & 0xFF); + scratch.data[3] = (byte) (clearHeaderSize & 0xFF); + // encryptedDataSize = sampleSize (unsigned short) + scratch.data[4] = (byte) ((sampleSize >> 24) & 0xFF); + scratch.data[5] = (byte) ((sampleSize >> 16) & 0xFF); + scratch.data[6] = (byte) ((sampleSize >> 8) & 0xFF); + scratch.data[7] = (byte) (sampleSize & 0xFF); + output.sampleData(scratch, SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + return 1 + vectorSize + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH; + } + + ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; + int subsampleCount = subsampleEncryptionData.readUnsignedShort(); + subsampleEncryptionData.skipBytes(-2); + int subsampleDataLength = 2 + 6 * subsampleCount; + + if (clearHeaderSize != 0) { + // We need to account for the additional clear header by adding clearHeaderSize to + // clearDataSize for the first subsample specified in the subsample encryption data. + scratch.reset(subsampleDataLength); + scratch.readBytes(subsampleEncryptionData.data, /* offset= */ 0, subsampleDataLength); + subsampleEncryptionData.skipBytes(subsampleDataLength); + + int clearDataSize = (scratch.data[2] & 0xFF) << 8 | (scratch.data[3] & 0xFF); + int adjustedClearDataSize = clearDataSize + clearHeaderSize; + scratch.data[2] = (byte) ((adjustedClearDataSize >> 8) & 0xFF); + scratch.data[3] = (byte) (adjustedClearDataSize & 0xFF); + subsampleEncryptionData = scratch; + } + + output.sampleData(subsampleEncryptionData, subsampleDataLength); + return 1 + vectorSize + subsampleDataLength; + } + + /** Skips the encryption data for the current sample. */ + private void skipSampleEncryptionData() { + TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + if (encryptionBox == null) { + return; + } + + ParsableByteArray sampleEncryptionData = fragment.sampleEncryptionData; + if (encryptionBox.perSampleIvSize != 0) { + sampleEncryptionData.skipBytes(encryptionBox.perSampleIvSize); + } + if (fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex)) { + sampleEncryptionData.skipBytes(6 * sampleEncryptionData.readUnsignedShort()); + } + } + + private TrackEncryptionBox getEncryptionBoxIfEncrypted() { + int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; + TrackEncryptionBox encryptionBox = + fragment.trackEncryptionBox != null + ? fragment.trackEncryptionBox + : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); + return encryptionBox != null && encryptionBox.isEncrypted ? encryptionBox : null; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java new file mode 100644 index 0000000000..7040df6425 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format + * Specification. + */ +public final class MdtaMetadataEntry implements Metadata.Entry { + + /** The metadata key name. */ + public final String key; + /** The payload. The interpretation of the value depends on {@link #typeIndicator}. */ + public final byte[] value; + /** The four byte locale indicator. */ + public final int localeIndicator; + /** The four byte type indicator. */ + public final int typeIndicator; + + /** Creates a new metadata entry for the specified metadata key/value. */ + public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) { + this.key = key; + this.value = value; + this.localeIndicator = localeIndicator; + this.typeIndicator = typeIndicator; + } + + private MdtaMetadataEntry(Parcel in) { + key = Util.castNonNull(in.readString()); + value = new byte[in.readInt()]; + in.readByteArray(value); + localeIndicator = in.readInt(); + typeIndicator = in.readInt(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MdtaMetadataEntry other = (MdtaMetadataEntry) obj; + return key.equals(other.key) + && Arrays.equals(value, other.value) + && localeIndicator == other.localeIndicator + && typeIndicator == other.typeIndicator; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + key.hashCode(); + result = 31 * result + Arrays.hashCode(value); + result = 31 * result + localeIndicator; + result = 31 * result + typeIndicator; + return result; + } + + @Override + public String toString() { + return "mdta: key=" + key; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeInt(value.length); + dest.writeByteArray(value); + dest.writeInt(localeIndicator); + dest.writeInt(typeIndicator); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<MdtaMetadataEntry> CREATOR = + new Parcelable.Creator<MdtaMetadataEntry>() { + + @Override + public MdtaMetadataEntry createFromParcel(Parcel in) { + return new MdtaMetadataEntry(in); + } + + @Override + public MdtaMetadataEntry[] newArray(int size) { + return new MdtaMetadataEntry[size]; + } + }; +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java new file mode 100644 index 0000000000..7d4de0e498 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -0,0 +1,588 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.ApicFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.CommentFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Frame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.InternalFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; + +/** Utilities for handling metadata in MP4. */ +/* package */ final class MetadataUtil { + + private static final String TAG = "MetadataUtil"; + + // Codes that start with the copyright character (omitted) and have equivalent ID3 frames. + private static final int SHORT_TYPE_NAME_1 = 0x006e616d; + private static final int SHORT_TYPE_NAME_2 = 0x0074726b; + private static final int SHORT_TYPE_COMMENT = 0x00636d74; + private static final int SHORT_TYPE_YEAR = 0x00646179; + private static final int SHORT_TYPE_ARTIST = 0x00415254; + private static final int SHORT_TYPE_ENCODER = 0x00746f6f; + private static final int SHORT_TYPE_ALBUM = 0x00616c62; + private static final int SHORT_TYPE_COMPOSER_1 = 0x00636f6d; + private static final int SHORT_TYPE_COMPOSER_2 = 0x00777274; + private static final int SHORT_TYPE_LYRICS = 0x006c7972; + private static final int SHORT_TYPE_GENRE = 0x0067656e; + + // Codes that have equivalent ID3 frames. + private static final int TYPE_COVER_ART = 0x636f7672; + private static final int TYPE_GENRE = 0x676e7265; + private static final int TYPE_GROUPING = 0x00677270; + private static final int TYPE_DISK_NUMBER = 0x6469736b; + private static final int TYPE_TRACK_NUMBER = 0x74726b6e; + private static final int TYPE_TEMPO = 0x746d706f; + private static final int TYPE_COMPILATION = 0x6370696c; + private static final int TYPE_ALBUM_ARTIST = 0x61415254; + private static final int TYPE_SORT_TRACK_NAME = 0x736f6e6d; + private static final int TYPE_SORT_ALBUM = 0x736f616c; + private static final int TYPE_SORT_ARTIST = 0x736f6172; + private static final int TYPE_SORT_ALBUM_ARTIST = 0x736f6161; + private static final int TYPE_SORT_COMPOSER = 0x736f636f; + + // Types that do not have equivalent ID3 frames. + private static final int TYPE_RATING = 0x72746e67; + private static final int TYPE_GAPLESS_ALBUM = 0x70676170; + private static final int TYPE_TV_SORT_SHOW = 0x736f736e; + private static final int TYPE_TV_SHOW = 0x74767368; + + // Type for items that are intended for internal use by the player. + private static final int TYPE_INTERNAL = 0x2d2d2d2d; + + private static final int PICTURE_TYPE_FRONT_COVER = 3; + + // Standard genres. + @VisibleForTesting + /* package */ static final String[] STANDARD_GENRES = + new String[] { + // These are the official ID3v1 genres. + "Blues", + "Classic Rock", + "Country", + "Dance", + "Disco", + "Funk", + "Grunge", + "Hip-Hop", + "Jazz", + "Metal", + "New Age", + "Oldies", + "Other", + "Pop", + "R&B", + "Rap", + "Reggae", + "Rock", + "Techno", + "Industrial", + "Alternative", + "Ska", + "Death Metal", + "Pranks", + "Soundtrack", + "Euro-Techno", + "Ambient", + "Trip-Hop", + "Vocal", + "Jazz+Funk", + "Fusion", + "Trance", + "Classical", + "Instrumental", + "Acid", + "House", + "Game", + "Sound Clip", + "Gospel", + "Noise", + "AlternRock", + "Bass", + "Soul", + "Punk", + "Space", + "Meditative", + "Instrumental Pop", + "Instrumental Rock", + "Ethnic", + "Gothic", + "Darkwave", + "Techno-Industrial", + "Electronic", + "Pop-Folk", + "Eurodance", + "Dream", + "Southern Rock", + "Comedy", + "Cult", + "Gangsta", + "Top 40", + "Christian Rap", + "Pop/Funk", + "Jungle", + "Native American", + "Cabaret", + "New Wave", + "Psychadelic", + "Rave", + "Showtunes", + "Trailer", + "Lo-Fi", + "Tribal", + "Acid Punk", + "Acid Jazz", + "Polka", + "Retro", + "Musical", + "Rock & Roll", + "Hard Rock", + // Genres made up by the authors of Winamp (v1.91) and later added to the ID3 spec. + "Folk", + "Folk-Rock", + "National Folk", + "Swing", + "Fast Fusion", + "Bebob", + "Latin", + "Revival", + "Celtic", + "Bluegrass", + "Avantgarde", + "Gothic Rock", + "Progressive Rock", + "Psychedelic Rock", + "Symphonic Rock", + "Slow Rock", + "Big Band", + "Chorus", + "Easy Listening", + "Acoustic", + "Humour", + "Speech", + "Chanson", + "Opera", + "Chamber Music", + "Sonata", + "Symphony", + "Booty Bass", + "Primus", + "Porn Groove", + "Satire", + "Slow Jam", + "Club", + "Tango", + "Samba", + "Folklore", + "Ballad", + "Power Ballad", + "Rhythmic Soul", + "Freestyle", + "Duet", + "Punk Rock", + "Drum Solo", + "A capella", + "Euro-House", + "Dance Hall", + // Genres made up by the authors of Winamp (v1.91) but have not been added to the ID3 spec. + "Goa", + "Drum & Bass", + "Club-House", + "Hardcore", + "Terror", + "Indie", + "BritPop", + "Afro-Punk", + "Polsk Punk", + "Beat", + "Christian Gangsta Rap", + "Heavy Metal", + "Black Metal", + "Crossover", + "Contemporary Christian", + "Christian Rock", + "Merengue", + "Salsa", + "Thrash Metal", + "Anime", + "Jpop", + "Synthpop", + // Genres made up by the authors of Winamp (v5.6) but have not been added to the ID3 spec. + "Abstract", + "Art Rock", + "Baroque", + "Bhangra", + "Big beat", + "Breakbeat", + "Chillout", + "Downtempo", + "Dub", + "EBM", + "Eclectic", + "Electro", + "Electroclash", + "Emo", + "Experimental", + "Garage", + "Global", + "IDM", + "Illbient", + "Industro-Goth", + "Jam Band", + "Krautrock", + "Leftfield", + "Lounge", + "Math Rock", + "New Romantic", + "Nu-Breakz", + "Post-Punk", + "Post-Rock", + "Psytrance", + "Shoegaze", + "Space Rock", + "Trop Rock", + "World Music", + "Neoclassical", + "Audiobook", + "Audio theatre", + "Neue Deutsche Welle", + "Podcast", + "Indie-Rock", + "G-Funk", + "Dubstep", + "Garage Rock", + "Psybient" + }; + + private static final String LANGUAGE_UNDEFINED = "und"; + + private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9; + private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD. + + private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps"; + private static final int MDTA_TYPE_INDICATOR_FLOAT = 23; + + private MetadataUtil() {} + + /** + * Returns a {@link Format} that is the same as the input format but includes information from the + * specified sources of metadata. + */ + public static Format getFormatWithMetadata( + int trackType, + Format format, + @Nullable Metadata udtaMetadata, + @Nullable Metadata mdtaMetadata, + GaplessInfoHolder gaplessInfoHolder) { + if (trackType == C.TRACK_TYPE_AUDIO) { + if (gaplessInfoHolder.hasGaplessInfo()) { + format = + format.copyWithGaplessInfo( + gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding); + } + // We assume all udta metadata is associated with the audio track. + if (udtaMetadata != null) { + format = format.copyWithMetadata(udtaMetadata); + } + } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) { + // Populate only metadata keys that are known to be specific to video. + for (int i = 0; i < mdtaMetadata.length(); i++) { + Metadata.Entry entry = mdtaMetadata.get(i); + if (entry instanceof MdtaMetadataEntry) { + MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry; + if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key) + && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) { + try { + float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get(); + format = format.copyWithFrameRate(fps); + format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry)); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring invalid framerate"); + } + } + } + } + } + return format; + } + + /** + * Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read + * starting from the current position of the {@link ParsableByteArray}, and the position is + * advanced by the size of the element. The position is advanced even if the element's type is + * unrecognized. + * + * @param ilst Holds the data to be parsed. + * @return The parsed element, or null if the element's type was not recognized. + */ + @Nullable + public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) { + int position = ilst.getPosition(); + int endPosition = position + ilst.readInt(); + int type = ilst.readInt(); + int typeTopByte = (type >> 24) & 0xFF; + try { + if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) { + int shortType = type & 0x00FFFFFF; + if (shortType == SHORT_TYPE_COMMENT) { + return parseCommentAttribute(type, ilst); + } else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) { + return parseTextAttribute(type, "TIT2", ilst); + } else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) { + return parseTextAttribute(type, "TCOM", ilst); + } else if (shortType == SHORT_TYPE_YEAR) { + return parseTextAttribute(type, "TDRC", ilst); + } else if (shortType == SHORT_TYPE_ARTIST) { + return parseTextAttribute(type, "TPE1", ilst); + } else if (shortType == SHORT_TYPE_ENCODER) { + return parseTextAttribute(type, "TSSE", ilst); + } else if (shortType == SHORT_TYPE_ALBUM) { + return parseTextAttribute(type, "TALB", ilst); + } else if (shortType == SHORT_TYPE_LYRICS) { + return parseTextAttribute(type, "USLT", ilst); + } else if (shortType == SHORT_TYPE_GENRE) { + return parseTextAttribute(type, "TCON", ilst); + } else if (shortType == TYPE_GROUPING) { + return parseTextAttribute(type, "TIT1", ilst); + } + } else if (type == TYPE_GENRE) { + return parseStandardGenreAttribute(ilst); + } else if (type == TYPE_DISK_NUMBER) { + return parseIndexAndCountAttribute(type, "TPOS", ilst); + } else if (type == TYPE_TRACK_NUMBER) { + return parseIndexAndCountAttribute(type, "TRCK", ilst); + } else if (type == TYPE_TEMPO) { + return parseUint8Attribute(type, "TBPM", ilst, true, false); + } else if (type == TYPE_COMPILATION) { + return parseUint8Attribute(type, "TCMP", ilst, true, true); + } else if (type == TYPE_COVER_ART) { + return parseCoverArt(ilst); + } else if (type == TYPE_ALBUM_ARTIST) { + return parseTextAttribute(type, "TPE2", ilst); + } else if (type == TYPE_SORT_TRACK_NAME) { + return parseTextAttribute(type, "TSOT", ilst); + } else if (type == TYPE_SORT_ALBUM) { + return parseTextAttribute(type, "TSO2", ilst); + } else if (type == TYPE_SORT_ARTIST) { + return parseTextAttribute(type, "TSOA", ilst); + } else if (type == TYPE_SORT_ALBUM_ARTIST) { + return parseTextAttribute(type, "TSOP", ilst); + } else if (type == TYPE_SORT_COMPOSER) { + return parseTextAttribute(type, "TSOC", ilst); + } else if (type == TYPE_RATING) { + return parseUint8Attribute(type, "ITUNESADVISORY", ilst, false, false); + } else if (type == TYPE_GAPLESS_ALBUM) { + return parseUint8Attribute(type, "ITUNESGAPLESS", ilst, false, true); + } else if (type == TYPE_TV_SORT_SHOW) { + return parseTextAttribute(type, "TVSHOWSORT", ilst); + } else if (type == TYPE_TV_SHOW) { + return parseTextAttribute(type, "TVSHOW", ilst); + } else if (type == TYPE_INTERNAL) { + return parseInternalAttribute(ilst, endPosition); + } + Log.d(TAG, "Skipped unknown metadata entry: " + Atom.getAtomTypeString(type)); + return null; + } finally { + ilst.setPosition(endPosition); + } + } + + /** + * Parses an 'mdta' metadata entry starting at the current position in an ilst box. + * + * @param ilst The ilst box. + * @param endPosition The end position of the entry in the ilst box. + * @param key The mdta metadata entry key for the entry. + * @return The parsed element, or null if the entry wasn't recognized. + */ + @Nullable + public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst( + ParsableByteArray ilst, int endPosition, String key) { + int atomPosition; + while ((atomPosition = ilst.getPosition()) < endPosition) { + int atomSize = ilst.readInt(); + int atomType = ilst.readInt(); + if (atomType == Atom.TYPE_data) { + int typeIndicator = ilst.readInt(); + int localeIndicator = ilst.readInt(); + int dataSize = atomSize - 16; + byte[] value = new byte[dataSize]; + ilst.readBytes(value, 0, dataSize); + return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator); + } + ilst.setPosition(atomPosition + atomSize); + } + return null; + } + + @Nullable + private static TextInformationFrame parseTextAttribute( + int type, String id, ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(atomSize - 16); + return new TextInformationFrame(id, /* description= */ null, value); + } + Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + @Nullable + private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(atomSize - 16); + return new CommentFrame(LANGUAGE_UNDEFINED, value, value); + } + Log.w(TAG, "Failed to parse comment attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + @Nullable + private static Id3Frame parseUint8Attribute( + int type, + String id, + ParsableByteArray data, + boolean isTextInformationFrame, + boolean isBoolean) { + int value = parseUint8AttributeValue(data); + if (isBoolean) { + value = Math.min(1, value); + } + if (value >= 0) { + return isTextInformationFrame + ? new TextInformationFrame(id, /* description= */ null, Integer.toString(value)) + : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value)); + } + Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + @Nullable + private static TextInformationFrame parseIndexAndCountAttribute( + int type, String attributeName, ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data && atomSize >= 22) { + data.skipBytes(10); // version (1), flags (3), empty (4), empty (2) + int index = data.readUnsignedShort(); + if (index > 0) { + String value = "" + index; + int count = data.readUnsignedShort(); + if (count > 0) { + value += "/" + count; + } + return new TextInformationFrame(attributeName, /* description= */ null, value); + } + } + Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + @Nullable + private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) { + int genreCode = parseUint8AttributeValue(data); + String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) + ? STANDARD_GENRES[genreCode - 1] : null; + if (genreString != null) { + return new TextInformationFrame("TCON", /* description= */ null, genreString); + } + Log.w(TAG, "Failed to parse standard genre code"); + return null; + } + + @Nullable + private static ApicFrame parseCoverArt(ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + int fullVersionInt = data.readInt(); + int flags = Atom.parseFullAtomFlags(fullVersionInt); + String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null; + if (mimeType == null) { + Log.w(TAG, "Unrecognized cover art flags: " + flags); + return null; + } + data.skipBytes(4); // empty (4) + byte[] pictureData = new byte[atomSize - 16]; + data.readBytes(pictureData, 0, pictureData.length); + return new ApicFrame( + mimeType, + /* description= */ null, + /* pictureType= */ PICTURE_TYPE_FRONT_COVER, + pictureData); + } + Log.w(TAG, "Failed to parse cover art attribute"); + return null; + } + + @Nullable + private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) { + String domain = null; + String name = null; + int dataAtomPosition = -1; + int dataAtomSize = -1; + while (data.getPosition() < endPosition) { + int atomPosition = data.getPosition(); + int atomSize = data.readInt(); + int atomType = data.readInt(); + data.skipBytes(4); // version (1), flags (3) + if (atomType == Atom.TYPE_mean) { + domain = data.readNullTerminatedString(atomSize - 12); + } else if (atomType == Atom.TYPE_name) { + name = data.readNullTerminatedString(atomSize - 12); + } else { + if (atomType == Atom.TYPE_data) { + dataAtomPosition = atomPosition; + dataAtomSize = atomSize; + } + data.skipBytes(atomSize - 12); + } + } + if (domain == null || name == null || dataAtomPosition == -1) { + return null; + } + data.setPosition(dataAtomPosition); + data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(dataAtomSize - 16); + return new InternalFrame(domain, name, value); + } + + private static int parseUint8AttributeValue(ParsableByteArray data) { + data.skipBytes(4); // atomSize + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + return data.readUnsignedByte(); + } + Log.w(TAG, "Failed to parse uint8 attribute value"); + return -1; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java new file mode 100644 index 0000000000..254cad1eb1 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -0,0 +1,824 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; + +/** + * Extracts data from the MP4 container format. + */ +public final class Mp4Extractor implements Extractor, SeekMap { + + /** Factory for {@link Mp4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp4Extractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + public @interface Flags {} + /** + * Flag to ignore any edit lists in the stream. + */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1; + + /** Parser states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_READING_ATOM_HEADER, STATE_READING_ATOM_PAYLOAD, STATE_READING_SAMPLE}) + private @interface State {} + + private static final int STATE_READING_ATOM_HEADER = 0; + private static final int STATE_READING_ATOM_PAYLOAD = 1; + private static final int STATE_READING_SAMPLE = 2; + + /** Brand stored in the ftyp atom for QuickTime media. */ + private static final int BRAND_QUICKTIME = 0x71742020; + + /** + * When seeking within the source, if the offset is greater than or equal to this value (or the + * offset is negative), the source will be reloaded. + */ + private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; + + /** + * For poorly interleaved streams, the maximum byte difference one track is allowed to be read + * ahead before the source will be reloaded at a new position to read another track. + */ + private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 10 * 1024 * 1024; + + private final @Flags int flags; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalLength; + private final ParsableByteArray scratch; + + private final ParsableByteArray atomHeader; + private final ArrayDeque<ContainerAtom> containerAtoms; + + @State private int parserState; + private int atomType; + private long atomSize; + private int atomHeaderBytesRead; + private ParsableByteArray atomData; + + private int sampleTrackIndex; + private int sampleBytesRead; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; + + // Extractor outputs. + private ExtractorOutput extractorOutput; + private Mp4Track[] tracks; + private long[][] accumulatedSampleSizes; + private int firstVideoTrackIndex; + private long durationUs; + private boolean isQuickTime; + + /** + * Creates a new extractor for unfragmented MP4 streams. + */ + public Mp4Extractor() { + this(0); + } + + /** + * Creates a new extractor for unfragmented MP4 streams, using the specified flags to control the + * extractor's behavior. + * + * @param flags Flags that control the extractor's behavior. + */ + public Mp4Extractor(@Flags int flags) { + this.flags = flags; + atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); + containerAtoms = new ArrayDeque<>(); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalLength = new ParsableByteArray(4); + scratch = new ParsableByteArray(); + sampleTrackIndex = C.INDEX_UNSET; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return Sniffer.sniffUnfragmented(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + } + + @Override + public void seek(long position, long timeUs) { + containerAtoms.clear(); + atomHeaderBytesRead = 0; + sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + if (position == 0) { + enterReadingAtomHeaderState(); + } else if (tracks != null) { + updateSampleIndices(timeUs); + } + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + while (true) { + switch (parserState) { + case STATE_READING_ATOM_HEADER: + if (!readAtomHeader(input)) { + return RESULT_END_OF_INPUT; + } + break; + case STATE_READING_ATOM_PAYLOAD: + if (readAtomPayload(input, seekPosition)) { + return RESULT_SEEK; + } + break; + case STATE_READING_SAMPLE: + return readSample(input, seekPosition); + default: + throw new IllegalStateException(); + } + } + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + if (tracks.length == 0) { + return new SeekPoints(SeekPoint.START); + } + + long firstTimeUs; + long firstOffset; + long secondTimeUs = C.TIME_UNSET; + long secondOffset = C.POSITION_UNSET; + + // If we have a video track, use it to establish one or two seek points. + if (firstVideoTrackIndex != C.INDEX_UNSET) { + TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable; + int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs); + if (sampleIndex == C.INDEX_UNSET) { + return new SeekPoints(SeekPoint.START); + } + long sampleTimeUs = sampleTable.timestampsUs[sampleIndex]; + firstTimeUs = sampleTimeUs; + firstOffset = sampleTable.offsets[sampleIndex]; + if (sampleTimeUs < timeUs && sampleIndex < sampleTable.sampleCount - 1) { + int secondSampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + if (secondSampleIndex != C.INDEX_UNSET && secondSampleIndex != sampleIndex) { + secondTimeUs = sampleTable.timestampsUs[secondSampleIndex]; + secondOffset = sampleTable.offsets[secondSampleIndex]; + } + } + } else { + firstTimeUs = timeUs; + firstOffset = Long.MAX_VALUE; + } + + // Take into account other tracks. + for (int i = 0; i < tracks.length; i++) { + if (i != firstVideoTrackIndex) { + TrackSampleTable sampleTable = tracks[i].sampleTable; + firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset); + if (secondTimeUs != C.TIME_UNSET) { + secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset); + } + } + } + + SeekPoint firstSeekPoint = new SeekPoint(firstTimeUs, firstOffset); + if (secondTimeUs == C.TIME_UNSET) { + return new SeekPoints(firstSeekPoint); + } else { + SeekPoint secondSeekPoint = new SeekPoint(secondTimeUs, secondOffset); + return new SeekPoints(firstSeekPoint, secondSeekPoint); + } + } + + // Private methods. + + private void enterReadingAtomHeaderState() { + parserState = STATE_READING_ATOM_HEADER; + atomHeaderBytesRead = 0; + } + + private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { + if (atomHeaderBytesRead == 0) { + // Read the standard length atom header. + if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { + return false; + } + atomHeaderBytesRead = Atom.HEADER_SIZE; + atomHeader.setPosition(0); + atomSize = atomHeader.readUnsignedInt(); + atomType = atomHeader.readInt(); + } + + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large size. + int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; + input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); + atomHeaderBytesRead += headerBytesRemaining; + atomSize = atomHeader.readUnsignedLongToLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. Note that if the atom is within a container we can + // work out its size even if the input length is unknown. + long endPosition = input.getLength(); + if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { + endPosition = containerAtoms.peek().endPosition; + } + if (endPosition != C.LENGTH_UNSET) { + atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; + } + } + + if (atomSize < atomHeaderBytesRead) { + throw new ParserException("Atom size less than header length (unsupported)."); + } + + if (shouldParseContainerAtom(atomType)) { + long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; + if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) { + maybeSkipRemainingMetaAtomHeaderBytes(input); + } + containerAtoms.push(new ContainerAtom(atomType, endPosition)); + if (atomSize == atomHeaderBytesRead) { + processAtomEnded(endPosition); + } else { + // Start reading the first child atom. + enterReadingAtomHeaderState(); + } + } else if (shouldParseLeafAtom(atomType)) { + // We don't support parsing of leaf atoms that define extended atom sizes, or that have + // lengths greater than Integer.MAX_VALUE. + Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE); + Assertions.checkState(atomSize <= Integer.MAX_VALUE); + atomData = new ParsableByteArray((int) atomSize); + System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE); + parserState = STATE_READING_ATOM_PAYLOAD; + } else { + atomData = null; + parserState = STATE_READING_ATOM_PAYLOAD; + } + + return true; + } + + /** + * Processes the atom payload. If {@link #atomData} is null and the size is at or above the + * threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should + * restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped. + */ + private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder) + throws IOException, InterruptedException { + long atomPayloadSize = atomSize - atomHeaderBytesRead; + long atomEndPosition = input.getPosition() + atomPayloadSize; + boolean seekRequired = false; + if (atomData != null) { + input.readFully(atomData.data, atomHeaderBytesRead, (int) atomPayloadSize); + if (atomType == Atom.TYPE_ftyp) { + isQuickTime = processFtypAtom(atomData); + } else if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData)); + } + } else { + // We don't need the data. Skip or seek, depending on how large the atom is. + if (atomPayloadSize < RELOAD_MINIMUM_SEEK_DISTANCE) { + input.skipFully((int) atomPayloadSize); + } else { + positionHolder.position = input.getPosition() + atomPayloadSize; + seekRequired = true; + } + } + processAtomEnded(atomEndPosition); + return seekRequired && parserState != STATE_READING_SAMPLE; + } + + private void processAtomEnded(long atomEndPosition) throws ParserException { + while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) { + Atom.ContainerAtom containerAtom = containerAtoms.pop(); + if (containerAtom.type == Atom.TYPE_moov) { + // We've reached the end of the moov atom. Process it and prepare to read samples. + processMoovAtom(containerAtom); + containerAtoms.clear(); + parserState = STATE_READING_SAMPLE; + } else if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(containerAtom); + } + } + if (parserState != STATE_READING_SAMPLE) { + enterReadingAtomHeaderState(); + } + } + + /** + * Updates the stored track metadata to reflect the contents of the specified moov atom. + */ + private void processMoovAtom(ContainerAtom moov) throws ParserException { + int firstVideoTrackIndex = C.INDEX_UNSET; + long durationUs = C.TIME_UNSET; + List<Mp4Track> tracks = new ArrayList<>(); + + // Process metadata. + Metadata udtaMetadata = null; + GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); + Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); + if (udta != null) { + udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime); + if (udtaMetadata != null) { + gaplessInfoHolder.setFromMetadata(udtaMetadata); + } + } + Metadata mdtaMetadata = null; + Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta); + if (meta != null) { + mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta); + } + + boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0; + ArrayList<TrackSampleTable> trackSampleTables = + getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists); + + int trackCount = trackSampleTables.size(); + for (int i = 0; i < trackCount; i++) { + TrackSampleTable trackSampleTable = trackSampleTables.get(i); + Track track = trackSampleTable.track; + long trackDurationUs = + track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs; + durationUs = Math.max(durationUs, trackDurationUs); + Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, + extractorOutput.track(i, track.type)); + + // Each sample has up to three bytes of overhead for the start code that replaces its length. + // Allow ten source samples per output sample, like the platform extractor. + int maxInputSize = trackSampleTable.maximumSize + 3 * 10; + Format format = track.format.copyWithMaxInputSize(maxInputSize); + if (track.type == C.TRACK_TYPE_VIDEO + && trackDurationUs > 0 + && trackSampleTable.sampleCount > 1) { + float frameRate = trackSampleTable.sampleCount / (trackDurationUs / 1000000f); + format = format.copyWithFrameRate(frameRate); + } + format = + MetadataUtil.getFormatWithMetadata( + track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder); + mp4Track.trackOutput.format(format); + + if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) { + firstVideoTrackIndex = tracks.size(); + } + tracks.add(mp4Track); + } + this.firstVideoTrackIndex = firstVideoTrackIndex; + this.durationUs = durationUs; + this.tracks = tracks.toArray(new Mp4Track[0]); + accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks); + + extractorOutput.endTracks(); + extractorOutput.seekMap(this); + } + + private ArrayList<TrackSampleTable> getTrackSampleTables( + ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists) + throws ParserException { + ArrayList<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; + } + Track track = + AtomParsers.parseTrak( + atom, + moov.getLeafAtomOfType(Atom.TYPE_mvhd), + /* duration= */ C.TIME_UNSET, + /* drmInitData= */ null, + ignoreEditLists, + isQuickTime); + if (track == null) { + continue; + } + Atom.ContainerAtom stblAtom = + atom.getContainerAtomOfType(Atom.TYPE_mdia) + .getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); + TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); + if (trackSampleTable.sampleCount == 0) { + continue; + } + trackSampleTables.add(trackSampleTable); + } + return trackSampleTables; + } + + /** + * Attempts to extract the next sample in the current mdat atom for the specified track. + * <p> + * Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in + * {@code positionHolder}. + * <p> + * Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns + * {@link #RESULT_CONTINUE}. + * + * @param input The {@link ExtractorInput} from which to read data. + * @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the + * position of the required data. + * @return One of the {@code RESULT_*} flags in {@link Extractor}. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int readSample(ExtractorInput input, PositionHolder positionHolder) + throws IOException, InterruptedException { + long inputPosition = input.getPosition(); + if (sampleTrackIndex == C.INDEX_UNSET) { + sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition); + if (sampleTrackIndex == C.INDEX_UNSET) { + return RESULT_END_OF_INPUT; + } + } + Mp4Track track = tracks[sampleTrackIndex]; + TrackOutput trackOutput = track.trackOutput; + int sampleIndex = track.sampleIndex; + long position = track.sampleTable.offsets[sampleIndex]; + int sampleSize = track.sampleTable.sizes[sampleIndex]; + long skipAmount = position - inputPosition + sampleBytesRead; + if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { + positionHolder.position = position; + return RESULT_SEEK; + } + if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { + // The sample information is contained in a cdat atom. The header must be discarded for + // committing. + skipAmount += Atom.HEADER_SIZE; + sampleSize -= Atom.HEADER_SIZE; + } + input.skipFully((int) skipAmount); + if (track.track.nalUnitLengthFieldLength != 0) { + // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalLengthData = nalLength.data; + nalLengthData[0] = 0; + nalLengthData[1] = 0; + nalLengthData[2] = 0; + int nalUnitLengthFieldLength = track.track.nalUnitLengthFieldLength; + int nalUnitLengthFieldLengthDiff = 4 - track.track.nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + while (sampleBytesWritten < sampleSize) { + if (sampleCurrentNalBytesRemaining == 0) { + // Read the NAL length so that we know where we find the next one. + input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; + nalLength.setPosition(0); + int nalLengthInt = nalLength.readInt(); + if (nalLengthInt < 0) { + throw new ParserException("Invalid NAL length"); + } + sampleCurrentNalBytesRemaining = nalLengthInt; + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + trackOutput.sampleData(nalStartCode, 4); + sampleBytesWritten += 4; + sampleSize += nalUnitLengthFieldLengthDiff; + } else { + // Write the payload of the NAL unit. + int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); + sampleBytesRead += writtenBytes; + sampleBytesWritten += writtenBytes; + sampleCurrentNalBytesRemaining -= writtenBytes; + } + } + } else { + if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) { + if (sampleBytesWritten == 0) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } + sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; + } + while (sampleBytesWritten < sampleSize) { + int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesRead += writtenBytes; + sampleBytesWritten += writtenBytes; + sampleCurrentNalBytesRemaining -= writtenBytes; + } + } + trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex], + track.sampleTable.flags[sampleIndex], sampleSize, 0, null); + track.sampleIndex++; + sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + return RESULT_CONTINUE; + } + + /** + * Returns the index of the track that contains the next sample to be read, or {@link + * C#INDEX_UNSET} if no samples remain. + * + * <p>The preferred choice is the sample with the smallest offset not requiring a source reload, + * or if not available the sample with the smallest overall offset to avoid subsequent source + * reloads. + * + * <p>To deal with poor sample interleaving, we also check whether the required memory to catch up + * with the next logical sample (based on sample time) exceeds {@link + * #MAXIMUM_READ_AHEAD_BYTES_STREAM}. If this is the case, we continue with this sample even + * though it may require a source reload. + */ + private int getTrackIndexOfNextReadSample(long inputPosition) { + long preferredSkipAmount = Long.MAX_VALUE; + boolean preferredRequiresReload = true; + int preferredTrackIndex = C.INDEX_UNSET; + long preferredAccumulatedBytes = Long.MAX_VALUE; + long minAccumulatedBytes = Long.MAX_VALUE; + boolean minAccumulatedBytesRequiresReload = true; + int minAccumulatedBytesTrackIndex = C.INDEX_UNSET; + for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) { + Mp4Track track = tracks[trackIndex]; + int sampleIndex = track.sampleIndex; + if (sampleIndex == track.sampleTable.sampleCount) { + continue; + } + long sampleOffset = track.sampleTable.offsets[sampleIndex]; + long sampleAccumulatedBytes = accumulatedSampleSizes[trackIndex][sampleIndex]; + long skipAmount = sampleOffset - inputPosition; + boolean requiresReload = skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE; + if ((!requiresReload && preferredRequiresReload) + || (requiresReload == preferredRequiresReload && skipAmount < preferredSkipAmount)) { + preferredRequiresReload = requiresReload; + preferredSkipAmount = skipAmount; + preferredTrackIndex = trackIndex; + preferredAccumulatedBytes = sampleAccumulatedBytes; + } + if (sampleAccumulatedBytes < minAccumulatedBytes) { + minAccumulatedBytes = sampleAccumulatedBytes; + minAccumulatedBytesRequiresReload = requiresReload; + minAccumulatedBytesTrackIndex = trackIndex; + } + } + return minAccumulatedBytes == Long.MAX_VALUE + || !minAccumulatedBytesRequiresReload + || preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM + ? preferredTrackIndex + : minAccumulatedBytesTrackIndex; + } + + /** + * Updates every track's sample index to point its latest sync sample before/at {@code timeUs}. + */ + private void updateSampleIndices(long timeUs) { + for (Mp4Track track : tracks) { + TrackSampleTable sampleTable = track.sampleTable; + int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + if (sampleIndex == C.INDEX_UNSET) { + // Handle the case where the requested time is before the first synchronization sample. + sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + } + track.sampleIndex = sampleIndex; + } + } + + /** + * Possibly skips the version and flags fields (1+3 byte) of a full meta atom of the {@code + * input}. + * + * <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. + */ + private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input) + throws IOException, InterruptedException { + scratch.reset(8); + // Peek the next 8 bytes which 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. + input.peekFully(scratch.data, 0, 8); + scratch.skipBytes(4); + if (scratch.readInt() == Atom.TYPE_hdlr) { + input.resetPeekPosition(); + } else { + input.skipFully(4); + } + } + + /** + * For each sample of each track, calculates accumulated size of all samples which need to be read + * before this sample can be used. + */ + private static long[][] calculateAccumulatedSampleSizes(Mp4Track[] tracks) { + long[][] accumulatedSampleSizes = new long[tracks.length][]; + int[] nextSampleIndex = new int[tracks.length]; + long[] nextSampleTimesUs = new long[tracks.length]; + boolean[] tracksFinished = new boolean[tracks.length]; + for (int i = 0; i < tracks.length; i++) { + accumulatedSampleSizes[i] = new long[tracks[i].sampleTable.sampleCount]; + nextSampleTimesUs[i] = tracks[i].sampleTable.timestampsUs[0]; + } + long accumulatedSampleSize = 0; + int finishedTracks = 0; + while (finishedTracks < tracks.length) { + long minTimeUs = Long.MAX_VALUE; + int minTimeTrackIndex = -1; + for (int i = 0; i < tracks.length; i++) { + if (!tracksFinished[i] && nextSampleTimesUs[i] <= minTimeUs) { + minTimeTrackIndex = i; + minTimeUs = nextSampleTimesUs[i]; + } + } + int trackSampleIndex = nextSampleIndex[minTimeTrackIndex]; + accumulatedSampleSizes[minTimeTrackIndex][trackSampleIndex] = accumulatedSampleSize; + accumulatedSampleSize += tracks[minTimeTrackIndex].sampleTable.sizes[trackSampleIndex]; + nextSampleIndex[minTimeTrackIndex] = ++trackSampleIndex; + if (trackSampleIndex < accumulatedSampleSizes[minTimeTrackIndex].length) { + nextSampleTimesUs[minTimeTrackIndex] = + tracks[minTimeTrackIndex].sampleTable.timestampsUs[trackSampleIndex]; + } else { + tracksFinished[minTimeTrackIndex] = true; + finishedTracks++; + } + } + return accumulatedSampleSizes; + } + + /** + * Adjusts a seek point offset to take into account the track with the given {@code sampleTable}, + * for a given {@code seekTimeUs}. + * + * @param sampleTable The sample table to use. + * @param seekTimeUs The seek time in microseconds. + * @param offset The current offset. + * @return The adjusted offset. + */ + private static long maybeAdjustSeekOffset( + TrackSampleTable sampleTable, long seekTimeUs, long offset) { + int sampleIndex = getSynchronizationSampleIndex(sampleTable, seekTimeUs); + if (sampleIndex == C.INDEX_UNSET) { + return offset; + } + long sampleOffset = sampleTable.offsets[sampleIndex]; + return Math.min(sampleOffset, offset); + } + + /** + * Returns the index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} if + * there are no synchronization samples in the table. + * + * @param sampleTable The sample table in which to locate a synchronization sample. + * @param timeUs A time in microseconds. + * @return The index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} + * if there are no synchronization samples in the table. + */ + private static int getSynchronizationSampleIndex(TrackSampleTable sampleTable, long timeUs) { + int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + if (sampleIndex == C.INDEX_UNSET) { + // Handle the case where the requested time is before the first synchronization sample. + sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + } + return sampleIndex; + } + + /** + * Process an ftyp atom to determine whether the media is QuickTime. + * + * @param atomData The ftyp atom data. + * @return Whether the media is QuickTime. + */ + private static boolean processFtypAtom(ParsableByteArray atomData) { + atomData.setPosition(Atom.HEADER_SIZE); + int majorBrand = atomData.readInt(); + if (majorBrand == BRAND_QUICKTIME) { + return true; + } + atomData.skipBytes(4); // minor_version + while (atomData.bytesLeft() > 0) { + if (atomData.readInt() == BRAND_QUICKTIME) { + return true; + } + } + return false; + } + + /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */ + private static boolean shouldParseLeafAtom(int atom) { + return atom == Atom.TYPE_mdhd + || atom == Atom.TYPE_mvhd + || atom == Atom.TYPE_hdlr + || atom == Atom.TYPE_stsd + || atom == Atom.TYPE_stts + || atom == Atom.TYPE_stss + || atom == Atom.TYPE_ctts + || atom == Atom.TYPE_elst + || atom == Atom.TYPE_stsc + || atom == Atom.TYPE_stsz + || atom == Atom.TYPE_stz2 + || atom == Atom.TYPE_stco + || atom == Atom.TYPE_co64 + || atom == Atom.TYPE_tkhd + || atom == Atom.TYPE_ftyp + || atom == Atom.TYPE_udta + || atom == Atom.TYPE_keys + || atom == Atom.TYPE_ilst; + } + + /** Returns whether the extractor should decode a container atom with type {@code atom}. */ + private static boolean shouldParseContainerAtom(int atom) { + return atom == Atom.TYPE_moov + || atom == Atom.TYPE_trak + || atom == Atom.TYPE_mdia + || atom == Atom.TYPE_minf + || atom == Atom.TYPE_stbl + || atom == Atom.TYPE_edts + || atom == Atom.TYPE_meta; + } + + private static final class Mp4Track { + + public final Track track; + public final TrackSampleTable sampleTable; + public final TrackOutput trackOutput; + + public int sampleIndex; + + public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) { + this.track = track; + this.sampleTable = sampleTable; + this.trackOutput = trackOutput; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java new file mode 100644 index 0000000000..ddb13aeb9c --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -0,0 +1,208 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * Utility methods for handling PSSH atoms. + */ +public final class PsshAtomUtil { + + private static final String TAG = "PsshAtomUtil"; + + private PsshAtomUtil() {} + + /** + * Builds a version 0 PSSH atom for a given system id, containing the given data. + * + * @param systemId The system id of the scheme. + * @param data The scheme specific data. + * @return The PSSH atom. + */ + public static byte[] buildPsshAtom(UUID systemId, @Nullable byte[] data) { + return buildPsshAtom(systemId, null, data); + } + + /** + * Builds a PSSH atom for the given system id, containing the given key ids and data. + * + * @param systemId The system id of the scheme. + * @param keyIds The key ids for a version 1 PSSH atom, or null for a version 0 PSSH atom. + * @param data The scheme specific data. + * @return The PSSH atom. + */ + // dereference of possibly-null reference keyId + @SuppressWarnings({"ParameterNotNullable", "nullness:dereference.of.nullable"}) + public static byte[] buildPsshAtom( + UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { + int dataLength = data != null ? data.length : 0; + int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength; + if (keyIds != null) { + psshBoxLength += 4 /* KID_count */ + (keyIds.length * 16) /* KIDs */; + } + ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength); + psshBox.putInt(psshBoxLength); + psshBox.putInt(Atom.TYPE_pssh); + psshBox.putInt(keyIds != null ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */); + psshBox.putLong(systemId.getMostSignificantBits()); + psshBox.putLong(systemId.getLeastSignificantBits()); + if (keyIds != null) { + psshBox.putInt(keyIds.length); + for (UUID keyId : keyIds) { + psshBox.putLong(keyId.getMostSignificantBits()); + psshBox.putLong(keyId.getLeastSignificantBits()); + } + } + if (data != null && data.length != 0) { + psshBox.putInt(data.length); + psshBox.put(data); + } // Else the last 4 bytes are a 0 DataSize. + return psshBox.array(); + } + + /** + * Returns whether the data is a valid PSSH atom. + * + * @param data The data to parse. + * @return Whether the data is a valid PSSH atom. + */ + public static boolean isPsshAtom(byte[] data) { + return parsePsshAtom(data) != null; + } + + /** + * Parses the UUID from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + * + * <p>The UUID is only parsed if the data is a valid PSSH atom. + * + * @param atom The atom to parse. + * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has an + * unsupported version. + */ + public static @Nullable UUID parseUuid(byte[] atom) { + PsshAtom parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return null; + } + return parsedAtom.uuid; + } + + /** + * Parses the version from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + * <p> + * The version is only parsed if the data is a valid PSSH atom. + * + * @param atom The atom to parse. + * @return The parsed version. -1 if the input is not a valid PSSH atom, or if the PSSH atom has + * an unsupported version. + */ + public static int parseVersion(byte[] atom) { + PsshAtom parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return -1; + } + return parsedAtom.version; + } + + /** + * Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + * + * <p>The scheme specific data is only parsed if the data is a valid PSSH atom matching the given + * UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null. + * + * @param atom The atom to parse. + * @param uuid The required UUID of the PSSH atom, or null to accept any UUID. + * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the + * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID. + */ + public static @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { + PsshAtom parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return null; + } + if (uuid != null && !uuid.equals(parsedAtom.uuid)) { + Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.uuid + "."); + return null; + } + return parsedAtom.schemeData; + } + + /** + * Parses a PSSH atom. Version 0 and 1 PSSH atoms are supported. + * + * @param atom The atom to parse. + * @return The parsed PSSH atom. Null if the input is not a valid PSSH atom, or if the PSSH atom + * has an unsupported version. + */ + // TODO: Support parsing of the key ids for version 1 PSSH atoms. + private static @Nullable PsshAtom parsePsshAtom(byte[] atom) { + ParsableByteArray atomData = new ParsableByteArray(atom); + if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { + // Data too short. + return null; + } + atomData.setPosition(0); + int atomSize = atomData.readInt(); + if (atomSize != atomData.bytesLeft() + 4) { + // Not an atom, or incorrect atom size. + return null; + } + int atomType = atomData.readInt(); + if (atomType != Atom.TYPE_pssh) { + // Not an atom, or incorrect atom type. + return null; + } + int atomVersion = Atom.parseFullAtomVersion(atomData.readInt()); + if (atomVersion > 1) { + Log.w(TAG, "Unsupported pssh version: " + atomVersion); + return null; + } + UUID uuid = new UUID(atomData.readLong(), atomData.readLong()); + if (atomVersion == 1) { + int keyIdCount = atomData.readUnsignedIntToInt(); + atomData.skipBytes(16 * keyIdCount); + } + int dataSize = atomData.readUnsignedIntToInt(); + if (dataSize != atomData.bytesLeft()) { + // Incorrect dataSize. + return null; + } + byte[] data = new byte[dataSize]; + atomData.readBytes(data, 0, dataSize); + return new PsshAtom(uuid, atomVersion, data); + } + + // TODO: Consider exposing this and making parsePsshAtom public. + private static class PsshAtom { + + private final UUID uuid; + private final int version; + private final byte[] schemeData; + + public PsshAtom(UUID uuid, int version, byte[] schemeData) { + this.uuid = uuid; + this.version = version; + this.schemeData = schemeData; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java new file mode 100644 index 0000000000..d58c2f06eb --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -0,0 +1,201 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** + * Provides methods that peek data from an {@link ExtractorInput} and return whether the input + * appears to be in MP4 format. + */ +/* package */ final class Sniffer { + + /** The maximum number of bytes to peek when sniffing. */ + private static final int SEARCH_LENGTH = 4 * 1024; + + private static final int[] COMPATIBLE_BRANDS = + new int[] { + 0x69736f6d, // isom + 0x69736f32, // iso2 + 0x69736f33, // iso3 + 0x69736f34, // iso4 + 0x69736f35, // iso5 + 0x69736f36, // iso6 + 0x61766331, // avc1 + 0x68766331, // hvc1 + 0x68657631, // hev1 + 0x61763031, // av01 + 0x6d703431, // mp41 + 0x6d703432, // mp42 + 0x33673261, // 3g2a + 0x33673262, // 3g2b + 0x33677236, // 3gr6 + 0x33677336, // 3gs6 + 0x33676536, // 3ge6 + 0x33676736, // 3gg6 + 0x4d345620, // M4V[space] + 0x4d344120, // M4A[space] + 0x66347620, // f4v[space] + 0x6b646469, // kddi + 0x4d345650, // M4VP + 0x71742020, // qt[space][space], Apple QuickTime + 0x4d534e56, // MSNV, Sony PSP + 0x64627931, // dby1, Dolby Vision + }; + + /** + * Returns whether data peeked from the current position in {@code input} is consistent with the + * input being a fragmented MP4 file. + * + * @param input The extractor input from which to peek data. The peek position will be modified. + * @return Whether the input appears to be in the fragmented MP4 format. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static boolean sniffFragmented(ExtractorInput input) + throws IOException, InterruptedException { + return sniffInternal(input, true); + } + + /** + * Returns whether data peeked from the current position in {@code input} is consistent with the + * input being an unfragmented MP4 file. + * + * @param input The extractor input from which to peek data. The peek position will be modified. + * @return Whether the input appears to be in the unfragmented MP4 format. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static boolean sniffUnfragmented(ExtractorInput input) + throws IOException, InterruptedException { + return sniffInternal(input, false); + } + + private static boolean sniffInternal(ExtractorInput input, boolean fragmented) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH + ? SEARCH_LENGTH : inputLength); + + ParsableByteArray buffer = new ParsableByteArray(64); + int bytesSearched = 0; + boolean foundGoodFileType = false; + boolean isFragmented = false; + while (bytesSearched < bytesToSearch) { + // Read an atom header. + int headerSize = Atom.HEADER_SIZE; + buffer.reset(headerSize); + input.peekFully(buffer.data, 0, headerSize); + long atomSize = buffer.readUnsignedInt(); + int atomType = buffer.readInt(); + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large atom size. + headerSize = Atom.LONG_HEADER_SIZE; + input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE); + buffer.setLimit(Atom.LONG_HEADER_SIZE); + atomSize = buffer.readLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. + long fileEndPosition = input.getLength(); + if (fileEndPosition != C.LENGTH_UNSET) { + atomSize = fileEndPosition - input.getPeekPosition() + headerSize; + } + } + + if (atomSize < headerSize) { + // The file is invalid because the atom size is too small for its header. + return false; + } + bytesSearched += headerSize; + + if (atomType == Atom.TYPE_moov) { + // We have seen the moov atom. We increase the search size to make sure we don't miss an + // mvex atom because the moov's size exceeds the search length. + bytesToSearch += (int) atomSize; + if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) { + // Make sure we don't exceed the file size. + bytesToSearch = (int) inputLength; + } + // Check for an mvex atom inside the moov atom to identify whether the file is fragmented. + continue; + } + + if (atomType == Atom.TYPE_moof || atomType == Atom.TYPE_mvex) { + // The movie is fragmented. Stop searching as we must have read any ftyp atom already. + isFragmented = true; + break; + } + + if (bytesSearched + atomSize - headerSize >= bytesToSearch) { + // Stop searching as peeking this atom would exceed the search limit. + break; + } + + int atomDataSize = (int) (atomSize - headerSize); + bytesSearched += atomDataSize; + if (atomType == Atom.TYPE_ftyp) { + // Parse the atom and check the file type/brand is compatible with the extractors. + if (atomDataSize < 8) { + return false; + } + buffer.reset(atomDataSize); + input.peekFully(buffer.data, 0, atomDataSize); + int brandsCount = atomDataSize / 4; + for (int i = 0; i < brandsCount; i++) { + if (i == 1) { + // This index refers to the minorVersion, not a brand, so skip it. + buffer.skipBytes(4); + } else if (isCompatibleBrand(buffer.readInt())) { + foundGoodFileType = true; + break; + } + } + if (!foundGoodFileType) { + // The types were not compatible and there is only one ftyp atom, so reject the file. + return false; + } + } else if (atomDataSize != 0) { + // Skip the atom. + input.advancePeekPosition(atomDataSize); + } + } + return foundGoodFileType && fragmented == isFragmented; + } + + /** + * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors. + */ + private static boolean isCompatibleBrand(int brand) { + // Accept all brands starting '3gp'. + if (brand >>> 8 == 0x00336770) { + return true; + } + for (int compatibleBrand : COMPATIBLE_BRANDS) { + if (compatibleBrand == brand) { + return true; + } + } + return false; + } + + private Sniffer() { + // Prevent instantiation. + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java new file mode 100644 index 0000000000..b7a1555a76 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -0,0 +1,148 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Encapsulates information describing an MP4 track. + */ +public final class Track { + + /** + * The transformation to apply to samples in the track, if any. One of {@link + * #TRANSFORMATION_NONE} or {@link #TRANSFORMATION_CEA608_CDAT}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({TRANSFORMATION_NONE, TRANSFORMATION_CEA608_CDAT}) + public @interface Transformation {} + /** + * A no-op sample transformation. + */ + public static final int TRANSFORMATION_NONE = 0; + /** + * A transformation for caption samples in cdat atoms. + */ + public static final int TRANSFORMATION_CEA608_CDAT = 1; + + /** + * The track identifier. + */ + public final int id; + + /** + * One of {@link C#TRACK_TYPE_AUDIO}, {@link C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_TEXT}. + */ + public final int type; + + /** + * The track timescale, defined as the number of time units that pass in one second. + */ + public final long timescale; + + /** + * The movie timescale. + */ + public final long movieTimescale; + + /** + * The duration of the track in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public final long durationUs; + + /** + * The format. + */ + public final Format format; + + /** + * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each + * sample. + */ + @Transformation public final int sampleTransformation; + + /** + * Durations of edit list segments in the movie timescale. Null if there is no edit list. + */ + @Nullable public final long[] editListDurations; + + /** + * Media times for edit list segments in the track timescale. Null if there is no edit list. + */ + @Nullable public final long[] editListMediaTimes; + + /** + * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for + * other track types. + */ + public final int nalUnitLengthFieldLength; + + @Nullable private final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; + + public Track(int id, int type, long timescale, long movieTimescale, long durationUs, + Format format, @Transformation int sampleTransformation, + @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, + @Nullable long[] editListDurations, @Nullable long[] editListMediaTimes) { + this.id = id; + this.type = type; + this.timescale = timescale; + this.movieTimescale = movieTimescale; + this.durationUs = durationUs; + this.format = format; + this.sampleTransformation = sampleTransformation; + this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes; + this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; + this.editListDurations = editListDurations; + this.editListMediaTimes = editListMediaTimes; + } + + /** + * Returns the {@link TrackEncryptionBox} for the given sample description index. + * + * @param sampleDescriptionIndex The given sample description index + * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no + * such entry exists. + */ + @Nullable + public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) { + return sampleDescriptionEncryptionBoxes == null ? null + : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; + } + + // incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") + public Track copyWithFormat(Format format) { + return new Track( + id, + type, + timescale, + movieTimescale, + durationUs, + format, + sampleTransformation, + sampleDescriptionEncryptionBoxes, + nalUnitLengthFieldLength, + editListDurations, + editListMediaTimes); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java new file mode 100644 index 0000000000..04bfb82210 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java @@ -0,0 +1,103 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; + +/** + * Encapsulates information parsed from a track encryption (tenc) box or sample group description + * (sgpd) box in an MP4 stream. + */ +public final class TrackEncryptionBox { + + private static final String TAG = "TrackEncryptionBox"; + + /** + * Indicates the encryption state of the samples in the sample group. + */ + public final boolean isEncrypted; + + /** + * The protection scheme type, as defined by the 'schm' box, or null if unknown. + */ + @Nullable public final String schemeType; + + /** + * A {@link TrackOutput.CryptoData} instance containing the encryption information from this + * {@link TrackEncryptionBox}. + */ + public final TrackOutput.CryptoData cryptoData; + + /** The initialization vector size in bytes for the samples in the corresponding sample group. */ + public final int perSampleIvSize; + + /** + * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the + * track encryption box or sample group description box. Null otherwise. + */ + @Nullable public final byte[] defaultInitializationVector; + + /** + * @param isEncrypted See {@link #isEncrypted}. + * @param schemeType See {@link #schemeType}. + * @param perSampleIvSize See {@link #perSampleIvSize}. + * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}. + * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}. + * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}. + * @param defaultInitializationVector See {@link #defaultInitializationVector}. + */ + public TrackEncryptionBox( + boolean isEncrypted, + @Nullable String schemeType, + int perSampleIvSize, + byte[] keyId, + int defaultEncryptedBlocks, + int defaultClearBlocks, + @Nullable byte[] defaultInitializationVector) { + Assertions.checkArgument(perSampleIvSize == 0 ^ defaultInitializationVector == null); + this.isEncrypted = isEncrypted; + this.schemeType = schemeType; + this.perSampleIvSize = perSampleIvSize; + this.defaultInitializationVector = defaultInitializationVector; + cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId, + defaultEncryptedBlocks, defaultClearBlocks); + } + + @C.CryptoMode + private static int schemeToCryptoMode(@Nullable String schemeType) { + if (schemeType == null) { + // If unknown, assume cenc. + return C.CRYPTO_MODE_AES_CTR; + } + switch (schemeType) { + case C.CENC_TYPE_cenc: + case C.CENC_TYPE_cens: + return C.CRYPTO_MODE_AES_CTR; + case C.CENC_TYPE_cbc1: + case C.CENC_TYPE_cbcs: + return C.CRYPTO_MODE_AES_CBC; + default: + Log.w(TAG, "Unsupported protection scheme type '" + schemeType + "'. Assuming AES-CTR " + + "crypto mode."); + return C.CRYPTO_MODE_AES_CTR; + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java new file mode 100644 index 0000000000..e027d6ed76 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java @@ -0,0 +1,197 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** + * A holder for information corresponding to a single fragment of an mp4 file. + */ +/* package */ final class TrackFragment { + + /** + * The default values for samples from the track fragment header. + */ + public DefaultSampleValues header; + /** + * The position (byte offset) of the start of fragment. + */ + public long atomPosition; + /** + * The position (byte offset) of the start of data contained in the fragment. + */ + public long dataPosition; + /** + * The position (byte offset) of the start of auxiliary data. + */ + public long auxiliaryDataPosition; + /** + * The number of track runs of the fragment. + */ + public int trunCount; + /** + * The total number of samples in the fragment. + */ + public int sampleCount; + /** + * The position (byte offset) of the start of sample data of each track run in the fragment. + */ + public long[] trunDataPosition; + /** + * The number of samples contained by each track run in the fragment. + */ + public int[] trunLength; + /** + * The size of each sample in the fragment. + */ + public int[] sampleSizeTable; + /** + * The composition time offset of each sample in the fragment. + */ + public int[] sampleCompositionTimeOffsetTable; + /** + * The decoding time of each sample in the fragment. + */ + public long[] sampleDecodingTimeTable; + /** + * Indicates which samples are sync frames. + */ + public boolean[] sampleIsSyncFrameTable; + /** + * Whether the fragment defines encryption data. + */ + public boolean definesEncryptionData; + /** + * If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption. + * Undefined otherwise. + */ + public boolean[] sampleHasSubsampleEncryptionTable; + /** + * Fragment specific track encryption. May be null. + */ + public TrackEncryptionBox trackEncryptionBox; + /** + * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data. + * Undefined otherwise. + */ + public int sampleEncryptionDataLength; + /** + * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined + * otherwise. + */ + public ParsableByteArray sampleEncryptionData; + /** + * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data. + */ + public boolean sampleEncryptionDataNeedsFill; + /** + * The absolute decode time of the start of the next fragment. + */ + public long nextFragmentDecodeTime; + + /** + * Resets the fragment. + * <p> + * {@link #sampleCount} and {@link #nextFragmentDecodeTime} are set to 0, and both + * {@link #definesEncryptionData} and {@link #sampleEncryptionDataNeedsFill} is set to false, + * and {@link #trackEncryptionBox} is set to null. + */ + public void reset() { + trunCount = 0; + nextFragmentDecodeTime = 0; + definesEncryptionData = false; + sampleEncryptionDataNeedsFill = false; + trackEncryptionBox = null; + } + + /** + * Configures the fragment for the specified number of samples. + * <p> + * The {@link #sampleCount} of the fragment is set to the specified sample count, and the + * contained tables are resized if necessary such that they are at least this length. + * + * @param sampleCount The number of samples in the new run. + */ + public void initTables(int trunCount, int sampleCount) { + this.trunCount = trunCount; + this.sampleCount = sampleCount; + if (trunLength == null || trunLength.length < trunCount) { + trunDataPosition = new long[trunCount]; + trunLength = new int[trunCount]; + } + if (sampleSizeTable == null || sampleSizeTable.length < sampleCount) { + // Size the tables 25% larger than needed, so as to make future resize operations less + // likely. The choice of 25% is relatively arbitrary. + int tableSize = (sampleCount * 125) / 100; + sampleSizeTable = new int[tableSize]; + sampleCompositionTimeOffsetTable = new int[tableSize]; + sampleDecodingTimeTable = new long[tableSize]; + sampleIsSyncFrameTable = new boolean[tableSize]; + sampleHasSubsampleEncryptionTable = new boolean[tableSize]; + } + } + + /** + * Configures the fragment to be one that defines encryption data of the specified length. + * <p> + * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to + * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it + * is at least this length. + * + * @param length The length in bytes of the encryption data. + */ + public void initEncryptionData(int length) { + if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) { + sampleEncryptionData = new ParsableByteArray(length); + } + sampleEncryptionDataLength = length; + definesEncryptionData = true; + sampleEncryptionDataNeedsFill = true; + } + + /** + * Fills {@link #sampleEncryptionData} from the provided input. + * + * @param input An {@link ExtractorInput} from which to read the encryption data. + */ + public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException { + input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + sampleEncryptionData.setPosition(0); + sampleEncryptionDataNeedsFill = false; + } + + /** + * Fills {@link #sampleEncryptionData} from the provided source. + * + * @param source A source from which to read the encryption data. + */ + public void fillEncryptionData(ParsableByteArray source) { + source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + sampleEncryptionData.setPosition(0); + sampleEncryptionDataNeedsFill = false; + } + + public long getSamplePresentationTime(int index) { + return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; + } + + /** Returns whether the sample at the given index has a subsample encryption table. */ + public boolean sampleHasSubsampleEncryptionTable(int index) { + return definesEncryptionData && sampleHasSubsampleEncryptionTable[index]; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java new file mode 100644 index 0000000000..bb9891b302 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java @@ -0,0 +1,108 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Sample table for a track in an MP4 file. + */ +/* package */ final class TrackSampleTable { + + /** The track corresponding to this sample table. */ + public final Track track; + /** Number of samples. */ + public final int sampleCount; + /** Sample offsets in bytes. */ + public final long[] offsets; + /** Sample sizes in bytes. */ + public final int[] sizes; + /** Maximum sample size in {@link #sizes}. */ + public final int maximumSize; + /** Sample timestamps in microseconds. */ + public final long[] timestampsUs; + /** Sample flags. */ + public final int[] flags; + /** + * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample + * table is empty. + */ + public final long durationUs; + + public TrackSampleTable( + Track track, + long[] offsets, + int[] sizes, + int maximumSize, + long[] timestampsUs, + int[] flags, + long durationUs) { + Assertions.checkArgument(sizes.length == timestampsUs.length); + Assertions.checkArgument(offsets.length == timestampsUs.length); + Assertions.checkArgument(flags.length == timestampsUs.length); + + this.track = track; + this.offsets = offsets; + this.sizes = sizes; + this.maximumSize = maximumSize; + this.timestampsUs = timestampsUs; + this.flags = flags; + this.durationUs = durationUs; + sampleCount = offsets.length; + if (flags.length > 0) { + flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE; + } + } + + /** + * Returns the sample index of the closest synchronization sample at or before the given + * timestamp, if one is available. + * + * @param timeUs Timestamp adjacent to which to find a synchronization sample. + * @return Index of the synchronization sample, or {@link C#INDEX_UNSET} if none. + */ + public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { + // Video frame timestamps may not be sorted, so the behavior of this call can be undefined. + // Frames are not reordered past synchronization samples so this works in practice. + int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false); + for (int i = startIndex; i >= 0; i--) { + if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + return i; + } + } + return C.INDEX_UNSET; + } + + /** + * Returns the sample index of the closest synchronization sample at or after the given timestamp, + * if one is available. + * + * @param timeUs Timestamp adjacent to which to find a synchronization sample. + * @return index Index of the synchronization sample, or {@link C#INDEX_UNSET} if none. + */ + public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) { + int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false); + for (int i = startIndex; i < timestampsUs.length; i++) { + if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + return i; + } + } + return C.INDEX_UNSET; + } + +} |