1 /* 2 * Copyright (C) 2009 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 17 package android.media; 18 19 import android.graphics.Bitmap; 20 import android.os.Parcel; 21 import android.util.Log; 22 23 import java.util.Calendar; 24 import java.util.Collections; 25 import java.util.Date; 26 import java.util.HashMap; 27 import java.util.Set; 28 import java.util.TimeZone; 29 30 31 /** 32 Class to hold the media's metadata. Metadata are used 33 for human consumption and can be embedded in the media (e.g 34 shoutcast) or available from an external source. The source can be 35 local (e.g thumbnail stored in the DB) or remote (e.g caption 36 server). 37 38 Metadata is like a Bundle. It is sparse and each key can occur at 39 most once. The key is an integer and the value is the actual metadata. 40 41 The caller is expected to know the type of the metadata and call 42 the right get* method to fetch its value. 43 44 // FIXME: unhide. 45 {@hide} 46 */ 47 public class Metadata 48 { 49 // The metadata are keyed using integers rather than more heavy 50 // weight strings. We considered using Bundle to ship the metadata 51 // between the native layer and the java layer but dropped that 52 // option since keeping in sync a native implementation of Bundle 53 // and the java one would be too burdensome. Besides Bundle uses 54 // String for its keys. 55 // The key range [0 8192) is reserved for the system. 56 // 57 // We manually serialize the data in Parcels. For large memory 58 // blob (bitmaps, raw pictures) we use MemoryFile which allow the 59 // client to make the data purge-able once it is done with it. 60 // 61 62 public static final int ANY = 0; // Never used for metadata returned, only for filtering. 63 // Keep in sync with kAny in MediaPlayerService.cpp 64 65 // TODO: Should we use numbers compatible with the metadata retriever? 66 public static final int TITLE = 1; // String 67 public static final int COMMENT = 2; // String 68 public static final int COPYRIGHT = 3; // String 69 public static final int ALBUM = 4; // String 70 public static final int ARTIST = 5; // String 71 public static final int AUTHOR = 6; // String 72 public static final int COMPOSER = 7; // String 73 public static final int GENRE = 8; // String 74 public static final int DATE = 9; // Date 75 public static final int DURATION = 10; // Integer(millisec) 76 public static final int CD_TRACK_NUM = 11; // Integer 1-based 77 public static final int CD_TRACK_MAX = 12; // Integer 78 public static final int RATING = 13; // String 79 public static final int ALBUM_ART = 14; // byte[] 80 public static final int VIDEO_FRAME = 15; // Bitmap 81 public static final int CAPTION = 16; // TimedText 82 83 public static final int BIT_RATE = 17; // Integer, Aggregate rate of 84 // all the streams in bps. 85 86 public static final int AUDIO_BIT_RATE = 18; // Integer, bps 87 public static final int VIDEO_BIT_RATE = 19; // Integer, bps 88 public static final int AUDIO_SAMPLE_RATE = 20; // Integer, Hz 89 public static final int VIDEO_FRAME_RATE = 21; // Integer, Hz 90 91 // See RFC2046 and RFC4281. 92 public static final int MIME_TYPE = 22; // String 93 public static final int AUDIO_CODEC = 23; // String 94 public static final int VIDEO_CODEC = 24; // String 95 96 public static final int VIDEO_HEIGHT = 25; // Integer 97 public static final int VIDEO_WIDTH = 26; // Integer 98 public static final int NUM_TRACKS = 27; // Integer 99 public static final int DRM_CRIPPLED = 28; // Boolean 100 101 // Playback capabilities. 102 public static final int PAUSE_AVAILABLE = 29; // Boolean 103 public static final int SEEK_BACKWARD_AVAILABLE = 30; // Boolean 104 public static final int SEEK_FORWARD_AVAILABLE = 31; // Boolean 105 public static final int SEEK_AVAILABLE = 32; // Boolean 106 107 private static final int LAST_SYSTEM = 32; 108 private static final int FIRST_CUSTOM = 8192; 109 110 // Shorthands to set the MediaPlayer's metadata filter. 111 public static final Set<Integer> MATCH_NONE = Collections.EMPTY_SET; 112 public static final Set<Integer> MATCH_ALL = Collections.singleton(ANY); 113 114 public static final int STRING_VAL = 1; 115 public static final int INTEGER_VAL = 2; 116 public static final int BOOLEAN_VAL = 3; 117 public static final int LONG_VAL = 4; 118 public static final int DOUBLE_VAL = 5; 119 public static final int TIMED_TEXT_VAL = 6; 120 public static final int DATE_VAL = 7; 121 public static final int BYTE_ARRAY_VAL = 8; 122 // FIXME: misses a type for shared heap is missing (MemoryFile). 123 // FIXME: misses a type for bitmaps. 124 private static final int LAST_TYPE = 8; 125 126 private static final String TAG = "media.Metadata"; 127 private static final int kInt32Size = 4; 128 private static final int kMetaHeaderSize = 2 * kInt32Size; // size + marker 129 private static final int kRecordHeaderSize = 3 * kInt32Size; // size + id + type 130 131 private static final int kMetaMarker = 0x4d455441; // 'M' 'E' 'T' 'A' 132 133 // After a successful parsing, set the parcel with the serialized metadata. 134 private Parcel mParcel; 135 136 // Map to associate a Metadata key (e.g TITLE) with the offset of 137 // the record's payload in the parcel. 138 // Used to look up if a key was present too. 139 // Key: Metadata ID 140 // Value: Offset of the metadata type field in the record. 141 private final HashMap<Integer, Integer> mKeyToPosMap = 142 new HashMap<Integer, Integer>(); 143 144 /** 145 * Helper class to hold a triple (time, duration, text). Can be used to 146 * implement caption. 147 */ 148 public class TimedText { 149 private Date mTime; 150 private int mDuration; // millisec 151 private String mText; 152 TimedText(Date time, int duration, String text)153 public TimedText(Date time, int duration, String text) { 154 mTime = time; 155 mDuration = duration; 156 mText = text; 157 } 158 toString()159 public String toString() { 160 StringBuilder res = new StringBuilder(80); 161 res.append(mTime).append("-").append(mDuration) 162 .append(":").append(mText); 163 return res.toString(); 164 } 165 } 166 Metadata()167 public Metadata() { } 168 169 /** 170 * Go over all the records, collecting metadata keys and records' 171 * type field offset in the Parcel. These are stored in 172 * mKeyToPosMap for latter retrieval. 173 * Format of a metadata record: 174 <pre> 175 1 2 3 176 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 177 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 178 | record size | 179 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 180 | metadata key | // TITLE 181 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 182 | metadata type | // STRING_VAL 183 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 184 | | 185 | .... metadata payload .... | 186 | | 187 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 188 </pre> 189 * @param parcel With the serialized records. 190 * @param bytesLeft How many bytes in the parcel should be processed. 191 * @return false if an error occurred during parsing. 192 */ scanAllRecords(Parcel parcel, int bytesLeft)193 private boolean scanAllRecords(Parcel parcel, int bytesLeft) { 194 int recCount = 0; 195 boolean error = false; 196 197 mKeyToPosMap.clear(); 198 while (bytesLeft > kRecordHeaderSize) { 199 final int start = parcel.dataPosition(); 200 // Check the size. 201 final int size = parcel.readInt(); 202 203 if (size <= kRecordHeaderSize) { // at least 1 byte should be present. 204 Log.e(TAG, "Record is too short"); 205 error = true; 206 break; 207 } 208 209 // Check the metadata key. 210 final int metadataId = parcel.readInt(); 211 if (!checkMetadataId(metadataId)) { 212 error = true; 213 break; 214 } 215 216 // Store the record offset which points to the type 217 // field so we can later on read/unmarshall the record 218 // payload. 219 if (mKeyToPosMap.containsKey(metadataId)) { 220 Log.e(TAG, "Duplicate metadata ID found"); 221 error = true; 222 break; 223 } 224 225 mKeyToPosMap.put(metadataId, parcel.dataPosition()); 226 227 // Check the metadata type. 228 final int metadataType = parcel.readInt(); 229 if (metadataType <= 0 || metadataType > LAST_TYPE) { 230 Log.e(TAG, "Invalid metadata type " + metadataType); 231 error = true; 232 break; 233 } 234 235 // Skip to the next one. 236 parcel.setDataPosition(start + size); 237 bytesLeft -= size; 238 ++recCount; 239 } 240 241 if (0 != bytesLeft || error) { 242 Log.e(TAG, "Ran out of data or error on record " + recCount); 243 mKeyToPosMap.clear(); 244 return false; 245 } else { 246 return true; 247 } 248 } 249 250 /** 251 * Check a parcel containing metadata is well formed. The header 252 * is checked as well as the individual records format. However, the 253 * data inside the record is not checked because we do lazy access 254 * (we check/unmarshall only data the user asks for.) 255 * 256 * Format of a metadata parcel: 257 <pre> 258 1 2 3 259 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 260 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 261 | metadata total size | 262 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 263 | 'M' | 'E' | 'T' | 'A' | 264 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 265 | | 266 | .... metadata records .... | 267 | | 268 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 269 </pre> 270 * 271 * @param parcel With the serialized data. Metadata keeps a 272 * reference on it to access it later on. The caller 273 * should not modify the parcel after this call (and 274 * not call recycle on it.) 275 * @return false if an error occurred. 276 */ parse(Parcel parcel)277 public boolean parse(Parcel parcel) { 278 if (parcel.dataAvail() < kMetaHeaderSize) { 279 Log.e(TAG, "Not enough data " + parcel.dataAvail()); 280 return false; 281 } 282 283 final int pin = parcel.dataPosition(); // to roll back in case of errors. 284 final int size = parcel.readInt(); 285 286 // The extra kInt32Size below is to account for the int32 'size' just read. 287 if (parcel.dataAvail() + kInt32Size < size || size < kMetaHeaderSize) { 288 Log.e(TAG, "Bad size " + size + " avail " + parcel.dataAvail() + " position " + pin); 289 parcel.setDataPosition(pin); 290 return false; 291 } 292 293 // Checks if the 'M' 'E' 'T' 'A' marker is present. 294 final int kShouldBeMetaMarker = parcel.readInt(); 295 if (kShouldBeMetaMarker != kMetaMarker ) { 296 Log.e(TAG, "Marker missing " + Integer.toHexString(kShouldBeMetaMarker)); 297 parcel.setDataPosition(pin); 298 return false; 299 } 300 301 // Scan the records to collect metadata ids and offsets. 302 if (!scanAllRecords(parcel, size - kMetaHeaderSize)) { 303 parcel.setDataPosition(pin); 304 return false; 305 } 306 mParcel = parcel; 307 return true; 308 } 309 310 /** 311 * @return The set of metadata ID found. 312 */ keySet()313 public Set<Integer> keySet() { 314 return mKeyToPosMap.keySet(); 315 } 316 317 /** 318 * @return true if a value is present for the given key. 319 */ has(final int metadataId)320 public boolean has(final int metadataId) { 321 if (!checkMetadataId(metadataId)) { 322 throw new IllegalArgumentException("Invalid key: " + metadataId); 323 } 324 return mKeyToPosMap.containsKey(metadataId); 325 } 326 327 // Accessors. 328 // Caller must make sure the key is present using the {@code has} 329 // method otherwise a RuntimeException will occur. 330 getString(final int key)331 public String getString(final int key) { 332 checkType(key, STRING_VAL); 333 return mParcel.readString(); 334 } 335 getInt(final int key)336 public int getInt(final int key) { 337 checkType(key, INTEGER_VAL); 338 return mParcel.readInt(); 339 } 340 getBoolean(final int key)341 public boolean getBoolean(final int key) { 342 checkType(key, BOOLEAN_VAL); 343 return mParcel.readInt() == 1; 344 } 345 getLong(final int key)346 public long getLong(final int key) { 347 checkType(key, LONG_VAL); 348 return mParcel.readLong(); 349 } 350 getDouble(final int key)351 public double getDouble(final int key) { 352 checkType(key, DOUBLE_VAL); 353 return mParcel.readDouble(); 354 } 355 getByteArray(final int key)356 public byte[] getByteArray(final int key) { 357 checkType(key, BYTE_ARRAY_VAL); 358 return mParcel.createByteArray(); 359 } 360 getDate(final int key)361 public Date getDate(final int key) { 362 checkType(key, DATE_VAL); 363 final long timeSinceEpoch = mParcel.readLong(); 364 final String timeZone = mParcel.readString(); 365 366 if (timeZone.length() == 0) { 367 return new Date(timeSinceEpoch); 368 } else { 369 TimeZone tz = TimeZone.getTimeZone(timeZone); 370 Calendar cal = Calendar.getInstance(tz); 371 372 cal.setTimeInMillis(timeSinceEpoch); 373 return cal.getTime(); 374 } 375 } 376 getTimedText(final int key)377 public TimedText getTimedText(final int key) { 378 checkType(key, TIMED_TEXT_VAL); 379 final Date startTime = new Date(mParcel.readLong()); // epoch 380 final int duration = mParcel.readInt(); // millisec 381 382 return new TimedText(startTime, 383 duration, 384 mParcel.readString()); 385 } 386 387 // @return the last available system metadata id. Ids are 388 // 1-indexed. lastSytemId()389 public static int lastSytemId() { return LAST_SYSTEM; } 390 391 // @return the first available cutom metadata id. firstCustomId()392 public static int firstCustomId() { return FIRST_CUSTOM; } 393 394 // @return the last value of known type. Types are 1-indexed. lastType()395 public static int lastType() { return LAST_TYPE; } 396 397 // Check val is either a system id or a custom one. 398 // @param val Metadata key to test. 399 // @return true if it is in a valid range. checkMetadataId(final int val)400 private boolean checkMetadataId(final int val) { 401 if (val <= ANY || (LAST_SYSTEM < val && val < FIRST_CUSTOM)) { 402 Log.e(TAG, "Invalid metadata ID " + val); 403 return false; 404 } 405 return true; 406 } 407 408 // Check the type of the data match what is expected. checkType(final int key, final int expectedType)409 private void checkType(final int key, final int expectedType) { 410 final int pos = mKeyToPosMap.get(key); 411 412 mParcel.setDataPosition(pos); 413 414 final int type = mParcel.readInt(); 415 if (type != expectedType) { 416 throw new IllegalStateException("Wrong type " + expectedType + " but got " + type); 417 } 418 } 419 } 420