Commit f2f10580 by Philip Simpson

Added ID3 chapter support.

parent deefe50a
/*
* 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.metadata.id3;
import android.os.Parcel;
import com.google.android.exoplayer2.util.Util;
/**
* Chapter information "CHAP" 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;
public final String title;
public final String url;
public final ApicFrame image;
public ChapterFrame(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 */ 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();
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;
}
ChapterFrame other = (ChapterFrame) obj;
return Util.areEqual(chapterId, other.chapterId)
&& startTime == other.startTime
&& endTime == other.endTime
&& startOffset == other.startOffset
&& endOffset == other.endOffset
&& title != null ? title.equals(other.title) : other.title == null
&& url != null ? url.equals(other.url) : other.url == null
&& image != null ? image.equals(other.image) : other.image == null;
}
@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(this.image, flags);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<ChapterFrame> CREATOR = new Creator<ChapterFrame>() {
@Override
public ChapterFrame createFromParcel(Parcel in) {
return new ChapterFrame(in);
}
@Override
public ChapterFrame[] newArray(int size) {
return new ChapterFrame[size];
}
};
}
/*
* 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.metadata.id3;
import android.os.Parcel;
import java.util.Arrays;
/**
* Chapter table of contents information "CTOC" ID3 frame.
*/
public 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 String title;
public ChapterTOCFrame(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 */ ChapterTOCFrame(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;
}
ChapterTOCFrame other = (ChapterTOCFrame) obj;
return elementId != null ? elementId.equals(other.elementId) : other.elementId == null
&& isRoot == other.isRoot
&& isOrdered == other.isOrdered
&& Arrays.equals(children, other.children)
&& title != null ? title.equals(other.title) : other.title == null;
}
@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(this.elementId);
dest.writeByte((byte)(this.isRoot ? 1 : 0));
dest.writeByte((byte)(this.isOrdered ? 1 : 0));
dest.writeStringArray(this.children);
dest.writeString(this.title);
}
public static final Creator<ChapterTOCFrame> CREATOR = new Creator<ChapterTOCFrame>() {
@Override
public ChapterTOCFrame createFromParcel(Parcel in) {
return new ChapterTOCFrame(in);
}
@Override
public ChapterTOCFrame[] newArray(int size) {
return new ChapterTOCFrame[size];
}
};
}
...@@ -282,6 +282,12 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -282,6 +282,12 @@ public final class Id3Decoder implements MetadataDecoder {
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M'
&& (frameId3 == 'M' || majorVersion == 2)) { && (frameId3 == 'M' || majorVersion == 2)) {
frame = decodeCommentFrame(id3Data, frameSize); frame = decodeCommentFrame(id3Data, frameSize);
} else if (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
frame = decodeUrlLinkFrame(id3Data, frameSize);
} else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') {
frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack);
} else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') {
frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack);
} else { } else {
String id = majorVersion == 2 String id = majorVersion == 2
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
...@@ -444,6 +450,112 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -444,6 +450,112 @@ public final class Id3Decoder implements MetadataDecoder {
return new TextInformationFrame(id, description); return new TextInformationFrame(id, description);
} }
private static UrlLinkFrame decodeUrlLinkFrame(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 urlStartIndex = descriptionEndIndex + 1;
int urlEndIndex = indexOfEos(data, urlStartIndex, encoding);
int urlLength = urlEndIndex - urlStartIndex;
String url = new String(data, urlStartIndex, urlLength, charset);
return new UrlLinkFrame(description, url);
}
private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize,
int majorVersion, boolean unsignedIntFrameSizeHack) 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;
int frameHeaderSize = majorVersion == 2 ? 6 : 10;
while (chapterData.bytesLeft() >= frameHeaderSize) {
Id3Frame frame = decodeFrame(majorVersion, chapterData, unsignedIntFrameSizeHack);
if (frame == null) {
continue;
}
if (frame instanceof TextInformationFrame) {
TextInformationFrame textFrame = (TextInformationFrame)frame;
if (textFrame.id != null && textFrame.id.equals("TIT2")) {
title = textFrame.description;
}
}
else if (frame instanceof UrlLinkFrame) {
UrlLinkFrame linkFrame = (UrlLinkFrame)frame;
url = linkFrame.url;
}
else if (frame instanceof ApicFrame) {
image = (ApicFrame)frame;
}
}
return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, title, url, image);
}
private static ChapterTOCFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize,
int majorVersion, boolean unsignedIntFrameSizeHack) 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 && tocData.bytesLeft() > 0; 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;
int frameHeaderSize = majorVersion == 2 ? 6 : 10;
while (tocData.bytesLeft() >= frameHeaderSize) {
Id3Frame frame = decodeFrame(majorVersion, tocData, unsignedIntFrameSizeHack);
if (frame == null) {
continue;
}
if (frame instanceof TextInformationFrame) {
TextInformationFrame textFrame = (TextInformationFrame)frame;
if (textFrame.id != null && textFrame.id.equals("TIT2")) {
title = textFrame.description;
}
}
}
return new ChapterTOCFrame(id, isRoot, isOrdered, children, title);
}
private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,
String id) { String id) {
byte[] frame = new byte[frameSize]; byte[] frame = new byte[frameSize];
......
/*
* 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.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/**
* Url Frame "WXX" ID3 frame.
*/
public class UrlLinkFrame extends Id3Frame {
public static final String ID = "WXXX";
public final String description;
public final String url;
public UrlLinkFrame(String description, String url) {
super(ID);
this.description = description;
this.url = url;
}
/* package */ UrlLinkFrame(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;
}
UrlLinkFrame other = (UrlLinkFrame) 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<UrlLinkFrame> CREATOR =
new Parcelable.Creator<UrlLinkFrame>() {
@Override
public UrlLinkFrame createFromParcel(Parcel in) {
return new UrlLinkFrame(in);
}
@Override
public UrlLinkFrame[] newArray(int size) {
return new UrlLinkFrame[size];
}
};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment