1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.extractor.mp4;
17 
18 import androidx.annotation.Nullable;
19 import androidx.annotation.VisibleForTesting;
20 import com.google.android.exoplayer2.C;
21 import com.google.android.exoplayer2.Format;
22 import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
23 import com.google.android.exoplayer2.metadata.Metadata;
24 import com.google.android.exoplayer2.metadata.id3.ApicFrame;
25 import com.google.android.exoplayer2.metadata.id3.CommentFrame;
26 import com.google.android.exoplayer2.metadata.id3.Id3Frame;
27 import com.google.android.exoplayer2.metadata.id3.InternalFrame;
28 import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
29 import com.google.android.exoplayer2.util.Log;
30 import com.google.android.exoplayer2.util.ParsableByteArray;
31 
32 /** Utilities for handling metadata in MP4. */
33 /* package */ final class MetadataUtil {
34 
35   private static final String TAG = "MetadataUtil";
36 
37   // Codes that start with the copyright character (omitted) and have equivalent ID3 frames.
38   private static final int SHORT_TYPE_NAME_1 = 0x006e616d;
39   private static final int SHORT_TYPE_NAME_2 = 0x0074726b;
40   private static final int SHORT_TYPE_COMMENT = 0x00636d74;
41   private static final int SHORT_TYPE_YEAR = 0x00646179;
42   private static final int SHORT_TYPE_ARTIST = 0x00415254;
43   private static final int SHORT_TYPE_ENCODER = 0x00746f6f;
44   private static final int SHORT_TYPE_ALBUM = 0x00616c62;
45   private static final int SHORT_TYPE_COMPOSER_1 = 0x00636f6d;
46   private static final int SHORT_TYPE_COMPOSER_2 = 0x00777274;
47   private static final int SHORT_TYPE_LYRICS = 0x006c7972;
48   private static final int SHORT_TYPE_GENRE = 0x0067656e;
49 
50   // Codes that have equivalent ID3 frames.
51   private static final int TYPE_COVER_ART = 0x636f7672;
52   private static final int TYPE_GENRE = 0x676e7265;
53   private static final int TYPE_GROUPING = 0x00677270;
54   private static final int TYPE_DISK_NUMBER = 0x6469736b;
55   private static final int TYPE_TRACK_NUMBER = 0x74726b6e;
56   private static final int TYPE_TEMPO = 0x746d706f;
57   private static final int TYPE_COMPILATION = 0x6370696c;
58   private static final int TYPE_ALBUM_ARTIST = 0x61415254;
59   private static final int TYPE_SORT_TRACK_NAME = 0x736f6e6d;
60   private static final int TYPE_SORT_ALBUM = 0x736f616c;
61   private static final int TYPE_SORT_ARTIST = 0x736f6172;
62   private static final int TYPE_SORT_ALBUM_ARTIST = 0x736f6161;
63   private static final int TYPE_SORT_COMPOSER = 0x736f636f;
64 
65   // Types that do not have equivalent ID3 frames.
66   private static final int TYPE_RATING = 0x72746e67;
67   private static final int TYPE_GAPLESS_ALBUM = 0x70676170;
68   private static final int TYPE_TV_SORT_SHOW = 0x736f736e;
69   private static final int TYPE_TV_SHOW = 0x74767368;
70 
71   // Type for items that are intended for internal use by the player.
72   private static final int TYPE_INTERNAL = 0x2d2d2d2d;
73 
74   private static final int PICTURE_TYPE_FRONT_COVER = 3;
75 
76   // Standard genres.
77   @VisibleForTesting
78   /* package */ static final String[] STANDARD_GENRES =
79       new String[] {
80         // These are the official ID3v1 genres.
81         "Blues",
82         "Classic Rock",
83         "Country",
84         "Dance",
85         "Disco",
86         "Funk",
87         "Grunge",
88         "Hip-Hop",
89         "Jazz",
90         "Metal",
91         "New Age",
92         "Oldies",
93         "Other",
94         "Pop",
95         "R&B",
96         "Rap",
97         "Reggae",
98         "Rock",
99         "Techno",
100         "Industrial",
101         "Alternative",
102         "Ska",
103         "Death Metal",
104         "Pranks",
105         "Soundtrack",
106         "Euro-Techno",
107         "Ambient",
108         "Trip-Hop",
109         "Vocal",
110         "Jazz+Funk",
111         "Fusion",
112         "Trance",
113         "Classical",
114         "Instrumental",
115         "Acid",
116         "House",
117         "Game",
118         "Sound Clip",
119         "Gospel",
120         "Noise",
121         "AlternRock",
122         "Bass",
123         "Soul",
124         "Punk",
125         "Space",
126         "Meditative",
127         "Instrumental Pop",
128         "Instrumental Rock",
129         "Ethnic",
130         "Gothic",
131         "Darkwave",
132         "Techno-Industrial",
133         "Electronic",
134         "Pop-Folk",
135         "Eurodance",
136         "Dream",
137         "Southern Rock",
138         "Comedy",
139         "Cult",
140         "Gangsta",
141         "Top 40",
142         "Christian Rap",
143         "Pop/Funk",
144         "Jungle",
145         "Native American",
146         "Cabaret",
147         "New Wave",
148         "Psychadelic",
149         "Rave",
150         "Showtunes",
151         "Trailer",
152         "Lo-Fi",
153         "Tribal",
154         "Acid Punk",
155         "Acid Jazz",
156         "Polka",
157         "Retro",
158         "Musical",
159         "Rock & Roll",
160         "Hard Rock",
161         // Genres made up by the authors of Winamp (v1.91) and later added to the ID3 spec.
162         "Folk",
163         "Folk-Rock",
164         "National Folk",
165         "Swing",
166         "Fast Fusion",
167         "Bebob",
168         "Latin",
169         "Revival",
170         "Celtic",
171         "Bluegrass",
172         "Avantgarde",
173         "Gothic Rock",
174         "Progressive Rock",
175         "Psychedelic Rock",
176         "Symphonic Rock",
177         "Slow Rock",
178         "Big Band",
179         "Chorus",
180         "Easy Listening",
181         "Acoustic",
182         "Humour",
183         "Speech",
184         "Chanson",
185         "Opera",
186         "Chamber Music",
187         "Sonata",
188         "Symphony",
189         "Booty Bass",
190         "Primus",
191         "Porn Groove",
192         "Satire",
193         "Slow Jam",
194         "Club",
195         "Tango",
196         "Samba",
197         "Folklore",
198         "Ballad",
199         "Power Ballad",
200         "Rhythmic Soul",
201         "Freestyle",
202         "Duet",
203         "Punk Rock",
204         "Drum Solo",
205         "A capella",
206         "Euro-House",
207         "Dance Hall",
208         // Genres made up by the authors of Winamp (v1.91) but have not been added to the ID3 spec.
209         "Goa",
210         "Drum & Bass",
211         "Club-House",
212         "Hardcore",
213         "Terror",
214         "Indie",
215         "BritPop",
216         "Afro-Punk",
217         "Polsk Punk",
218         "Beat",
219         "Christian Gangsta Rap",
220         "Heavy Metal",
221         "Black Metal",
222         "Crossover",
223         "Contemporary Christian",
224         "Christian Rock",
225         "Merengue",
226         "Salsa",
227         "Thrash Metal",
228         "Anime",
229         "Jpop",
230         "Synthpop",
231         // Genres made up by the authors of Winamp (v5.6) but have not been added to the ID3 spec.
232         "Abstract",
233         "Art Rock",
234         "Baroque",
235         "Bhangra",
236         "Big beat",
237         "Breakbeat",
238         "Chillout",
239         "Downtempo",
240         "Dub",
241         "EBM",
242         "Eclectic",
243         "Electro",
244         "Electroclash",
245         "Emo",
246         "Experimental",
247         "Garage",
248         "Global",
249         "IDM",
250         "Illbient",
251         "Industro-Goth",
252         "Jam Band",
253         "Krautrock",
254         "Leftfield",
255         "Lounge",
256         "Math Rock",
257         "New Romantic",
258         "Nu-Breakz",
259         "Post-Punk",
260         "Post-Rock",
261         "Psytrance",
262         "Shoegaze",
263         "Space Rock",
264         "Trop Rock",
265         "World Music",
266         "Neoclassical",
267         "Audiobook",
268         "Audio theatre",
269         "Neue Deutsche Welle",
270         "Podcast",
271         "Indie-Rock",
272         "G-Funk",
273         "Dubstep",
274         "Garage Rock",
275         "Psybient"
276       };
277 
278   private static final String LANGUAGE_UNDEFINED = "und";
279 
280   private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9;
281   private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD.
282 
283   private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps";
284 
MetadataUtil()285   private MetadataUtil() {}
286 
287   /** Updates a {@link Format.Builder} to include metadata from the provided sources. */
setFormatMetadata( int trackType, @Nullable Metadata udtaMetadata, @Nullable Metadata mdtaMetadata, GaplessInfoHolder gaplessInfoHolder, Format.Builder formatBuilder)288   public static void setFormatMetadata(
289       int trackType,
290       @Nullable Metadata udtaMetadata,
291       @Nullable Metadata mdtaMetadata,
292       GaplessInfoHolder gaplessInfoHolder,
293       Format.Builder formatBuilder) {
294     if (trackType == C.TRACK_TYPE_AUDIO) {
295       if (gaplessInfoHolder.hasGaplessInfo()) {
296         formatBuilder
297             .setEncoderDelay(gaplessInfoHolder.encoderDelay)
298             .setEncoderPadding(gaplessInfoHolder.encoderPadding);
299       }
300       // We assume all udta metadata is associated with the audio track.
301       if (udtaMetadata != null) {
302         formatBuilder.setMetadata(udtaMetadata);
303       }
304     } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) {
305       // Populate only metadata keys that are known to be specific to video.
306       for (int i = 0; i < mdtaMetadata.length(); i++) {
307         Metadata.Entry entry = mdtaMetadata.get(i);
308         if (entry instanceof MdtaMetadataEntry) {
309           MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry;
310           if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)) {
311             formatBuilder.setMetadata(new Metadata(mdtaMetadataEntry));
312           }
313         }
314       }
315     }
316   }
317 
318   /**
319    * Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read
320    * starting from the current position of the {@link ParsableByteArray}, and the position is
321    * advanced by the size of the element. The position is advanced even if the element's type is
322    * unrecognized.
323    *
324    * @param ilst Holds the data to be parsed.
325    * @return The parsed element, or null if the element's type was not recognized.
326    */
327   @Nullable
parseIlstElement(ParsableByteArray ilst)328   public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
329     int position = ilst.getPosition();
330     int endPosition = position + ilst.readInt();
331     int type = ilst.readInt();
332     int typeTopByte = (type >> 24) & 0xFF;
333     try {
334       if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) {
335         int shortType = type & 0x00FFFFFF;
336         if (shortType == SHORT_TYPE_COMMENT) {
337           return parseCommentAttribute(type, ilst);
338         } else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) {
339           return parseTextAttribute(type, "TIT2", ilst);
340         } else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) {
341           return parseTextAttribute(type, "TCOM", ilst);
342         } else if (shortType == SHORT_TYPE_YEAR) {
343           return parseTextAttribute(type, "TDRC", ilst);
344         } else if (shortType == SHORT_TYPE_ARTIST) {
345           return parseTextAttribute(type, "TPE1", ilst);
346         } else if (shortType == SHORT_TYPE_ENCODER) {
347           return parseTextAttribute(type, "TSSE", ilst);
348         } else if (shortType == SHORT_TYPE_ALBUM) {
349           return parseTextAttribute(type, "TALB", ilst);
350         } else if (shortType == SHORT_TYPE_LYRICS) {
351           return parseTextAttribute(type, "USLT", ilst);
352         } else if (shortType == SHORT_TYPE_GENRE) {
353           return parseTextAttribute(type, "TCON", ilst);
354         } else if (shortType == TYPE_GROUPING) {
355           return parseTextAttribute(type, "TIT1", ilst);
356         }
357       } else if (type == TYPE_GENRE) {
358         return parseStandardGenreAttribute(ilst);
359       } else if (type == TYPE_DISK_NUMBER) {
360         return parseIndexAndCountAttribute(type, "TPOS", ilst);
361       } else if (type == TYPE_TRACK_NUMBER) {
362         return parseIndexAndCountAttribute(type, "TRCK", ilst);
363       } else if (type == TYPE_TEMPO) {
364         return parseUint8Attribute(type, "TBPM", ilst, true, false);
365       } else if (type == TYPE_COMPILATION) {
366         return parseUint8Attribute(type, "TCMP", ilst, true, true);
367       } else if (type == TYPE_COVER_ART) {
368         return parseCoverArt(ilst);
369       } else if (type == TYPE_ALBUM_ARTIST) {
370         return parseTextAttribute(type, "TPE2", ilst);
371       } else if (type == TYPE_SORT_TRACK_NAME) {
372         return parseTextAttribute(type, "TSOT", ilst);
373       } else if (type == TYPE_SORT_ALBUM) {
374         return parseTextAttribute(type, "TSO2", ilst);
375       } else if (type == TYPE_SORT_ARTIST) {
376         return parseTextAttribute(type, "TSOA", ilst);
377       } else if (type == TYPE_SORT_ALBUM_ARTIST) {
378         return parseTextAttribute(type, "TSOP", ilst);
379       } else if (type == TYPE_SORT_COMPOSER) {
380         return parseTextAttribute(type, "TSOC", ilst);
381       } else if (type == TYPE_RATING) {
382         return parseUint8Attribute(type, "ITUNESADVISORY", ilst, false, false);
383       } else if (type == TYPE_GAPLESS_ALBUM) {
384         return parseUint8Attribute(type, "ITUNESGAPLESS", ilst, false, true);
385       } else if (type == TYPE_TV_SORT_SHOW) {
386         return parseTextAttribute(type, "TVSHOWSORT", ilst);
387       } else if (type == TYPE_TV_SHOW) {
388         return parseTextAttribute(type, "TVSHOW", ilst);
389       } else if (type == TYPE_INTERNAL) {
390         return parseInternalAttribute(ilst, endPosition);
391       }
392       Log.d(TAG, "Skipped unknown metadata entry: " + Atom.getAtomTypeString(type));
393       return null;
394     } finally {
395       ilst.setPosition(endPosition);
396     }
397   }
398 
399   /**
400    * Parses an 'mdta' metadata entry starting at the current position in an ilst box.
401    *
402    * @param ilst The ilst box.
403    * @param endPosition The end position of the entry in the ilst box.
404    * @param key The mdta metadata entry key for the entry.
405    * @return The parsed element, or null if the entry wasn't recognized.
406    */
407   @Nullable
parseMdtaMetadataEntryFromIlst( ParsableByteArray ilst, int endPosition, String key)408   public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst(
409       ParsableByteArray ilst, int endPosition, String key) {
410     int atomPosition;
411     while ((atomPosition = ilst.getPosition()) < endPosition) {
412       int atomSize = ilst.readInt();
413       int atomType = ilst.readInt();
414       if (atomType == Atom.TYPE_data) {
415         int typeIndicator = ilst.readInt();
416         int localeIndicator = ilst.readInt();
417         int dataSize = atomSize - 16;
418         byte[] value = new byte[dataSize];
419         ilst.readBytes(value, 0, dataSize);
420         return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator);
421       }
422       ilst.setPosition(atomPosition + atomSize);
423     }
424     return null;
425   }
426 
427   @Nullable
parseTextAttribute( int type, String id, ParsableByteArray data)428   private static TextInformationFrame parseTextAttribute(
429       int type, String id, ParsableByteArray data) {
430     int atomSize = data.readInt();
431     int atomType = data.readInt();
432     if (atomType == Atom.TYPE_data) {
433       data.skipBytes(8); // version (1), flags (3), empty (4)
434       String value = data.readNullTerminatedString(atomSize - 16);
435       return new TextInformationFrame(id, /* description= */ null, value);
436     }
437     Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
438     return null;
439   }
440 
441   @Nullable
parseCommentAttribute(int type, ParsableByteArray data)442   private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
443     int atomSize = data.readInt();
444     int atomType = data.readInt();
445     if (atomType == Atom.TYPE_data) {
446       data.skipBytes(8); // version (1), flags (3), empty (4)
447       String value = data.readNullTerminatedString(atomSize - 16);
448       return new CommentFrame(LANGUAGE_UNDEFINED, value, value);
449     }
450     Log.w(TAG, "Failed to parse comment attribute: " + Atom.getAtomTypeString(type));
451     return null;
452   }
453 
454   @Nullable
parseUint8Attribute( int type, String id, ParsableByteArray data, boolean isTextInformationFrame, boolean isBoolean)455   private static Id3Frame parseUint8Attribute(
456       int type,
457       String id,
458       ParsableByteArray data,
459       boolean isTextInformationFrame,
460       boolean isBoolean) {
461     int value = parseUint8AttributeValue(data);
462     if (isBoolean) {
463       value = Math.min(1, value);
464     }
465     if (value >= 0) {
466       return isTextInformationFrame
467           ? new TextInformationFrame(id, /* description= */ null, Integer.toString(value))
468           : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
469     }
470     Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
471     return null;
472   }
473 
474   @Nullable
parseIndexAndCountAttribute( int type, String attributeName, ParsableByteArray data)475   private static TextInformationFrame parseIndexAndCountAttribute(
476       int type, String attributeName, ParsableByteArray data) {
477     int atomSize = data.readInt();
478     int atomType = data.readInt();
479     if (atomType == Atom.TYPE_data && atomSize >= 22) {
480       data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
481       int index = data.readUnsignedShort();
482       if (index > 0) {
483         String value = "" + index;
484         int count = data.readUnsignedShort();
485         if (count > 0) {
486           value += "/" + count;
487         }
488         return new TextInformationFrame(attributeName, /* description= */ null, value);
489       }
490     }
491     Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
492     return null;
493   }
494 
495   @Nullable
parseStandardGenreAttribute(ParsableByteArray data)496   private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
497     int genreCode = parseUint8AttributeValue(data);
498     @Nullable
499     String genreString =
500         (0 < genreCode && genreCode <= STANDARD_GENRES.length)
501             ? STANDARD_GENRES[genreCode - 1]
502             : null;
503     if (genreString != null) {
504       return new TextInformationFrame("TCON", /* description= */ null, genreString);
505     }
506     Log.w(TAG, "Failed to parse standard genre code");
507     return null;
508   }
509 
510   @Nullable
parseCoverArt(ParsableByteArray data)511   private static ApicFrame parseCoverArt(ParsableByteArray data) {
512     int atomSize = data.readInt();
513     int atomType = data.readInt();
514     if (atomType == Atom.TYPE_data) {
515       int fullVersionInt = data.readInt();
516       int flags = Atom.parseFullAtomFlags(fullVersionInt);
517       @Nullable String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null;
518       if (mimeType == null) {
519         Log.w(TAG, "Unrecognized cover art flags: " + flags);
520         return null;
521       }
522       data.skipBytes(4); // empty (4)
523       byte[] pictureData = new byte[atomSize - 16];
524       data.readBytes(pictureData, 0, pictureData.length);
525       return new ApicFrame(
526           mimeType,
527           /* description= */ null,
528           /* pictureType= */ PICTURE_TYPE_FRONT_COVER,
529           pictureData);
530     }
531     Log.w(TAG, "Failed to parse cover art attribute");
532     return null;
533   }
534 
535   @Nullable
parseInternalAttribute(ParsableByteArray data, int endPosition)536   private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
537     @Nullable String domain = null;
538     @Nullable String name = null;
539     int dataAtomPosition = -1;
540     int dataAtomSize = -1;
541     while (data.getPosition() < endPosition) {
542       int atomPosition = data.getPosition();
543       int atomSize = data.readInt();
544       int atomType = data.readInt();
545       data.skipBytes(4); // version (1), flags (3)
546       if (atomType == Atom.TYPE_mean) {
547         domain = data.readNullTerminatedString(atomSize - 12);
548       } else if (atomType == Atom.TYPE_name) {
549         name = data.readNullTerminatedString(atomSize - 12);
550       } else {
551         if (atomType == Atom.TYPE_data) {
552           dataAtomPosition = atomPosition;
553           dataAtomSize = atomSize;
554         }
555         data.skipBytes(atomSize - 12);
556       }
557     }
558     if (domain == null || name == null || dataAtomPosition == -1) {
559       return null;
560     }
561     data.setPosition(dataAtomPosition);
562     data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
563     String value = data.readNullTerminatedString(dataAtomSize - 16);
564     return new InternalFrame(domain, name, value);
565   }
566 
parseUint8AttributeValue(ParsableByteArray data)567   private static int parseUint8AttributeValue(ParsableByteArray data) {
568     data.skipBytes(4); // atomSize
569     int atomType = data.readInt();
570     if (atomType == Atom.TYPE_data) {
571       data.skipBytes(8); // version (1), flags (3), empty (4)
572       return data.readUnsignedByte();
573     }
574     Log.w(TAG, "Failed to parse uint8 attribute value");
575     return -1;
576   }
577 
578 }
579