diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapFrame.java new file mode 100644 index 0000000000..0a032b3d88 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapFrame.java @@ -0,0 +1,125 @@ +/* + * 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; + +/** + * Chapter information "CHAP" ID3 frame. + */ +public final class ChapFrame 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; + public final String title; + public final String url; + public final ApicFrame image; + + public ChapFrame(String chapterId, int startTime, int endTime, int startOffset, int endOffset, + String title, String url, ApicFrame image) { + super(ID); + this.chapterId = chapterId; + this.startTime = startTime; + this.endTime = endTime; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.title = title; + this.url = url; + this.image = image; + } + + /* package */ ChapFrame(Parcel in) { + super(ID); + this.chapterId = in.readString(); + this.startTime = in.readInt(); + this.endTime = in.readInt(); + this.startOffset = in.readInt(); + this.endOffset = in.readInt(); + this.title = in.readString(); + this.url = in.readString(); + this.image = in.readParcelable(ApicFrame.class.getClassLoader()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ChapFrame other = (ChapFrame) obj; + return startTime == other.startTime + && endTime == other.endTime + && startOffset == other.startOffset + && endOffset == other.endOffset + && Util.areEqual(chapterId, other.chapterId) + && Util.areEqual(title, other.title) + && Util.areEqual(url, other.url) + && Util.areEqual(image, other.image); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (chapterId != null ? chapterId.hashCode() : 0); + result = 31 * result + startTime; + result = 31 * result + endTime; + result = 31 * result + startOffset; + result = 31 * result + endOffset; + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + (url != null ? url.hashCode() : 0); + result = 31 * result + (image != null ? image.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.writeString(title); + dest.writeString(url); + dest.writeString(title); + dest.writeParcelable(image, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ChapFrame createFromParcel(Parcel in) { + return new ChapFrame(in); + } + + @Override + public ChapFrame[] newArray(int size) { + return new ChapFrame[size]; + } + }; +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CtocFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CtocFrame.java new file mode 100644 index 0000000000..1511763682 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CtocFrame.java @@ -0,0 +1,102 @@ +/* + * 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 information "CTOC" ID3 frame. + */ +public final class CtocFrame 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 String title; + + public CtocFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children, String title) { + super(ID); + this.elementId = elementId; + this.isRoot = isRoot; + this.isOrdered = isOrdered; + this.children = children; + this.title = title; + } + + /* package */ CtocFrame(Parcel in) { + super(ID); + this.elementId = in.readString(); + this.isRoot = in.readByte() != 0; + this.isOrdered = in.readByte() != 0; + this.children = in.createStringArray(); + this.title = in.readString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CtocFrame other = (CtocFrame) obj; + return isRoot == other.isRoot + && isOrdered == other.isOrdered + && Util.areEqual(elementId, other.elementId) + && Util.areEqual(title, other.title) + && Arrays.equals(children, other.children); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (elementId != null ? elementId.hashCode() : 0); + result = 31 * result + (isRoot ? 1 : 0); + result = 31 * result + (isOrdered ? 1 : 0); + result = 31 * result + Arrays.hashCode(children); + result = 31 * result + (title != null ? title.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.writeString(title); + } + + public static final Creator CREATOR = new Creator() { + @Override + public CtocFrame createFromParcel(Parcel in) { + return new CtocFrame(in); + } + + @Override + public CtocFrame[] newArray(int size) { + return new CtocFrame[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..6a7cef4d50 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,7 @@ 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 +204,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(); @@ -296,6 +296,15 @@ public final class Id3Decoder implements MetadataDecoder { } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && (frameId3 == 'M' || majorVersion == 2)) { frame = decodeCommentFrame(id3Data, frameSize); + } else if (majorVersion == 2 ? (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X') + : (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X')) { + frame = decodeWxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') { + frame = decodeChapFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, + frameHeaderSize); + } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') { + frame = decodeCtocFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, + frameHeaderSize); } else { String id = majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) @@ -458,6 +467,115 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, description); } + private static WxxxFrame 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 = indexOfEos(data, urlStartIndex, encoding); + url = new String(data, urlStartIndex, urlEndIndex - urlStartIndex, charset); + } else { + url = ""; + } + + return new WxxxFrame(description, url); + } + + private static ChapFrame decodeChapFrame(ParsableByteArray id3Data, int frameSize, + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) + throws UnsupportedEncodingException { + byte[] frameBytes = new byte[frameSize]; + id3Data.readBytes(frameBytes, 0, frameSize - 1); + + ParsableByteArray chapterData = new ParsableByteArray(frameBytes); + + int chapterIdEndIndex = indexOfZeroByte(frameBytes, 0) + 1; + String chapterId = chapterData.readNullTerminatedString(chapterIdEndIndex); + + chapterData.setPosition(chapterIdEndIndex); + int startTime = chapterData.readInt(); + int endTime = chapterData.readInt(); + int startOffset = chapterData.readInt(); + int endOffset = chapterData.readInt(); + + String title = null; + String url = null; + ApicFrame image = null; + + while (chapterData.bytesLeft() >= frameHeaderSize) { + Id3Frame frame = decodeFrame(majorVersion, chapterData, unsignedIntFrameSizeHack, + frameHeaderSize); + if (frame == null) { + continue; + } + if (frame instanceof TextInformationFrame) { + TextInformationFrame textFrame = (TextInformationFrame)frame; + if ("TIT2".equals(textFrame.id)) { + title = textFrame.description; + } + } + else if (frame instanceof WxxxFrame) { + WxxxFrame linkFrame = (WxxxFrame)frame; + url = linkFrame.url; + } + else if (frame instanceof ApicFrame) { + image = (ApicFrame)frame; + } + } + + return new ChapFrame(chapterId, startTime, endTime, startOffset, endOffset, title, url, image); + } + + private static CtocFrame decodeCtocFrame(ParsableByteArray id3Data, int frameSize, + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) + throws UnsupportedEncodingException { + byte[] frameBytes = new byte[frameSize]; + id3Data.readBytes(frameBytes, 0, frameSize - 1); + + ParsableByteArray tocData = new ParsableByteArray(frameBytes); + + int idEndIndex = indexOfZeroByte(frameBytes, 0) + 1; + String id = tocData.readNullTerminatedString(idEndIndex); + tocData.setPosition(idEndIndex); + + int flags = tocData.readUnsignedByte(); + boolean isRoot = (flags & 0x0002) != 0; + boolean isOrdered = (flags & 0x0001) != 0; + + int entryCount = tocData.readUnsignedByte(); + String[] children = new String[entryCount]; + for (int i = 0; i < entryCount; i++) { + int startIndex = tocData.getPosition(); + int endIndex = indexOfZeroByte(frameBytes, startIndex) + 1; + int stringLength = endIndex - startIndex; + String childId = tocData.readNullTerminatedString(stringLength); + children[i] = childId; + } + + String title = null; + while (tocData.bytesLeft() >= frameHeaderSize) { + Id3Frame frame = decodeFrame(majorVersion, tocData, unsignedIntFrameSizeHack, + frameHeaderSize); + if (frame instanceof TextInformationFrame) { + TextInformationFrame textFrame = (TextInformationFrame)frame; + if ("TIT2".equals(textFrame.id)) { + title = textFrame.description; + } + } + } + + return new CtocFrame(id, isRoot, isOrdered, children, title); + } + private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, String id) { byte[] frame = new byte[frameSize]; diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/WxxxFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/WxxxFrame.java new file mode 100644 index 0000000000..725e1d779a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/WxxxFrame.java @@ -0,0 +1,87 @@ +/* + * 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 android.os.Parcelable; + +import com.google.android.exoplayer2.util.Util; + +/** + * Url Frame "WXX" ID3 frame. + */ +public final class WxxxFrame extends Id3Frame { + + public static final String ID = "WXXX"; + + public final String description; + public final String url; + + public WxxxFrame(String description, String url) { + super(ID); + this.description = description; + this.url = url; + } + + /* package */ WxxxFrame(Parcel in) { + super(ID); + description = in.readString(); + url = in.readString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + WxxxFrame other = (WxxxFrame) obj; + return Util.areEqual(description, other.description) + && Util.areEqual(url, other.url); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (url != null ? url.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(description); + dest.writeString(url); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public WxxxFrame createFromParcel(Parcel in) { + return new WxxxFrame(in); + } + + @Override + public WxxxFrame[] newArray(int size) { + return new WxxxFrame[size]; + } + + }; + +}