From 8caaf0b5d992d5a31e7771ca49ec76ff5232e84b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 26 Oct 2016 23:45:50 +0100 Subject: [PATCH] Big cleanup of mp4 metadata extraction --- .../android/exoplayer2/demo/EventLogger.java | 39 +- .../exoplayer2/extractor/mp4/Atom.java | 3 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 337 +----------------- .../extractor/mp4/MetadataUtil.java | 323 +++++++++++++++++ .../exoplayer2/metadata/id3/Id3Util.java | 63 ---- .../exoplayer2/util/ParsableByteArray.java | 6 +- 6 files changed, 369 insertions(+), 402 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java delete mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index c3fc5b9549..5e0b76f68b 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -154,6 +154,18 @@ import java.util.Locale; } Log.d(TAG, " ]"); } + // Log metadata for at most one of the tracks selected for the renderer. + if (trackSelection != null) { + for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) { + Metadata metadata = trackSelection.getFormat(selectionIndex).metadata; + if (metadata != null) { + Log.d(TAG, " Metadata ["); + printMetadata(metadata, " "); + Log.d(TAG, " ]"); + break; + } + } + } Log.d(TAG, " ]"); } } @@ -184,7 +196,7 @@ import java.util.Locale; @Override public void onMetadata(Metadata metadata) { Log.d(TAG, "onMetadata ["); - printMetadata(metadata); + printMetadata(metadata, " "); Log.d(TAG, "]"); } @@ -208,13 +220,8 @@ import java.util.Locale; @Override public void onAudioInputFormatChanged(Format format) { - boolean hasMetadata = format.metadata != null; Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format) - + (hasMetadata ? "" : "]")); - if (hasMetadata) { - printMetadata(format.metadata); - Log.d(TAG, "]"); - } + + "]"); } @Override @@ -335,35 +342,35 @@ import java.util.Locale; Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); } - private void printMetadata(Metadata metadata) { + private void printMetadata(Metadata metadata, String prefix) { for (int i = 0; i < metadata.length(); i++) { Metadata.Entry entry = metadata.get(i); if (entry instanceof TxxxFrame) { TxxxFrame txxxFrame = (TxxxFrame) entry; - Log.d(TAG, String.format(" %s: description=%s, value=%s", txxxFrame.id, + Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id, txxxFrame.description, txxxFrame.value)); } else if (entry instanceof PrivFrame) { PrivFrame privFrame = (PrivFrame) entry; - Log.d(TAG, String.format(" %s: owner=%s", privFrame.id, privFrame.owner)); + Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); } else if (entry instanceof GeobFrame) { GeobFrame geobFrame = (GeobFrame) entry; - Log.d(TAG, String.format(" %s: mimeType=%s, filename=%s, description=%s", + Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s", geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); } else if (entry instanceof ApicFrame) { ApicFrame apicFrame = (ApicFrame) entry; - Log.d(TAG, String.format(" %s: mimeType=%s, description=%s", + Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s", apicFrame.id, apicFrame.mimeType, apicFrame.description)); } else if (entry instanceof TextInformationFrame) { TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.d(TAG, String.format(" %s: description=%s", textInformationFrame.id, + Log.d(TAG, prefix + String.format("%s: description=%s", textInformationFrame.id, textInformationFrame.description)); } else if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - Log.d(TAG, String.format(" %s: language=%s description=%s", commentFrame.id, - commentFrame.language, commentFrame.description)); + Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id, + commentFrame.language, commentFrame.description, commentFrame.text)); } else if (entry instanceof Id3Frame) { Id3Frame id3Frame = (Id3Frame) entry; - Log.d(TAG, String.format(" %s", id3Frame.id)); + Log.d(TAG, prefix + String.format("%s", id3Frame.id)); } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index e93e9e3d9c..749c9b3542 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -132,7 +132,6 @@ import java.util.List; public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08"); public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); - public static final int TYPE_DASHES = Util.getIntegerCodeForString("----"); public final int type; @@ -299,7 +298,7 @@ import java.util.List; * @return The corresponding four character string. */ public static String getAtomTypeString(int type) { - return "" + (char) (type >> 24) + return "" + (char) ((type >> 24) & 0xFF) + (char) ((type >> 16) & 0xFF) + (char) ((type >> 8) & 0xFF) + (char) (type & 0xFF); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index b8456ecb4d..47cb3262e1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -24,11 +24,6 @@ import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.BinaryFrame; -import com.google.android.exoplayer2.metadata.id3.CommentFrame; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.Id3Util; -import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -418,336 +413,45 @@ import java.util.List; 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(udtaData.getPosition() - Atom.HEADER_SIZE); - udtaData.setLimit(udtaData.getPosition() + atomSize); - return parseMetaAtom(udtaData); + udtaData.setPosition(atomPosition); + return parseMetaAtom(udtaData, atomPosition + atomSize); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray data) { - data.skipBytes(Atom.FULL_HEADER_SIZE); - ParsableByteArray ilst = new ParsableByteArray(); - while (data.bytesLeft() >= Atom.HEADER_SIZE) { - int payloadSize = data.readInt() - Atom.HEADER_SIZE; - int atomType = data.readInt(); + private static Metadata parseMetaAtom(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) { - ilst.reset(data.data, data.getPosition() + payloadSize); - ilst.setPosition(data.getPosition()); - Metadata metadata = parseIlst(ilst); - if (metadata != null) { - return metadata; - } + meta.setPosition(atomPosition); + return parseIlst(meta, atomPosition + atomSize); } - data.skipBytes(payloadSize); + meta.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseIlst(ParsableByteArray ilst) { + private static Metadata parseIlst(ParsableByteArray ilst, int limit) { + ilst.skipBytes(Atom.HEADER_SIZE); ArrayList entries = new ArrayList<>(); - while (ilst.bytesLeft() > 0) { - int position = ilst.getPosition(); - int endPosition = position + ilst.readInt(); - int type = ilst.readInt(); - parseIlstElement(ilst, type, endPosition, entries); - ilst.setPosition(endPosition); + while (ilst.getPosition() < limit) { + Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst); + if (entry != null) { + entries.add(entry); + } } return entries.isEmpty() ? null : new Metadata(entries); } - private static final String P1 = "\u00a9"; - private static final String P2 = "\ufffd"; - private static final int TYPE_NAME_1 = Util.getIntegerCodeForString(P1 + "nam"); - private static final int TYPE_NAME_2 = Util.getIntegerCodeForString(P2 + "nam"); - private static final int TYPE_NAME_3 = Util.getIntegerCodeForString(P1 + "trk"); - private static final int TYPE_NAME_4 = Util.getIntegerCodeForString(P2 + "trk"); - private static final int TYPE_COMMENT_1 = Util.getIntegerCodeForString(P1 + "cmt"); - private static final int TYPE_COMMENT_2 = Util.getIntegerCodeForString(P2 + "cmt"); - private static final int TYPE_YEAR_1 = Util.getIntegerCodeForString(P1 + "day"); - private static final int TYPE_YEAR_2 = Util.getIntegerCodeForString(P2 + "day"); - private static final int TYPE_ARTIST_1 = Util.getIntegerCodeForString(P1 + "ART"); - private static final int TYPE_ARTIST_2 = Util.getIntegerCodeForString(P2 + "ART"); - private static final int TYPE_ENCODER_1 = Util.getIntegerCodeForString(P1 + "too"); - private static final int TYPE_ENCODER_2 = Util.getIntegerCodeForString(P2 + "too"); - private static final int TYPE_ALBUM_1 = Util.getIntegerCodeForString(P1 + "alb"); - private static final int TYPE_ALBUM_2 = Util.getIntegerCodeForString(P2 + "alb"); - private static final int TYPE_COMPOSER_1 = Util.getIntegerCodeForString(P1 + "com"); - private static final int TYPE_COMPOSER_2 = Util.getIntegerCodeForString(P2 + "com"); - private static final int TYPE_COMPOSER_3 = Util.getIntegerCodeForString(P1 + "wrt"); - private static final int TYPE_COMPOSER_4 = Util.getIntegerCodeForString(P2 + "wrt"); - private static final int TYPE_LYRICS_1 = Util.getIntegerCodeForString(P1 + "lyr"); - private static final int TYPE_LYRICS_2 = Util.getIntegerCodeForString(P2 + "lyr"); - private static final int TYPE_GENRE_1 = Util.getIntegerCodeForString(P1 + "gen"); - private static final int TYPE_GENRE_2 = Util.getIntegerCodeForString(P2 + "gen"); - private static final int TYPE_STANDARD_GENRE = Util.getIntegerCodeForString("gnre"); - private static final int TYPE_GROUPING_1 = Util.getIntegerCodeForString(P1 + "grp"); - private static final int TYPE_GROUPING_2 = Util.getIntegerCodeForString(P2 + "grp"); - private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); - private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); - private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); - private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); - private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); - private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); - private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); - private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); - private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); - private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); - private static final int TYPE_SORT_SHOW = Util.getIntegerCodeForString("sosn"); - private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); - private static final int TYPE_SHOW = Util.getIntegerCodeForString("tvsh"); - - // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes - - private static void parseIlstElement(ParsableByteArray ilst, int type, int endPosition, - List builder) { - if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { - parseTextAttribute(builder, "TIT2", ilst); - } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { - parseCommentAttribute(builder, "COMM", ilst); - } else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) { - parseTextAttribute(builder, "TDRC", ilst); - } else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) { - parseTextAttribute(builder, "TPE1", ilst); - } else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) { - parseTextAttribute(builder, "TSSE", ilst); - } else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) { - parseTextAttribute(builder, "TALB", ilst); - } else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 || - type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { - parseTextAttribute(builder, "TCOM", ilst); - } else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) { - parseTextAttribute(builder, "lyrics", ilst); - } else if (type == TYPE_STANDARD_GENRE) { - parseStandardGenreAttribute(builder, "TCON", ilst); - } else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) { - parseTextAttribute(builder, "TCON", ilst); - } else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) { - parseTextAttribute(builder, "TIT1", ilst); - } else if (type == TYPE_DISK_NUMBER) { - parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition); - } else if (type == TYPE_TRACK_NUMBER) { - parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition); - } else if (type == TYPE_TEMPO) { - parseIntegerAttribute(builder, "TBPM", ilst); - } else if (type == TYPE_COMPILATION) { - parseBooleanAttribute(builder, "TCMP", ilst); - } else if (type == TYPE_ALBUM_ARTIST) { - parseTextAttribute(builder, "TPE2", ilst); - } else if (type == TYPE_SORT_TRACK_NAME) { - parseTextAttribute(builder, "TSOT", ilst); - } else if (type == TYPE_SORT_ALBUM) { - parseTextAttribute(builder, "TSO2", ilst); - } else if (type == TYPE_SORT_ARTIST) { - parseTextAttribute(builder, "TSOA", ilst); - } else if (type == TYPE_SORT_ALBUM_ARTIST) { - parseTextAttribute(builder, "TSOP", ilst); - } else if (type == TYPE_SORT_COMPOSER) { - parseTextAttribute(builder, "TSOC", ilst); - } else if (type == TYPE_SORT_SHOW) { - parseTextAttribute(builder, "sortShow", ilst); - } else if (type == TYPE_GAPLESS_ALBUM) { - parseBooleanAttribute(builder, "gaplessAlbum", ilst); - } else if (type == TYPE_SHOW) { - parseTextAttribute(builder, "show", ilst); - } else if (type == Atom.TYPE_DASHES) { - parseExtendedAttribute(builder, ilst, endPosition); - } - } - - private static void parseTextAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - ilst.skipBytes(4); - String value = ilst.readNullTerminatedString(length - 4); - Id3Frame frame = new TextInformationFrame(attributeName, value); - builder.add(frame); - } else { - ilst.skipBytes(length); - } - } - - private static void parseCommentAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - ilst.skipBytes(4); - String value = ilst.readNullTerminatedString(length - 4); - Id3Frame frame = new CommentFrame("eng", attributeName, value); - builder.add(frame); - } else { - ilst.skipBytes(length); - } - } - - private static void parseBooleanAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof Integer) { - int n = (Integer) value; - String s = n == 0 ? "0" : "1"; - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseIntegerAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof Integer) { - int n = (Integer) value; - String s = "" + n; - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseIndexAndCountAttribute(List builder, - String attributeName, ParsableByteArray ilst, int endPosition) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof byte[]) { - byte[] bytes = (byte[]) value; - if (bytes.length == 8) { - int index = (bytes[2] << 8) + (bytes[3] & 0xFF); - int count = (bytes[4] << 8) + (bytes[5] & 0xFF); - if (index > 0) { - String s = "" + index; - if (count > 0) { - s = s + "/" + count; - } - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseStandardGenreAttribute(List builder, - String attributeName, ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof byte[]) { - byte[] bytes = (byte[]) value; - if (bytes.length == 2) { - int code = (bytes[0] << 8) + (bytes[1] & 0xFF); - String s = Id3Util.decodeGenre(code); - if (s != null) { - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseExtendedAttribute(List builder, ParsableByteArray ilst, - int endPosition) { - String domain = null; - String name = null; - Object value = null; - - while (ilst.getPosition() < endPosition) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_mean) { - domain = ilst.readNullTerminatedString(length); - } else if (key == Atom.TYPE_name) { - name = ilst.readNullTerminatedString(length); - } else if (key == Atom.TYPE_data) { - value = parseDataBox(ilst, length); - } else { - ilst.skipBytes(length); - } - } - - if (value != null) { - if (Util.areEqual(domain, "com.apple.iTunes")) { - String s = new String((byte[]) value); - Id3Frame frame = new CommentFrame("eng", name, s); - builder.add(frame); - } else if (domain != null && name != null) { - String extendedName = domain + "." + name; - if (value instanceof String) { - Id3Frame frame = new TextInformationFrame(extendedName, (String) value); - builder.add(frame); - } else if (value instanceof Integer) { - Id3Frame frame = new TextInformationFrame(extendedName, value.toString()); - builder.add(frame); - } else if (value instanceof byte[]) { - byte[] bb = (byte[]) value; - Id3Frame frame = new BinaryFrame(extendedName, bb); - builder.add(frame); - } - } - } - } - - private static Object parseDataBox(ParsableByteArray ilst, int length) { - int versionAndFlags = ilst.readInt(); - int flags = versionAndFlags & 0xFFFFFF; - boolean isText = (flags == 1); - boolean isData = (flags == 0); - boolean isImageData = (flags == 0xD); - boolean isInteger = (flags == 21); - int dataLength = length - 4; - if (isText) { - return ilst.readNullTerminatedString(dataLength); - } else if (isInteger) { - if (dataLength == 1) { - return ilst.readUnsignedByte(); - } else if (dataLength == 2) { - return ilst.readUnsignedShort(); - } else { - ilst.skipBytes(dataLength); - return null; - } - } else if (isData) { - byte[] bytes = new byte[dataLength]; - ilst.readBytes(bytes, 0, dataLength); - return bytes; - } else { - ilst.skipBytes(dataLength); - return null; - } - } - /** * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie. * @@ -756,12 +460,9 @@ import java.util.List; */ 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(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java new file mode 100644 index 0000000000..4bfef85d10 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.mp4; + +import android.util.Log; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.ApicFrame; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; +import com.google.android.exoplayer2.metadata.id3.Id3Frame; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; + +/** + * Parses metadata items stored in ilst atoms. + */ +/* 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 = Util.getIntegerCodeForString("nam"); + private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk"); + private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt"); + private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day"); + private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART"); + private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too"); + private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb"); + private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com"); + private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt"); + private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr"); + private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen"); + + // Codes that have equivalent ID3 frames. + private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr"); + private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre"); + private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp"); + private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); + private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); + private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); + private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); + private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); + private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); + private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); + private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); + private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); + private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); + + // Types that do not have equivalent ID3 frames. + private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng"); + private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); + private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn"); + private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh"); + + // Type for items that are intended for internal use by the player. + private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----"); + + // Standard genres. + private 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", + // These were made up by the authors of Winamp but backported into 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", + // These were also invented by the Winamp folks but ignored by the ID3 authors. + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", + "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", + "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", + "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", + "Synthpop" + }; + + private static final String LANGUAGE_UNDEFINED = "und"; + + private MetadataUtil() {} + + /** + * Parses a single 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. + */ + 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 == '\u00A9' /* Copyright char */ + || typeTopByte == '\uFFFD' /* Replacement char */) { + 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); + } + } + + 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, value); + } + Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + 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; + } + + 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, Integer.toString(value)) + : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value)); + } + Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + 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 description = "" + index; + int count = data.readUnsignedShort(); + if (count > 0) { + description += "/" + count; + } + return new TextInformationFrame(attributeName, description); + } + } + Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + 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", genreString); + } + Log.w(TAG, "Failed to parse standard genre code"); + return null; + } + + 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, null, 3 /* Cover (front) */, pictureData); + } + Log.w(TAG, "Failed to parse cover art attribute"); + return null; + } + + 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 (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) { + // We're only interested in iTunSMPB. + 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 CommentFrame(LANGUAGE_UNDEFINED, 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/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java deleted file mode 100644 index 64f2ce9908..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.metadata.id3; - -/** - * ID3 utility methods. - */ -public final class Id3Util { - - private 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", - // These were made up by the authors of Winamp but backported into 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", - // These were also invented by the Winamp folks but ignored by the ID3 authors. - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", - "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", - "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", - "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", - "Synthpop" - }; - - private Id3Util() {} - - public static String decodeGenre(int code) { - return (0 < code && code <= STANDARD_GENRES.length) ? STANDARD_GENRES[code - 1] : null; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 7691899ade..05c29ca032 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -300,9 +300,9 @@ public final class ParsableByteArray { */ public int readLittleEndianInt() { return (data[position++] & 0xFF) - | (data[position++] & 0xFF) << 8 - | (data[position++] & 0xFF) << 16 - | (data[position++] & 0xFF) << 24; + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF) << 16 + | (data[position++] & 0xFF) << 24; } /**