mirror of
https://github.com/samsonjs/media.git
synced 2026-03-31 10:25:48 +00:00
Further enhance ID3 decoder + support
This commit is contained in:
parent
e2ff401ea1
commit
7594f5b78b
4 changed files with 175 additions and 181 deletions
|
|
@ -16,6 +16,8 @@
|
|||
package com.google.android.exoplayer2.extractor;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
|
@ -65,6 +67,25 @@ public final class GaplessInfoHolder {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the holder with data parsed from ID3 {@link Metadata}.
|
||||
*
|
||||
* @param metadata The metadata from which to parse the gapless information.
|
||||
* @return Whether the holder was populated.
|
||||
*/
|
||||
public boolean setFromMetadata(Metadata metadata) {
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
Metadata.Entry entry = metadata.get(i);
|
||||
if (entry instanceof CommentFrame) {
|
||||
CommentFrame commentFrame = (CommentFrame) entry;
|
||||
if (setFromComment(commentFrame.description, commentFrame.text)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
|
||||
* or MPEG 4 user data), if valid and non-zero.
|
||||
|
|
|
|||
|
|
@ -1,95 +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.extractor.mp3;
|
||||
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Utility for parsing ID3 version 2 metadata in MP3 files.
|
||||
*/
|
||||
/* package */ final class Id3Util {
|
||||
|
||||
/**
|
||||
* The maximum valid length for metadata in bytes.
|
||||
*/
|
||||
private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024;
|
||||
|
||||
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
|
||||
|
||||
/**
|
||||
* Peeks data from the input and parses ID3 metadata, including gapless playback information.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} from which data should be peeked.
|
||||
* @return The metadata, if present, {@code null} otherwise.
|
||||
* @throws IOException If an error occurred peeking from the input.
|
||||
* @throws InterruptedException If the thread was interrupted.
|
||||
*/
|
||||
public static Metadata parseId3(ExtractorInput input)
|
||||
throws IOException, InterruptedException {
|
||||
Metadata result = null;
|
||||
ParsableByteArray scratch = new ParsableByteArray(10);
|
||||
int peekedId3Bytes = 0;
|
||||
while (true) {
|
||||
input.peekFully(scratch.data, 0, 10);
|
||||
scratch.setPosition(0);
|
||||
if (scratch.readUnsignedInt24() != ID3_TAG) {
|
||||
break;
|
||||
}
|
||||
|
||||
int majorVersion = scratch.readUnsignedByte();
|
||||
int minorVersion = scratch.readUnsignedByte();
|
||||
int flags = scratch.readUnsignedByte();
|
||||
int length = scratch.readSynchSafeInt();
|
||||
int frameLength = length + 10;
|
||||
|
||||
try {
|
||||
if (canParseMetadata(majorVersion, minorVersion, flags, length)) {
|
||||
input.resetPeekPosition();
|
||||
byte[] frame = new byte[frameLength];
|
||||
input.peekFully(frame, 0, frameLength);
|
||||
return new Id3Decoder().decode(frame, frameLength);
|
||||
} else {
|
||||
input.advancePeekPosition(length);
|
||||
}
|
||||
} catch (MetadataDecoderException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
peekedId3Bytes += frameLength;
|
||||
}
|
||||
input.resetPeekPosition();
|
||||
input.advancePeekPosition(peekedId3Bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
|
||||
int length) {
|
||||
return minorVersion != 0xFF && majorVersion >= 2 && majorVersion <= 4
|
||||
&& length <= MAXIMUM_METADATA_SIZE
|
||||
&& !(majorVersion == 2 && ((flags & 0x3F) != 0 || (flags & 0x40) != 0))
|
||||
&& !(majorVersion == 3 && (flags & 0x1F) != 0)
|
||||
&& !(majorVersion == 4 && (flags & 0x0F) != 0);
|
||||
}
|
||||
|
||||
private Id3Util() {}
|
||||
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.mp3;
|
||||
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
|
|
@ -28,7 +29,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
|
|||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.EOFException;
|
||||
|
|
@ -51,6 +53,8 @@ public final class Mp3Extractor implements Extractor {
|
|||
|
||||
};
|
||||
|
||||
private static final String TAG = "Mp3Extractor";
|
||||
|
||||
/**
|
||||
* The maximum number of bytes to search when synchronizing, before giving up.
|
||||
*/
|
||||
|
|
@ -59,6 +63,18 @@ public final class Mp3Extractor implements Extractor {
|
|||
* The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
|
||||
*/
|
||||
private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
|
||||
/**
|
||||
* First three bytes of a well formed ID3 tag header.
|
||||
*/
|
||||
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
|
||||
/**
|
||||
* Length of an ID3 tag header.
|
||||
*/
|
||||
private static final int ID3_HEADER_LENGTH = 10;
|
||||
/**
|
||||
* Maximum length of data read into {@link #scratch}.
|
||||
*/
|
||||
private static final int SCRATCH_LENGTH = 10;
|
||||
|
||||
/**
|
||||
* Mask that includes the audio header values that must match between frames.
|
||||
|
|
@ -100,7 +116,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
*/
|
||||
public Mp3Extractor(long forcedFirstSampleTimestampUs) {
|
||||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||
scratch = new ParsableByteArray(4);
|
||||
scratch = new ParsableByteArray(SCRATCH_LENGTH);
|
||||
synchronizedHeader = new MpegAudioHeader();
|
||||
gaplessInfoHolder = new GaplessInfoHolder();
|
||||
basisTimeUs = C.TIME_UNSET;
|
||||
|
|
@ -147,7 +163,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
|
||||
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
|
||||
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
|
||||
gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata));
|
||||
gaplessInfoHolder.encoderPadding, null, null, 0, null, null));
|
||||
}
|
||||
return readSample(input);
|
||||
}
|
||||
|
|
@ -202,18 +218,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
|
||||
input.resetPeekPosition();
|
||||
if (input.getPosition() == 0) {
|
||||
metadata = Id3Util.parseId3(input);
|
||||
if (!gaplessInfoHolder.hasGaplessInfo()) {
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
Metadata.Entry entry = metadata.get(i);
|
||||
if (entry instanceof CommentFrame) {
|
||||
CommentFrame commentFrame = (CommentFrame) entry;
|
||||
if (gaplessInfoHolder.setFromComment(commentFrame.description, commentFrame.text)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
peekId3Data(input);
|
||||
peekedId3Bytes = (int) input.getPeekPosition();
|
||||
if (!sniffing) {
|
||||
input.skipFully(peekedId3Bytes);
|
||||
|
|
@ -267,6 +272,49 @@ public final class Mp3Extractor implements Extractor {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peeks ID3 data from the input, including gapless playback information.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} from which data should be peeked.
|
||||
* @throws IOException If an error occurred peeking from the input.
|
||||
* @throws InterruptedException If the thread was interrupted.
|
||||
*/
|
||||
private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
|
||||
int peekedId3Bytes = 0;
|
||||
while (true) {
|
||||
input.peekFully(scratch.data, 0, ID3_HEADER_LENGTH);
|
||||
scratch.setPosition(0);
|
||||
if (scratch.readUnsignedInt24() != ID3_TAG) {
|
||||
// Not an ID3 tag.
|
||||
break;
|
||||
}
|
||||
scratch.skipBytes(3); // Skip major version, minor version and flags.
|
||||
int framesLength = scratch.readSynchSafeInt();
|
||||
int tagLength = ID3_HEADER_LENGTH + framesLength;
|
||||
|
||||
try {
|
||||
if (metadata == null) {
|
||||
byte[] id3Data = new byte[tagLength];
|
||||
System.arraycopy(scratch.data, 0, id3Data, 0, ID3_HEADER_LENGTH);
|
||||
input.peekFully(id3Data, ID3_HEADER_LENGTH, framesLength);
|
||||
metadata = new Id3Decoder().decode(id3Data, tagLength);
|
||||
if (metadata != null) {
|
||||
gaplessInfoHolder.setFromMetadata(metadata);
|
||||
}
|
||||
} else {
|
||||
input.advancePeekPosition(framesLength);
|
||||
}
|
||||
} catch (MetadataDecoderException e) {
|
||||
Log.e(TAG, "Failed to decode ID3 tag", e);
|
||||
}
|
||||
|
||||
peekedId3Bytes += tagLength;
|
||||
}
|
||||
|
||||
input.resetPeekPosition();
|
||||
input.advancePeekPosition(peekedId3Bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide
|
||||
* data from the start of the first frame in the stream. On returning, the input's position will
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
|
||||
int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
|
||||
while (id3Data.bytesLeft() >= frameHeaderSize) {
|
||||
Id3Frame frame = decodeFrame(id3Header, id3Data);
|
||||
Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data);
|
||||
if (frame != null) {
|
||||
id3Frames.add(frame);
|
||||
}
|
||||
|
|
@ -72,6 +72,40 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
return new Metadata(id3Frames);
|
||||
}
|
||||
|
||||
// TODO: Move the following three methods nearer to the bottom of the file.
|
||||
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 data A {@link ParsableByteArray} from which the header should be read.
|
||||
* @return The parsed header, or null if the ID3 tag is unsupported.
|
||||
|
|
@ -126,15 +160,15 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
return new Id3Header(majorVersion, isUnsynchronized, framesSize);
|
||||
}
|
||||
|
||||
private Id3Frame decodeFrame(Id3Header id3Header, ParsableByteArray id3Data)
|
||||
private Id3Frame decodeFrame(int majorVersion, 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 frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0;
|
||||
|
||||
int frameSize;
|
||||
if (id3Header.majorVersion == 4) {
|
||||
if (majorVersion == 4) {
|
||||
frameSize = id3Data.readUnsignedIntToInt();
|
||||
if ((frameSize & 0x808080L) == 0) {
|
||||
// Parse the frame size as a syncsafe integer, as per the spec.
|
||||
|
|
@ -144,13 +178,13 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
// 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) {
|
||||
} else if (majorVersion == 3) {
|
||||
frameSize = id3Data.readUnsignedIntToInt();
|
||||
} else /* id3Header.majorVersion == 2 */ {
|
||||
frameSize = id3Data.readUnsignedInt24();
|
||||
}
|
||||
|
||||
int flags = id3Header.majorVersion >= 2 ? id3Data.readShort() : 0;
|
||||
int flags = majorVersion >= 3 ? 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.
|
||||
|
|
@ -159,6 +193,9 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
}
|
||||
|
||||
int nextFramePosition = id3Data.getPosition() + frameSize;
|
||||
if (nextFramePosition > id3Data.limit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Frame flags.
|
||||
boolean isCompressed = false;
|
||||
|
|
@ -166,12 +203,12 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
boolean isUnsynchronized = false;
|
||||
boolean hasDataLength = false;
|
||||
boolean hasGroupIdentifier = false;
|
||||
if (id3Header.majorVersion == 3) {
|
||||
if (majorVersion == 3) {
|
||||
isCompressed = (flags & 0x0080) != 0;
|
||||
isEncrypted = (flags & 0x0040) != 0;
|
||||
hasGroupIdentifier = (flags & 0x0020) != 0;
|
||||
hasDataLength = isCompressed;
|
||||
} else if (id3Header.majorVersion == 4) {
|
||||
} else if (majorVersion == 4) {
|
||||
hasGroupIdentifier = (flags & 0x0040) != 0;
|
||||
isCompressed = (flags & 0x0008) != 0;
|
||||
isEncrypted = (flags & 0x0004) != 0;
|
||||
|
|
@ -199,26 +236,29 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
|
||||
try {
|
||||
Id3Frame frame;
|
||||
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
|
||||
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'
|
||||
&& (majorVersion == 2 || 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') {
|
||||
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O'
|
||||
&& (frameId3 == 'B' || majorVersion == 2)) {
|
||||
frame = decodeGeobFrame(id3Data, frameSize);
|
||||
} else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') {
|
||||
frame = decodeApicFrame(id3Data, frameSize);
|
||||
} else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C')
|
||||
: (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) {
|
||||
frame = decodeApicFrame(id3Data, frameSize, majorVersion);
|
||||
} 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);
|
||||
String id = majorVersion == 2
|
||||
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
|
||||
: String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
|
||||
frame = decodeTextInformationFrame(id3Data, frameSize, id);
|
||||
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' &&
|
||||
(frameId3 == 'M' || frameId3 == 0)) {
|
||||
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M'
|
||||
&& (frameId3 == 'M' || majorVersion == 2)) {
|
||||
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);
|
||||
String id = majorVersion == 2
|
||||
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
|
||||
: String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
|
||||
frame = decodeBinaryFrame(id3Data, frameSize, id);
|
||||
}
|
||||
return frame;
|
||||
|
|
@ -288,16 +328,29 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
return new GeobFrame(mimeType, filename, description, objectData);
|
||||
}
|
||||
|
||||
private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize)
|
||||
throws UnsupportedEncodingException {
|
||||
private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize,
|
||||
int majorVersion) throws UnsupportedEncodingException {
|
||||
int encoding = id3Data.readUnsignedByte();
|
||||
String charset = getCharsetName(encoding);
|
||||
|
||||
byte[] data = new byte[frameSize - 1];
|
||||
id3Data.readBytes(data, 0, frameSize - 1);
|
||||
|
||||
int mimeTypeEndIndex = indexOfZeroByte(data, 0);
|
||||
String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1");
|
||||
String mimeType;
|
||||
int mimeTypeEndIndex;
|
||||
if (majorVersion == 2) {
|
||||
mimeTypeEndIndex = 2;
|
||||
mimeType = "image/" + new String(data, 0, 3, "ISO-8859-1").toLowerCase();
|
||||
if (mimeType.equals("image/jpg")) {
|
||||
mimeType = "image/jpeg";
|
||||
}
|
||||
} else {
|
||||
mimeTypeEndIndex = indexOfZeroByte(data, 0);
|
||||
mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1").toLowerCase();
|
||||
if (mimeType.indexOf('/') == -1) {
|
||||
mimeType = "image/" + mimeType;
|
||||
}
|
||||
}
|
||||
|
||||
int pictureType = data[mimeTypeEndIndex + 1] & 0xFF;
|
||||
|
||||
|
|
@ -312,20 +365,6 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
return new ApicFrame(mimeType, description, pictureType, pictureData);
|
||||
}
|
||||
|
||||
private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
|
||||
int frameSize, String id) throws UnsupportedEncodingException {
|
||||
int encoding = id3Data.readUnsignedByte();
|
||||
String charset = getCharsetName(encoding);
|
||||
|
||||
byte[] data = new byte[frameSize - 1];
|
||||
id3Data.readBytes(data, 0, frameSize - 1);
|
||||
|
||||
int descriptionEndIndex = indexOfEos(data, 0, encoding);
|
||||
String description = new String(data, 0, descriptionEndIndex, charset);
|
||||
|
||||
return new TextInformationFrame(id, description);
|
||||
}
|
||||
|
||||
private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize)
|
||||
throws UnsupportedEncodingException {
|
||||
int encoding = id3Data.readUnsignedByte();
|
||||
|
|
@ -348,6 +387,20 @@ public final class Id3Decoder implements MetadataDecoder {
|
|||
return new CommentFrame(language, description, text);
|
||||
}
|
||||
|
||||
private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
|
||||
int frameSize, String id) throws UnsupportedEncodingException {
|
||||
int encoding = id3Data.readUnsignedByte();
|
||||
String charset = getCharsetName(encoding);
|
||||
|
||||
byte[] data = new byte[frameSize - 1];
|
||||
id3Data.readBytes(data, 0, frameSize - 1);
|
||||
|
||||
int descriptionEndIndex = indexOfEos(data, 0, encoding);
|
||||
String description = new String(data, 0, descriptionEndIndex, charset);
|
||||
|
||||
return new TextInformationFrame(id, description);
|
||||
}
|
||||
|
||||
private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,
|
||||
String id) {
|
||||
byte[] frame = new byte[frameSize];
|
||||
|
|
@ -395,39 +448,6 @@ 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;
|
||||
}
|
||||
|
||||
private static final class Id3Header {
|
||||
|
||||
private final int majorVersion;
|
||||
|
|
|
|||
Loading…
Reference in a new issue