mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
Improvements to ID3 decoder
This commit is contained in:
parent
50aeb20cc2
commit
110c8f6f1f
1 changed files with 202 additions and 192 deletions
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoder;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
|
|
@ -31,6 +32,8 @@ import java.util.Locale;
|
|||
*/
|
||||
public final class Id3Decoder implements MetadataDecoder {
|
||||
|
||||
private static final String TAG = "Id3Decoder";
|
||||
|
||||
private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
|
||||
private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
|
||||
private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
|
||||
|
|
@ -45,201 +48,187 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
public Metadata decode(byte[] data, int size) throws MetadataDecoderException {
|
||||
List<Id3Frame> id3Frames = new ArrayList<>();
|
||||
ParsableByteArray id3Data = new ParsableByteArray(data, size);
|
||||
Id3Header id3Header = decodeId3Header(id3Data);
|
||||
|
||||
int framesBytesLeft = id3Header.framesSize;
|
||||
if (id3Header.isUnsynchronized) {
|
||||
id3Data = removeUnsynchronization(id3Data, id3Header.framesSize);
|
||||
framesBytesLeft = id3Data.bytesLeft();
|
||||
Id3Header id3Header = decodeHeader(id3Data);
|
||||
if (id3Header == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
while (framesBytesLeft > 0) {
|
||||
int frameId0 = id3Data.readUnsignedByte();
|
||||
int frameId1 = id3Data.readUnsignedByte();
|
||||
int frameId2 = id3Data.readUnsignedByte();
|
||||
int frameId3 = id3Header.majorVersion > 2 ? id3Data.readUnsignedByte() : 0;
|
||||
int frameSize = id3Header.majorVersion == 2 ? id3Data.readUnsignedInt24() :
|
||||
id3Header.majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt();
|
||||
int startPosition = id3Data.getPosition();
|
||||
int framesSize = id3Header.framesSize;
|
||||
if (id3Header.isUnsynchronized) {
|
||||
framesSize = removeUnsynchronization(id3Data, id3Header.framesSize);
|
||||
}
|
||||
id3Data.setLimit(startPosition + framesSize);
|
||||
|
||||
if (frameSize <= 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Frame flags.
|
||||
boolean isCompressed = false;
|
||||
boolean isEncrypted = false;
|
||||
boolean isUnsynchronized = false;
|
||||
boolean hasGroupIdentifier = false;
|
||||
boolean hasDataLength = false;
|
||||
|
||||
if (id3Header.majorVersion > 2) {
|
||||
int flags = id3Data.readShort();
|
||||
if (id3Header.majorVersion == 3) {
|
||||
isCompressed = (flags & 0x0080) != 0;
|
||||
isEncrypted = (flags & 0x0040) != 0;
|
||||
hasDataLength = isCompressed;
|
||||
} else {
|
||||
isCompressed = (flags & 0x0008) != 0;
|
||||
isEncrypted = (flags & 0x0004) != 0;
|
||||
isUnsynchronized = (flags & 0x0002) != 0;
|
||||
hasGroupIdentifier = (flags & 0x0040) != 0;
|
||||
hasDataLength = (flags & 0x0001) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
int headerSize = id3Header.majorVersion == 2 ? 6 : 10;
|
||||
|
||||
if (hasGroupIdentifier) {
|
||||
++headerSize;
|
||||
--frameSize;
|
||||
id3Data.skipBytes(1);
|
||||
}
|
||||
|
||||
if (isEncrypted) {
|
||||
++headerSize;
|
||||
--frameSize;
|
||||
id3Data.skipBytes(1);
|
||||
}
|
||||
|
||||
if (hasDataLength) {
|
||||
headerSize += 4;
|
||||
frameSize -= 4;
|
||||
id3Data.skipBytes(4);
|
||||
}
|
||||
|
||||
framesBytesLeft -= frameSize + headerSize;
|
||||
|
||||
if (isCompressed || isEncrypted) {
|
||||
id3Data.skipBytes(frameSize);
|
||||
} else {
|
||||
try {
|
||||
Id3Frame frame;
|
||||
ParsableByteArray frameData = id3Data;
|
||||
if (isUnsynchronized) {
|
||||
frameData = removeUnsynchronization(id3Data, frameSize);
|
||||
frameSize = frameData.bytesLeft();
|
||||
}
|
||||
|
||||
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
|
||||
frame = decodeTxxxFrame(frameData, frameSize);
|
||||
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
|
||||
frame = decodePrivFrame(frameData, frameSize);
|
||||
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
|
||||
frame = decodeGeobFrame(frameData, frameSize);
|
||||
} else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') {
|
||||
frame = decodeApicFrame(frameData, frameSize);
|
||||
} else if (frameId0 == 'T') {
|
||||
String id = frameId3 != 0 ?
|
||||
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
|
||||
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
|
||||
frame = decodeTextInformationFrame(frameData, frameSize, id);
|
||||
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' &&
|
||||
(frameId3 == 'M' || frameId3 == 0)) {
|
||||
frame = decodeCommentFrame(frameData, frameSize);
|
||||
} else {
|
||||
String id = frameId3 != 0 ?
|
||||
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
|
||||
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
|
||||
frame = decodeBinaryFrame(frameData, frameSize, id);
|
||||
}
|
||||
id3Frames.add(frame);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new MetadataDecoderException("Unsupported character encoding");
|
||||
}
|
||||
int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
|
||||
while (id3Data.bytesLeft() >= frameHeaderSize) {
|
||||
Id3Frame frame = decodeFrame(id3Header, id3Data);
|
||||
if (frame != null) {
|
||||
id3Frames.add(frame);
|
||||
}
|
||||
}
|
||||
|
||||
return new Metadata(id3Frames);
|
||||
}
|
||||
|
||||
private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
|
||||
int terminationPos = indexOfZeroByte(data, fromIndex);
|
||||
|
||||
// For single byte encoding charsets, we're done.
|
||||
if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) {
|
||||
return terminationPos;
|
||||
}
|
||||
|
||||
// Otherwise ensure an even index and look for a second zero byte.
|
||||
while (terminationPos < data.length - 1) {
|
||||
if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) {
|
||||
return terminationPos;
|
||||
}
|
||||
terminationPos = indexOfZeroByte(data, terminationPos + 1);
|
||||
}
|
||||
|
||||
return data.length;
|
||||
}
|
||||
|
||||
private static int indexOfZeroByte(byte[] data, int fromIndex) {
|
||||
for (int i = fromIndex; i < data.length; i++) {
|
||||
if (data[i] == (byte) 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return data.length;
|
||||
}
|
||||
|
||||
private static int delimiterLength(int encodingByte) {
|
||||
return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8)
|
||||
? 1 : 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id3Buffer A {@link ParsableByteArray} from which data should be read.
|
||||
* @return The parsed header.
|
||||
* @throws MetadataDecoderException If ID3 file identifier != "ID3".
|
||||
* @param data A {@link ParsableByteArray} from which the header should be read.
|
||||
* @return The parsed header, or null if the ID3 tag is unsupported.
|
||||
* @throws MetadataDecoderException If the first three bytes differ from "ID3".
|
||||
*/
|
||||
private static Id3Header decodeId3Header(ParsableByteArray id3Buffer)
|
||||
private static Id3Header decodeHeader(ParsableByteArray data)
|
||||
throws MetadataDecoderException {
|
||||
int id1 = id3Buffer.readUnsignedByte();
|
||||
int id2 = id3Buffer.readUnsignedByte();
|
||||
int id3 = id3Buffer.readUnsignedByte();
|
||||
int id1 = data.readUnsignedByte();
|
||||
int id2 = data.readUnsignedByte();
|
||||
int id3 = data.readUnsignedByte();
|
||||
if (id1 != 'I' || id2 != 'D' || id3 != '3') {
|
||||
throw new MetadataDecoderException(String.format(Locale.US,
|
||||
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
|
||||
"Unexpected ID3 tag identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
|
||||
}
|
||||
|
||||
int majorVersion = id3Buffer.readUnsignedByte();
|
||||
id3Buffer.skipBytes(1); // Skip minor version.
|
||||
boolean isUnsynchronized = false;
|
||||
int majorVersion = data.readUnsignedByte();
|
||||
data.skipBytes(1); // Skip minor version.
|
||||
int flags = data.readUnsignedByte();
|
||||
int framesSize = data.readSynchSafeInt();
|
||||
|
||||
int flags = id3Buffer.readUnsignedByte();
|
||||
int framesSize = id3Buffer.readSynchSafeInt();
|
||||
|
||||
if (majorVersion < 4) {
|
||||
// this flag is advisory in version 4, use the frame flags instead
|
||||
isUnsynchronized = (flags & 0x80) != 0;
|
||||
}
|
||||
|
||||
if (majorVersion == 3) {
|
||||
// check for extended header
|
||||
if ((flags & 0x40) != 0) {
|
||||
int extendedHeaderSize = id3Buffer.readInt(); // size excluding size field
|
||||
if (extendedHeaderSize == 6 || extendedHeaderSize == 10) {
|
||||
id3Buffer.skipBytes(extendedHeaderSize);
|
||||
framesSize -= (extendedHeaderSize + 4);
|
||||
}
|
||||
if (majorVersion == 2) {
|
||||
boolean isCompressed = (flags & 0x40) != 0;
|
||||
if (isCompressed) {
|
||||
Log.w(TAG, "Skipped ID3 tag with majorVersion=1 and undefined compression scheme");
|
||||
return null;
|
||||
}
|
||||
} else if (majorVersion >= 4) {
|
||||
// check for extended header
|
||||
if ((flags & 0x40) != 0) {
|
||||
int extendedHeaderSize = id3Buffer.readSynchSafeInt(); // size including size field
|
||||
if (extendedHeaderSize > 4) {
|
||||
id3Buffer.skipBytes(extendedHeaderSize - 4);
|
||||
}
|
||||
} else if (majorVersion == 3) {
|
||||
boolean hasExtendedHeader = (flags & 0x40) != 0;
|
||||
if (hasExtendedHeader) {
|
||||
int extendedHeaderSize = data.readInt(); // Size excluding size field.
|
||||
data.skipBytes(extendedHeaderSize);
|
||||
framesSize -= (extendedHeaderSize + 4);
|
||||
}
|
||||
} else if (majorVersion == 4) {
|
||||
boolean hasExtendedHeader = (flags & 0x40) != 0;
|
||||
if (hasExtendedHeader) {
|
||||
int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field.
|
||||
data.skipBytes(extendedHeaderSize - 4);
|
||||
framesSize -= extendedHeaderSize;
|
||||
}
|
||||
|
||||
// Check if footer presents.
|
||||
if ((flags & 0x10) != 0) {
|
||||
boolean hasFooter = (flags & 0x10) != 0;
|
||||
if (hasFooter) {
|
||||
framesSize -= 10;
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Skipped ID3 tag with unsupported majorVersion=" + majorVersion);
|
||||
return null;
|
||||
}
|
||||
|
||||
// isUnsynchronized is advisory only in version 4. Frame level flags are used instead.
|
||||
boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0;
|
||||
return new Id3Header(majorVersion, isUnsynchronized, framesSize);
|
||||
}
|
||||
|
||||
private Id3Frame decodeFrame(Id3Header id3Header, ParsableByteArray id3Data)
|
||||
throws MetadataDecoderException {
|
||||
int frameId0 = id3Data.readUnsignedByte();
|
||||
int frameId1 = id3Data.readUnsignedByte();
|
||||
int frameId2 = id3Data.readUnsignedByte();
|
||||
int frameId3 = id3Header.majorVersion >= 3 ? id3Data.readUnsignedByte() : 0;
|
||||
|
||||
int frameSize;
|
||||
if (id3Header.majorVersion == 4) {
|
||||
frameSize = id3Data.readUnsignedIntToInt();
|
||||
if ((frameSize & 0x808080L) == 0) {
|
||||
// Parse the frame size as a syncsafe integer, as per the spec.
|
||||
frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
|
||||
| (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
|
||||
} else {
|
||||
// Proceed using the frame size read as an unsigned integer.
|
||||
Log.w(TAG, "Frame size not specified as syncsafe integer");
|
||||
}
|
||||
} else if (id3Header.majorVersion == 3) {
|
||||
frameSize = id3Data.readUnsignedIntToInt();
|
||||
} else /* id3Header.majorVersion == 2 */ {
|
||||
frameSize = id3Data.readUnsignedInt24();
|
||||
}
|
||||
|
||||
int flags = id3Header.majorVersion >= 2 ? id3Data.readShort() : 0;
|
||||
if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0
|
||||
&& flags == 0) {
|
||||
// We must be reading zero padding at the end of the tag.
|
||||
id3Data.setPosition(id3Data.limit());
|
||||
return null;
|
||||
}
|
||||
|
||||
int nextFramePosition = id3Data.getPosition() + frameSize;
|
||||
|
||||
// Frame flags.
|
||||
boolean isCompressed = false;
|
||||
boolean isEncrypted = false;
|
||||
boolean isUnsynchronized = false;
|
||||
boolean hasDataLength = false;
|
||||
boolean hasGroupIdentifier = false;
|
||||
if (id3Header.majorVersion == 3) {
|
||||
isCompressed = (flags & 0x0080) != 0;
|
||||
isEncrypted = (flags & 0x0040) != 0;
|
||||
hasGroupIdentifier = (flags & 0x0020) != 0;
|
||||
hasDataLength = isCompressed;
|
||||
} else if (id3Header.majorVersion == 4) {
|
||||
hasGroupIdentifier = (flags & 0x0040) != 0;
|
||||
isCompressed = (flags & 0x0008) != 0;
|
||||
isEncrypted = (flags & 0x0004) != 0;
|
||||
isUnsynchronized = (flags & 0x0002) != 0;
|
||||
hasDataLength = (flags & 0x0001) != 0;
|
||||
}
|
||||
|
||||
if (isCompressed || isEncrypted) {
|
||||
Log.w(TAG, "Skipping unsupported compressed or encrypted frame");
|
||||
id3Data.setPosition(nextFramePosition);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasGroupIdentifier) {
|
||||
frameSize--;
|
||||
id3Data.skipBytes(1);
|
||||
}
|
||||
if (hasDataLength) {
|
||||
frameSize -= 4;
|
||||
id3Data.skipBytes(4);
|
||||
}
|
||||
if (isUnsynchronized) {
|
||||
frameSize = removeUnsynchronization(id3Data, frameSize);
|
||||
}
|
||||
|
||||
try {
|
||||
Id3Frame frame;
|
||||
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
|
||||
frame = decodeTxxxFrame(id3Data, frameSize);
|
||||
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
|
||||
frame = decodePrivFrame(id3Data, frameSize);
|
||||
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
|
||||
frame = decodeGeobFrame(id3Data, frameSize);
|
||||
} else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') {
|
||||
frame = decodeApicFrame(id3Data, frameSize);
|
||||
} else if (frameId0 == 'T') {
|
||||
String id = frameId3 != 0 ?
|
||||
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
|
||||
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
|
||||
frame = decodeTextInformationFrame(id3Data, frameSize, id);
|
||||
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' &&
|
||||
(frameId3 == 'M' || frameId3 == 0)) {
|
||||
frame = decodeCommentFrame(id3Data, frameSize);
|
||||
} else {
|
||||
String id = frameId3 != 0 ?
|
||||
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
|
||||
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
|
||||
frame = decodeBinaryFrame(id3Data, frameSize, id);
|
||||
}
|
||||
return frame;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new MetadataDecoderException("Unsupported character encoding");
|
||||
} finally {
|
||||
id3Data.setPosition(nextFramePosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
|
||||
throws UnsupportedEncodingException {
|
||||
int encoding = id3Data.readUnsignedByte();
|
||||
|
|
@ -368,34 +357,22 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Undo the unsynchronization applied to one or more frames.
|
||||
* @param dataSource The original data, positioned at the beginning of a frame.
|
||||
* @param count The number of valid bytes in the frames to be processed.
|
||||
* @return replacement data for the frames.
|
||||
* Performs in-place removal of unsynchronization for {@code length} bytes starting from
|
||||
* {@link ParsableByteArray#getPosition()}
|
||||
*
|
||||
* @param data Contains the data to be processed.
|
||||
* @param length The length of the data to be processed.
|
||||
* @return The length of the data after processing.
|
||||
*/
|
||||
private static ParsableByteArray removeUnsynchronization(ParsableByteArray dataSource, int count) {
|
||||
byte[] source = dataSource.data;
|
||||
int sourceIndex = dataSource.getPosition();
|
||||
int limit = sourceIndex + count;
|
||||
byte[] dest = new byte[count];
|
||||
int destIndex = 0;
|
||||
|
||||
while (sourceIndex < limit) {
|
||||
byte b = source[sourceIndex++];
|
||||
if ((b & 0xFF) == 0xFF) {
|
||||
int nextIndex = sourceIndex+1;
|
||||
if (nextIndex < limit) {
|
||||
int b2 = source[nextIndex];
|
||||
if (b2 == 0) {
|
||||
// skip the 0 byte
|
||||
++sourceIndex;
|
||||
}
|
||||
}
|
||||
private static int removeUnsynchronization(ParsableByteArray data, int length) {
|
||||
byte[] bytes = data.data;
|
||||
for (int i = data.getPosition(); i + 1 < length; i++) {
|
||||
if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {
|
||||
System.arraycopy(bytes, i + 2, bytes, i + 1, length - i - 2);
|
||||
length--;
|
||||
}
|
||||
dest[destIndex++] = b;
|
||||
}
|
||||
|
||||
return new ParsableByteArray(dest, destIndex);
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -418,6 +395,39 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
}
|
||||
}
|
||||
|
||||
private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
|
||||
int terminationPos = indexOfZeroByte(data, fromIndex);
|
||||
|
||||
// For single byte encoding charsets, we're done.
|
||||
if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) {
|
||||
return terminationPos;
|
||||
}
|
||||
|
||||
// Otherwise ensure an even index and look for a second zero byte.
|
||||
while (terminationPos < data.length - 1) {
|
||||
if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) {
|
||||
return terminationPos;
|
||||
}
|
||||
terminationPos = indexOfZeroByte(data, terminationPos + 1);
|
||||
}
|
||||
|
||||
return data.length;
|
||||
}
|
||||
|
||||
private static int indexOfZeroByte(byte[] data, int fromIndex) {
|
||||
for (int i = fromIndex; i < data.length; i++) {
|
||||
if (data[i] == (byte) 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return data.length;
|
||||
}
|
||||
|
||||
private static int delimiterLength(int encodingByte) {
|
||||
return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8)
|
||||
? 1 : 2;
|
||||
}
|
||||
|
||||
public static String decodeGenre(int code) {
|
||||
return (0 < code && code <= standardGenres.length) ? standardGenres[code - 1] : null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue