Merge ID3 parsing improvements from GitHub.

- Parse APIC and TextInformation frames.
- In MPEG-TS, don't mind if packets contain end padding.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=124079930
This commit is contained in:
olly 2016-06-05 06:23:41 -07:00 committed by Oliver Woodman
parent 444d21563c
commit cb9a64da33
7 changed files with 263 additions and 90 deletions

View file

@ -30,9 +30,11 @@ import com.google.android.exoplayer.TrackGroupArray;
import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.drm.UnsupportedDrmException;
import com.google.android.exoplayer.metadata.id3.ApicFrame;
import com.google.android.exoplayer.metadata.id3.GeobFrame;
import com.google.android.exoplayer.metadata.id3.Id3Frame;
import com.google.android.exoplayer.metadata.id3.PrivFrame;
import com.google.android.exoplayer.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer.metadata.id3.TxxxFrame;
import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.Cue;
@ -533,6 +535,14 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
GeobFrame geobFrame = (GeobFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description));
} else if (id3Frame instanceof ApicFrame) {
ApicFrame apicFrame = (ApicFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description));
} else if (id3Frame instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id,
textInformationFrame.description));
} else {
Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id));
}

View file

@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer.metadata.id3;
import com.google.android.exoplayer.ParserException;
import android.test.MoreAsserts;
import junit.framework.TestCase;
import java.util.List;
@ -24,21 +27,42 @@ import java.util.List;
*/
public class Id3ParserTest extends TestCase {
public void testParseTxxxFrames() {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31,
0, 0, 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50,
55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0};
public void testParseTxxxFrame() throws ParserException {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31, 0, 0,
3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54,
54, 52, 95, 115, 116, 97, 114, 116, 0};
Id3Parser parser = new Id3Parser();
try {
List<Id3Frame> id3Frames = parser.parse(rawId3, rawId3.length);
assertNotNull(id3Frames);
assertEquals(1, id3Frames.size());
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0);
assertEquals("", txxxFrame.description);
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
} catch (Exception exception) {
fail(exception.getMessage());
}
List<Id3Frame> id3Frames = parser.parse(rawId3, rawId3.length);
assertEquals(1, id3Frames.size());
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0);
assertEquals("", txxxFrame.description);
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
}
public void testParseApicFrame() throws ParserException {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 45, 65, 80, 73, 67, 0, 0, 0, 35, 0, 0,
3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87,
111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
Id3Parser parser = new Id3Parser();
List<Id3Frame> id3Frames = parser.parse(rawId3, rawId3.length);
assertEquals(1, id3Frames.size());
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0);
assertEquals("image/jpeg", apicFrame.mimeType);
assertEquals(16, apicFrame.pictureType);
assertEquals("Hello World", apicFrame.description);
assertEquals(10, apicFrame.pictureData.length);
MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, apicFrame.pictureData);
}
public void testParseTextInformationFrame() throws ParserException {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0,
3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0};
Id3Parser parser = new Id3Parser();
List<Id3Frame> id3Frames = parser.parse(rawId3, rawId3.length);
assertEquals(1, id3Frames.size());
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0);
assertEquals("TIT2", textInformationFrame.id);
assertEquals("Hello World", textInformationFrame.description);
}
}

View file

@ -79,8 +79,9 @@ import com.google.android.exoplayer.util.ParsableByteArray;
}
}
// Write data to the output.
output.sampleData(data, bytesAvailable);
sampleBytesRead += bytesAvailable;
int bytesToWrite = Math.min(bytesAvailable, sampleSize - sampleBytesRead);
output.sampleData(data, bytesToWrite);
sampleBytesRead += bytesToWrite;
}
@Override

View file

@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer.metadata;
import java.io.IOException;
import com.google.android.exoplayer.ParserException;
/**
* Parses objects of type <T> from binary data.
@ -38,8 +38,8 @@ public interface MetadataParser<T> {
* @param data The raw binary data from which to parse the metadata.
* @param size The size of the input data.
* @return @return A parsed metadata object of type <T>.
* @throws IOException If a problem occurred parsing the data.
* @throws ParserException If a problem occurred parsing the data.
*/
T parse(byte[] data, int size) throws IOException;
T parse(byte[] data, int size) throws ParserException;
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (C) 2014 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.exoplayer.metadata.id3;
/**
* APIC (Attached Picture) ID3 frame.
*/
public final class ApicFrame extends Id3Frame {
public static final String ID = "APIC";
public final String mimeType;
public final String description;
public final int pictureType;
public final byte[] pictureData;
public ApicFrame(String mimeType, String description, int pictureType, byte[] pictureData) {
super(ID);
this.mimeType = mimeType;
this.description = description;
this.pictureType = pictureType;
this.pictureData = pictureData;
}
}

View file

