Big cleanup of mp4 metadata extraction

This commit is contained in:
Oliver Woodman 2016-10-26 23:45:50 +01:00
parent 1b39d21ed4
commit 8caaf0b5d9
6 changed files with 369 additions and 402 deletions

View File

@ -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));
}
}
}

View File

@ -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);

View File

@ -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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> 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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
/**