diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 9053f8990a..a84bb7bd36 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.metadata.id3.GeobFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; -import com.google.android.exoplayer2.metadata.id3.TxxxFrame; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; @@ -359,10 +358,10 @@ import java.util.Locale; private void printMetadata(Metadata metadata, String prefix) { for (int i = 0; i < metadata.length(); i++) { Metadata.Entry entry = metadata.get(i); - if (entry instanceof TxxxFrame) { - TxxxFrame txxxFrame = (TxxxFrame) entry; - Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id, - txxxFrame.description, txxxFrame.value)); + if (entry instanceof TextInformationFrame) { + TextInformationFrame textInformationFrame = (TextInformationFrame) entry; + Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id, + textInformationFrame.value)); } else if (entry instanceof PrivFrame) { PrivFrame privFrame = (PrivFrame) entry; Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); @@ -374,10 +373,6 @@ import java.util.Locale; ApicFrame apicFrame = (ApicFrame) entry; Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s", apicFrame.id, apicFrame.mimeType, apicFrame.description)); - } else if (entry instanceof TextInformationFrame) { - TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.d(TAG, prefix + String.format("%s: description=%s", textInformationFrame.id, - textInformationFrame.description)); } else if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id, diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java index c8c1b4ed93..e13afceb40 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java @@ -59,8 +59,8 @@ public final class FormatTest extends TestCase { DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); byte[] projectionData = new byte[] {1, 2, 3}; Metadata metadata = new Metadata( - new TextInformationFrame("id1", "description1"), - new TextInformationFrame("id2", "description2")); + new TextInformationFrame("id1", "description1", "value1"), + new TextInformationFrame("id2", "description2", "value2")); Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100, diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java new file mode 100644 index 0000000000..182ae6f1c9 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 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.metadata.id3; + +import android.os.Parcel; +import junit.framework.TestCase; + +/** + * Test for {@link ChapterFrame}. + */ +public final class ChapterFrameTest extends TestCase { + + public void testParcelable() { + Id3Frame[] subFrames = new Id3Frame[] { + new TextInformationFrame("TIT2", null, "title"), + new UrlLinkFrame("WXXX", "description", "url") + }; + ChapterFrame chapterFrameToParcel = new ChapterFrame("id", 0, 1, 2, 3, subFrames); + + Parcel parcel = Parcel.obtain(); + chapterFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + ChapterFrame chapterFrameFromParcel = ChapterFrame.CREATOR.createFromParcel(parcel); + assertEquals(chapterFrameToParcel, chapterFrameFromParcel); + + parcel.recycle(); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrameTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrameTest.java new file mode 100644 index 0000000000..b0819ff427 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrameTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017 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.metadata.id3; + +import android.os.Parcel; +import junit.framework.TestCase; + +/** + * Test for {@link ChapterTOCFrame}. + */ +public final class ChapterTOCFrameTest extends TestCase { + + public void testParcelable() { + String[] children = new String[] {"child0", "child1"}; + Id3Frame[] subFrames = new Id3Frame[] { + new TextInformationFrame("TIT2", null, "title"), + new UrlLinkFrame("WXXX", "description", "url") + }; + ChapterTOCFrame chapterTOCFrameToParcel = new ChapterTOCFrame("id", false, true, children, + subFrames); + + Parcel parcel = Parcel.obtain(); + chapterTOCFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + ChapterTOCFrame chapterTOCFrameFromParcel = ChapterTOCFrame.CREATOR.createFromParcel(parcel); + assertEquals(chapterTOCFrameToParcel, chapterTOCFrameFromParcel); + + parcel.recycle(); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 20b026d670..e271108ce4 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -32,9 +32,10 @@ public final class Id3DecoderTest extends TestCase { Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); assertEquals(1, metadata.length()); - TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0); - assertEquals("", txxxFrame.description); - assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); + assertEquals("TXXX", textInformationFrame.id); + assertEquals("", textInformationFrame.description); + assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value); } public void testDecodeApicFrame() throws MetadataDecoderException { @@ -60,7 +61,8 @@ public final class Id3DecoderTest extends TestCase { assertEquals(1, metadata.length()); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertEquals("TIT2", textInformationFrame.id); - assertEquals("Hello World", textInformationFrame.description); + assertNull(textInformationFrame.description); + assertEquals("Hello World", textInformationFrame.value); } public void testDecodePrivFrame() throws MetadataDecoderException { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index e99dab053b..fed1694925 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -188,7 +188,7 @@ import com.google.android.exoplayer2.util.Util; if (atomType == Atom.TYPE_data) { data.skipBytes(8); // version (1), flags (3), empty (4) String value = data.readNullTerminatedString(atomSize - 16); - return new TextInformationFrame(id, value); + return new TextInformationFrame(id, null, value); } Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); return null; @@ -213,7 +213,7 @@ import com.google.android.exoplayer2.util.Util; value = Math.min(1, value); } if (value >= 0) { - return isTextInformationFrame ? new TextInformationFrame(id, Integer.toString(value)) + return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value)) : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value)); } Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); @@ -228,12 +228,12 @@ import com.google.android.exoplayer2.util.Util; data.skipBytes(10); // version (1), flags (3), empty (4), empty (2) int index = data.readUnsignedShort(); if (index > 0) { - String description = "" + index; + String value = "" + index; int count = data.readUnsignedShort(); if (count > 0) { - description += "/" + count; + value += "/" + count; } - return new TextInformationFrame(attributeName, description); + return new TextInformationFrame(attributeName, null, value); } } Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); @@ -245,7 +245,7 @@ import com.google.android.exoplayer2.util.Util; String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) ? STANDARD_GENRES[genreCode - 1] : null; if (genreString != null) { - return new TextInformationFrame("TCON", genreString); + return new TextInformationFrame("TCON", null, genreString); } Log.w(TAG, "Failed to parse standard genre code"); return null; diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java new file mode 100644 index 0000000000..22fd0d5fe4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2017 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.metadata.id3; + +import android.os.Parcel; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Chapter information ID3 frame. + */ +public final class ChapterFrame extends Id3Frame { + + public static final String ID = "CHAP"; + + public final String chapterId; + public final int startTime; + public final int endTime; + public final int startOffset; + public final int endOffset; + private final Id3Frame[] subFrames; + + public ChapterFrame(String chapterId, int startTime, int endTime, int startOffset, int endOffset, + Id3Frame[] subFrames) { + super(ID); + this.chapterId = chapterId; + this.startTime = startTime; + this.endTime = endTime; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.subFrames = subFrames; + } + + /* package */ ChapterFrame(Parcel in) { + super(ID); + this.chapterId = in.readString(); + this.startTime = in.readInt(); + this.endTime = in.readInt(); + this.startOffset = in.readInt(); + this.endOffset = in.readInt(); + int subFrameCount = in.readInt(); + subFrames = new Id3Frame[subFrameCount]; + for (int i = 0; i < subFrameCount; i++) { + subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader()); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ChapterFrame other = (ChapterFrame) obj; + return startTime == other.startTime + && endTime == other.endTime + && startOffset == other.startOffset + && endOffset == other.endOffset + && Util.areEqual(chapterId, other.chapterId) + && Arrays.equals(subFrames, other.subFrames); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + startTime; + result = 31 * result + endTime; + result = 31 * result + startOffset; + result = 31 * result + endOffset; + result = 31 * result + (chapterId != null ? chapterId.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(chapterId); + dest.writeInt(startTime); + dest.writeInt(endTime); + dest.writeInt(startOffset); + dest.writeInt(endOffset); + dest.writeInt(subFrames.length); + for (int i = 0; i < subFrames.length; i++) { + dest.writeParcelable(subFrames[i], 0); + } + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + + @Override + public ChapterFrame createFromParcel(Parcel in) { + return new ChapterFrame(in); + } + + @Override + public ChapterFrame[] newArray(int size) { + return new ChapterFrame[size]; + } + + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrame.java new file mode 100644 index 0000000000..6dfcf9f104 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrame.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2017 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.metadata.id3; + +import android.os.Parcel; + +import com.google.android.exoplayer2.util.Util; + +import java.util.Arrays; + +/** + * Chapter table of contents ID3 frame. + */ +public final class ChapterTOCFrame extends Id3Frame { + + public static final String ID = "CTOC"; + + public final String elementId; + public final boolean isRoot; + public final boolean isOrdered; + public final String[] children; + public final Id3Frame[] subFrames; + + public ChapterTOCFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children, + Id3Frame[] subFrames) { + super(ID); + this.elementId = elementId; + this.isRoot = isRoot; + this.isOrdered = isOrdered; + this.children = children; + this.subFrames = subFrames; + } + + /* package */ ChapterTOCFrame(Parcel in) { + super(ID); + this.elementId = in.readString(); + this.isRoot = in.readByte() != 0; + this.isOrdered = in.readByte() != 0; + this.children = in.createStringArray(); + int subFrameCount = in.readInt(); + subFrames = new Id3Frame[subFrameCount]; + for (int i = 0; i < subFrameCount; i++) { + subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader()); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ChapterTOCFrame other = (ChapterTOCFrame) obj; + return isRoot == other.isRoot + && isOrdered == other.isOrdered + && Util.areEqual(elementId, other.elementId) + && Arrays.equals(children, other.children) + && Arrays.equals(subFrames, other.subFrames); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (isRoot ? 1 : 0); + result = 31 * result + (isOrdered ? 1 : 0); + result = 31 * result + (elementId != null ? elementId.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(elementId); + dest.writeByte((byte) (isRoot ? 1 : 0)); + dest.writeByte((byte) (isOrdered ? 1 : 0)); + dest.writeStringArray(children); + dest.writeInt(subFrames.length); + for (int i = 0; i < subFrames.length; i++) { + dest.writeParcelable(subFrames[i], 0); + } + } + + public static final Creator CREATOR = new Creator() { + + @Override + public ChapterTOCFrame createFromParcel(Parcel in) { + return new ChapterTOCFrame(in); + } + + @Override + public ChapterTOCFrame[] newArray(int size) { + return new ChapterTOCFrame[size]; + } + + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 0316c6d986..3bcb4cfa08 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -98,7 +98,8 @@ public final class Id3Decoder implements MetadataDecoder { int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; while (id3Data.bytesLeft() >= frameHeaderSize) { - Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack); + Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack, + frameHeaderSize); if (frame != null) { id3Frames.add(frame); } @@ -204,7 +205,7 @@ public final class Id3Decoder implements MetadataDecoder { } private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, - boolean unsignedIntFrameSizeHack) { + boolean unsignedIntFrameSizeHack, int frameHeaderSize) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); @@ -280,6 +281,19 @@ public final class Id3Decoder implements MetadataDecoder { if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && (majorVersion == 2 || frameId3 == 'X')) { frame = decodeTxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'T') { + 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 == 'W' && frameId1 == 'X' && frameId2 == 'X' + && (majorVersion == 2 || frameId3 == 'X')) { + frame = decodeWxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'W') { + 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 = decodeUrlLinkFrame(id3Data, frameSize, id); } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { frame = decodePrivFrame(id3Data, frameSize); } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' @@ -288,14 +302,15 @@ public final class Id3Decoder implements MetadataDecoder { } 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 = 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' || majorVersion == 2)) { frame = decodeCommentFrame(id3Data, frameSize); + } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') { + frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, + frameHeaderSize); + } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') { + frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, + frameHeaderSize); } else { String id = majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) @@ -311,7 +326,7 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) + private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); @@ -331,7 +346,65 @@ public final class Id3Decoder implements MetadataDecoder { value = ""; } - return new TxxxFrame(description, value); + return new TextInformationFrame("TXXX", description, value); + } + + private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, + int frameSize, String id) throws UnsupportedEncodingException { + if (frameSize <= 1) { + // Frame is empty or contains only the text encoding byte. + return new TextInformationFrame(id, null, ""); + } + + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int valueEndIndex = indexOfEos(data, 0, encoding); + String value = new String(data, 0, valueEndIndex, charset); + + return new TextInformationFrame(id, null, value); + } + + private static UrlLinkFrame decodeWxxxFrame(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); + + String url; + int urlStartIndex = descriptionEndIndex + delimiterLength(encoding); + if (urlStartIndex < data.length) { + int urlEndIndex = indexOfZeroByte(data, 0); + url = new String(data, urlStartIndex, urlEndIndex - urlStartIndex, "ISO-8859-1"); + } else { + url = ""; + } + + return new UrlLinkFrame("WXXX", description, url); + } + + private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize, + String id) throws UnsupportedEncodingException { + if (frameSize == 0) { + // Frame is empty. + return new UrlLinkFrame(id, null, ""); + } + + byte[] data = new byte[frameSize]; + id3Data.readBytes(data, 0, frameSize); + + int urlEndIndex = indexOfZeroByte(data, 0); + String url = new String(data, 0, urlEndIndex, "ISO-8859-1"); + + return new UrlLinkFrame(id, null, url); } private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) @@ -439,23 +512,69 @@ public final class Id3Decoder implements MetadataDecoder { return new CommentFrame(language, description, text); } - private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, - int frameSize, String id) throws UnsupportedEncodingException { - if (frameSize <= 1) { - // Frame is empty or contains only the text encoding byte. - return new TextInformationFrame(id, ""); + private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize, + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) + throws UnsupportedEncodingException { + int framePosition = id3Data.getPosition(); + int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); + String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition, + "ISO-8859-1"); + id3Data.setPosition(chapterIdEndIndex + 1); + + int startTime = id3Data.readUnsignedByte(); + int endTime = id3Data.readUnsignedByte(); + int startOffset = id3Data.readUnsignedByte(); + int endOffset = id3Data.readUnsignedByte(); + + ArrayList subFrames = new ArrayList<>(); + int limit = framePosition + frameSize; + while (id3Data.getPosition() < limit) { + Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, + frameHeaderSize); + if (frame != null) { + subFrames.add(frame); + } } - int encoding = id3Data.readUnsignedByte(); - String charset = getCharsetName(encoding); + Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()]; + subFrames.toArray(subFrameArray); + return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray); + } - byte[] data = new byte[frameSize - 1]; - id3Data.readBytes(data, 0, frameSize - 1); + private static ChapterTOCFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize, + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) + throws UnsupportedEncodingException { + int framePosition = id3Data.getPosition(); + int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); + String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition, + "ISO-8859-1"); + id3Data.setPosition(elementIdEndIndex + 1); - int descriptionEndIndex = indexOfEos(data, 0, encoding); - String description = new String(data, 0, descriptionEndIndex, charset); + int ctocFlags = id3Data.readUnsignedByte(); + boolean isRoot = (ctocFlags & 0x0002) != 0; + boolean isOrdered = (ctocFlags & 0x0001) != 0; - return new TextInformationFrame(id, description); + int childCount = id3Data.readUnsignedByte(); + String[] children = new String[childCount]; + for (int i = 0; i < childCount; i++) { + int startIndex = id3Data.getPosition(); + int endIndex = indexOfZeroByte(id3Data.data, startIndex); + children[i] = new String(id3Data.data, startIndex, endIndex - startIndex, "ISO-8859-1"); + } + + ArrayList subFrames = new ArrayList<>(); + int limit = framePosition + frameSize; + while (id3Data.getPosition() < limit) { + Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, + frameHeaderSize); + if (frame != null) { + subFrames.add(frame); + } + } + + Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()]; + subFrames.toArray(subFrameArray); + return new ChapterTOCFrame(elementId, isRoot, isOrdered, children, subFrameArray); } private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, @@ -467,8 +586,8 @@ public final class Id3Decoder implements MetadataDecoder { } /** - * Performs in-place removal of unsynchronization for {@code length} bytes starting from - * {@link ParsableByteArray#getPosition()} + * 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. @@ -487,6 +606,7 @@ public final class Id3Decoder implements MetadataDecoder { /** * Maps encoding byte from ID3v2 frame to a Charset. + * * @param encodingByte The value of encoding byte from ID3v2 frame. * @return Charset name. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index b8c061fd0a..6c27b7f232 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -25,15 +25,18 @@ import com.google.android.exoplayer2.util.Util; public final class TextInformationFrame extends Id3Frame { public final String description; + public final String value; - public TextInformationFrame(String id, String description) { + public TextInformationFrame(String id, String description, String value) { super(id); this.description = description; + this.value = value; } /* package */ TextInformationFrame(Parcel in) { super(in.readString()); description = in.readString(); + value = in.readString(); } @Override @@ -45,7 +48,8 @@ public final class TextInformationFrame extends Id3Frame { return false; } TextInformationFrame other = (TextInformationFrame) obj; - return id.equals(other.id) && Util.areEqual(description, other.description); + return id.equals(other.id) && Util.areEqual(description, other.description) + && Util.areEqual(value, other.value); } @Override @@ -53,6 +57,7 @@ public final class TextInformationFrame extends Id3Frame { int result = 17; result = 31 * result + id.hashCode(); result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); return result; } @@ -60,6 +65,7 @@ public final class TextInformationFrame extends Id3Frame { public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); dest.writeString(description); + dest.writeString(value); } public static final Parcelable.Creator CREATOR = diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java similarity index 55% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java rename to library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index 5c24e70ef4..e3cc4baa38 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2017 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. @@ -17,28 +17,27 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; + import com.google.android.exoplayer2.util.Util; /** - * TXXX (User defined text information) ID3 frame. + * WXXX (User defined URL link) ID3 frame. */ -public final class TxxxFrame extends Id3Frame { - - public static final String ID = "TXXX"; +public final class UrlLinkFrame extends Id3Frame { public final String description; - public final String value; + public final String url; - public TxxxFrame(String description, String value) { - super(ID); + public UrlLinkFrame(String id, String description, String url) { + super(id); this.description = description; - this.value = value; + this.url = url; } - /* package */ TxxxFrame(Parcel in) { - super(ID); + /* package */ UrlLinkFrame(Parcel in) { + super(in.readString()); description = in.readString(); - value = in.readString(); + url = in.readString(); } @Override @@ -49,36 +48,40 @@ public final class TxxxFrame extends Id3Frame { if (obj == null || getClass() != obj.getClass()) { return false; } - TxxxFrame other = (TxxxFrame) obj; - return Util.areEqual(description, other.description) && Util.areEqual(value, other.value); + UrlLinkFrame other = (UrlLinkFrame) obj; + return id.equals(other.id) && Util.areEqual(description, other.description) + && Util.areEqual(url, other.url); } @Override public int hashCode() { int result = 17; + result = 31 * result + id.hashCode(); result = 31 * result + (description != null ? description.hashCode() : 0); - result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (url != null ? url.hashCode() : 0); return result; } @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); dest.writeString(description); - dest.writeString(value); + dest.writeString(url); } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { - @Override - public TxxxFrame createFromParcel(Parcel in) { - return new TxxxFrame(in); - } + @Override + public UrlLinkFrame createFromParcel(Parcel in) { + return new UrlLinkFrame(in); + } - @Override - public TxxxFrame[] newArray(int size) { - return new TxxxFrame[size]; - } + @Override + public UrlLinkFrame[] newArray(int size) { + return new UrlLinkFrame[size]; + } - }; + }; }