@ -43,8 +43,7 @@ public final class Id3Parser implements MetadataParser<List<Id3Frame>> {
}
@Override
public List<Id3Frame> parse(byte[] data, int size) throws UnsupportedEncodingException,
ParserException {
public List<Id3Frame> parse(byte[] data, int size) throws ParserException {
List<Id3Frame> id3Frames = new ArrayList<>();
ParsableByteArray id3Data = new ParsableByteArray(data, size);
int id3Size = parseId3Header(id3Data);
@ -62,94 +61,61 @@ public final class Id3Parser implements MetadataParser<List<Id3Frame>> {
// Skip frame flags.
id3Data.skipBytes(2);
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] frame = new byte[frameSize - 1];
id3Data.readBytes(frame, 0, frameSize - 1);
int descriptionEndIndex = indexOfEOS(frame, 0, encoding);
String description = new String(frame, 0, descriptionEndIndex, charset);
int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding);
String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, charset);
id3Frames.add(new TxxxFrame(description, value));
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
byte[] frame = new byte[frameSize];
id3Data.readBytes(frame, 0, frameSize);
int ownerEndIndex = indexOf(frame, 0, (byte) 0);
String owner = new String(frame, 0, ownerEndIndex, "ISO-8859-1");
byte[] privateData = Arrays.copyOfRange(frame, ownerEndIndex + 1, frame.length);
id3Frames.add(new PrivFrame(owner, privateData));
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] frame = new byte[frameSize - 1];
id3Data.readBytes(frame, 0, frameSize - 1);
int mimeTypeEndIndex = indexOf(frame, 0, (byte) 0);
String mimeType = new String(frame, 0, mimeTypeEndIndex, "ISO-8859-1");
int filenameStartIndex = mimeTypeEndIndex + 1;
int filenameEndIndex = indexOfEOS(frame, filenameStartIndex, encoding);
String filename = new String(frame, filenameStartIndex,
filenameEndIndex - filenameStartIndex, charset);
int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
int descriptionEndIndex = indexOfEOS(frame, descriptionStartIndex, encoding);
String description = new String(frame, descriptionStartIndex,
descriptionEndIndex - descriptionStartIndex, charset);
int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
byte[] objectData = Arrays.copyOfRange(frame, objectDataStartIndex, frame.length);
id3Frames.add(new GeobFrame(mimeType, filename, description, objectData));
} else {
String type = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
byte[] frame = new byte[frameSize];
id3Data.readBytes(frame, 0, frameSize);
id3Frames.add(new BinaryFrame(type, frame));
try {
Id3Frame frame;
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
frame = parseTxxxFrame(id3Data, frameSize);
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
frame = parsePrivFrame(id3Data, frameSize);
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
frame = parseGeobFrame(id3Data, frameSize);
} else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') {
frame = parseApicFrame(id3Data, frameSize);
} else if (frameId0 == 'T') {
String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = parseTextInformationFrame(id3Data, frameSize, id);
} else {
String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = parseBinaryFrame(id3Data, frameSize, id);
}
id3Frames.add(frame);
id3Size -= frameSize + 10 /* header size */;
} catch (UnsupportedEncodingException e) {
throw new ParserException(e);
}
id3Size -= frameSize + 10 /* header size */;
}
return Collections.unmodifiableList(id3Frames);
}
private static int indexOf(byte[] data, int fromIndex, byte key) {
for (int i = fromIndex; i < data.length; i++) {
if (data[i] == key) {
return i;
}
}
return data.length;
}
private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
int terminationPos = indexOfZeroByte(data, fromIndex);
private static int indexOfEOS(byte[] data, int fromIndex, int encodingByte) {
int terminationPos = indexOf(data, fromIndex, (byte) 0);
// For single byte encoding charsets, we are done
if (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) {
// 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, look for a two zero bytes
// Otherwise look for a second zero byte.
while (terminationPos < data.length - 1) {
if (data[terminationPos + 1] == (byte) 0) {
return terminationPos;
}
terminationPos = indexOf(data, terminationPos + 1, (byte) 0);
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;
@ -192,6 +158,110 @@ public final class Id3Parser implements MetadataParser<List<Id3Frame>> {
return id3Size;
}
private static TxxxFrame parseTxxxFrame(ParsableByteArray id3Data, int frameSize)
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);
int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
return new TxxxFrame(description, value);
}
private static PrivFrame parsePrivFrame(ParsableByteArray id3Data, int frameSize)
throws UnsupportedEncodingException {
byte[] data = new byte[frameSize];
id3Data.readBytes(data, 0, frameSize);
int ownerEndIndex = indexOfZeroByte(data, 0);
String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1");
int privateDataStartIndex = ownerEndIndex + 1;
byte[] privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length);
return new PrivFrame(owner, privateData);
}
private static GeobFrame parseGeobFrame(ParsableByteArray id3Data, int frameSize)
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");
int filenameStartIndex = mimeTypeEndIndex + 1;
int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding);
String filename = new String(data, filenameStartIndex, filenameEndIndex - filenameStartIndex,
charset);
int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
String description = new String(data, descriptionStartIndex,
descriptionEndIndex - descriptionStartIndex, charset);
int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
byte[] objectData = Arrays.copyOfRange(data, objectDataStartIndex, data.length);
return new GeobFrame(mimeType, filename, description, objectData);
}
private static ApicFrame parseApicFrame(ParsableByteArray id3Data, int frameSize)
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");
int pictureType = data[mimeTypeEndIndex + 1] & 0xFF;
int descriptionStartIndex = mimeTypeEndIndex + 2;
int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
String description = new String(data, descriptionStartIndex,
descriptionEndIndex - descriptionStartIndex, charset);
int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
byte[] pictureData = Arrays.copyOfRange(data, pictureDataStartIndex, data.length);
return new ApicFrame(mimeType, description, pictureType, pictureData);
}
private static TextInformationFrame parseTextInformationFrame(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 parseBinaryFrame(ParsableByteArray id3Data, int frameSize, String id) {
byte[] frame = new byte[frameSize];
id3Data.readBytes(frame, 0, frameSize);
return new BinaryFrame(id, frame);
}
/**
* Maps encoding byte from ID3v2 frame to a Charset.
* @param encodingByte The value of encoding byte from ID3v2 frame.

View file

@ -0,0 +1,30 @@
/*
* Copyright (C) 2014 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.exoplayer.metadata.id3;
/**
* Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame.
*/
public final class TextInformationFrame extends Id3Frame {
public final String description;
public TextInformationFrame(String id, String description) {
super(id);
this.description = description;
}
}