From cb9a64da330e3c0cc5ac5ebc3f03a6bed089bcac Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 5 Jun 2016 06:23:41 -0700 Subject: [PATCH] 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 --- .../exoplayer/demo/PlayerActivity.java | 10 + .../exoplayer/metadata/id3/Id3ParserTest.java | 52 +++-- .../exoplayer/extractor/ts/Id3Reader.java | 5 +- .../exoplayer/metadata/MetadataParser.java | 6 +- .../exoplayer/metadata/id3/ApicFrame.java | 38 ++++ .../exoplayer/metadata/id3/Id3Parser.java | 212 ++++++++++++------ .../metadata/id3/TextInformationFrame.java | 30 +++ 7 files changed, 263 insertions(+), 90 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/id3/ApicFrame.java create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/id3/TextInformationFrame.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 0423ffa698..8f0b6c5d89 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -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)); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer/metadata/id3/Id3ParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/metadata/id3/Id3ParserTest.java index 6946ba3b07..083ec2d939 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/metadata/id3/Id3ParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/metadata/id3/Id3ParserTest.java @@ -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 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 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 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 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); } } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java index a9c1406602..3f04cc3c26 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java @@ -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 diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java index 8b316a9a9c..94e043abfa 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer.metadata; -import java.io.IOException; +import com.google.android.exoplayer.ParserException; /** * Parses objects of type from binary data. @@ -38,8 +38,8 @@ public interface MetadataParser { * @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 . - * @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; } diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/id3/ApicFrame.java b/library/src/main/java/com/google/android/exoplayer/metadata/id3/ApicFrame.java new file mode 100644 index 0000000000..d279a2e189 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/id3/ApicFrame.java @@ -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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/id3/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/id3/Id3Parser.java index 9d5859b3ed..7998c8eb60 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/id3/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/id3/Id3Parser.java @@ -43,8 +43,7 @@ public final class Id3Parser implements MetadataParser> { } @Override - public List parse(byte[] data, int size) throws UnsupportedEncodingException, - ParserException { + public List parse(byte[] data, int size) throws ParserException { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); int id3Size = parseId3Header(id3Data); @@ -62,94 +61,61 @@ public final class Id3Parser implements MetadataParser> { // 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> { 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. diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/id3/TextInformationFrame.java b/library/src/main/java/com/google/android/exoplayer/metadata/id3/TextInformationFrame.java new file mode 100644 index 0000000000..3d47c5aa68 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/id3/TextInformationFrame.java @@ -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; + } + +}