summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java588
1 files changed, 588 insertions, 0 deletions
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;
+ }
+
+}