summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java558
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java1607
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java32
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java1660
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java115
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java588
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java824
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java208
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java201
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java148
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java103
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java197
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java108
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;
+ }
+
+}