diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c6d94ca360..3ae194c223 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,8 @@ ([#4360](https://github.com/google/ExoPlayer/issues/4360)). * Add `PlayerView.isControllerVisible` ([#4385](https://github.com/google/ExoPlayer/issues/4385)). +* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using + CommentFrame to InternalFrame for frames with gapless metadata in MP4. ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 75d8b4cf2d..54d48350fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; +import com.google.android.exoplayer2.metadata.id3.InternalFrame; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,7 +40,8 @@ public final class GaplessInfoHolder { } }; - private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; + private static final String GAPLESS_DOMAIN = "com.apple.iTunes"; + private static final String GAPLESS_DESCRIPTION = "iTunSMPB"; private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); @@ -91,7 +93,15 @@ public final class GaplessInfoHolder { Metadata.Entry entry = metadata.get(i); if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - if (setFromComment(commentFrame.description, commentFrame.text)) { + if (GAPLESS_DESCRIPTION.equals(commentFrame.description) + && setFromComment(commentFrame.text)) { + return true; + } + } else if (entry instanceof InternalFrame) { + InternalFrame internalFrame = (InternalFrame) entry; + if (GAPLESS_DOMAIN.equals(internalFrame.domain) + && GAPLESS_DESCRIPTION.equals(internalFrame.description) + && setFromComment(internalFrame.text)) { return true; } } @@ -103,14 +113,10 @@ public final class GaplessInfoHolder { * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header * or MPEG 4 user data), if valid and non-zero. * - * @param name The comment's identifier. * @param data The comment's payload data. * @return Whether the holder was populated. */ - private boolean setFromComment(String name, String data) { - if (!GAPLESS_COMMENT_ID.equals(name)) { - return false; - } + private boolean setFromComment(String data) { Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); if (matcher.find()) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index fed1694925..991f765d0d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; +import com.google.android.exoplayer2.metadata.id3.InternalFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -293,14 +294,13 @@ import com.google.android.exoplayer2.util.Util; data.skipBytes(atomSize - 12); } } - if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) { - // We're only interested in iTunSMPB. + if (domain == null || name == null || dataAtomPosition == -1) { return null; } data.setPosition(dataAtomPosition); data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4) String value = data.readNullTerminatedString(dataAtomSize - 16); - return new CommentFrame(LANGUAGE_UNDEFINED, name, value); + return new InternalFrame(domain, name, value); } private static int parseUint8AttributeValue(ParsableByteArray data) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java new file mode 100644 index 0000000000..a828d80069 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 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.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** Internal ID3 frame that is intended for use by the player. */ +public final class InternalFrame extends Id3Frame { + + public static final String ID = "----"; + + public final String domain; + public final String description; + public final String text; + + public InternalFrame(String domain, String description, String text) { + super(ID); + this.domain = domain; + this.description = description; + this.text = text; + } + + /* package */ InternalFrame(Parcel in) { + super(ID); + domain = Assertions.checkNotNull(in.readString()); + description = Assertions.checkNotNull(in.readString()); + text = Assertions.checkNotNull(in.readString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + InternalFrame other = (InternalFrame) obj; + return Util.areEqual(description, other.description) + && Util.areEqual(domain, other.domain) + && Util.areEqual(text, other.text); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (domain != null ? domain.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return id + ": domain=" + domain + ", description=" + description; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(domain); + dest.writeString(text); + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public InternalFrame createFromParcel(Parcel in) { + return new InternalFrame(in); + } + + @Override + public InternalFrame[] newArray(int size) { + return new InternalFrame[size]; + } + }; +}