From db5f81ecfd9310adf4daf0875662be4dcc3f7652 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Mar 2017 06:32:47 -0700 Subject: [PATCH] Allow disabling ID3 metadata parsing if not required Issue: #2553 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150184824 --- .../extractor/GaplessInfoHolder.java | 13 ++++ .../extractor/mp3/Mp3Extractor.java | 16 ++++- .../exoplayer2/metadata/id3/Id3Decoder.java | 59 +++++++++++++++---- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 7e2a1b4a23..75d8b4cf2d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor; 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 java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,6 +27,18 @@ import java.util.regex.Pattern; */ public final class GaplessInfoHolder { + /** + * A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed + * to {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback + * information are decoded. + */ + public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE = new FramePredicate() { + @Override + public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) { + return id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2); + } + }; + private static final String GAPLESS_COMMENT_ID = "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})"); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 00394f7912..b0faad71c0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -58,13 +58,18 @@ public final class Mp3Extractor implements Extractor { * Flags controlling the behavior of the extractor. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) public @interface Flags {} /** * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would * otherwise not be possible. */ public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + /** + * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not + * required. + */ + public static final int FLAG_DISABLE_ID3_METADATA = 2; /** * The maximum number of bytes to search when synchronizing, before giving up. @@ -178,7 +183,8 @@ public final class Mp3Extractor implements Extractor { trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); + gaplessInfoHolder.encoderPadding, null, null, 0, null, + (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); } return readSample(input); } @@ -311,7 +317,11 @@ public final class Mp3Extractor implements Extractor { byte[] id3Data = new byte[tagLength]; System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); - metadata = new Id3Decoder().decode(id3Data, tagLength); + // We need to parse enough ID3 metadata to retrieve any gapless playback information even + // if ID3 metadata parsing is disabled. + Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0 + ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null; + metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); if (metadata != null) { gaplessInfoHolder.setFromMetadata(metadata); } 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 d5f5b08370..cbe6c65030 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 @@ -34,6 +34,25 @@ import java.util.Locale; */ public final class Id3Decoder implements MetadataDecoder { + /** + * A predicate for determining whether individual frames should be decoded. + */ + public interface FramePredicate { + + /** + * Returns whether a frame with the specified parameters should be decoded. + * + * @param majorVersion The major version of the ID3 tag. + * @param id0 The first byte of the frame ID. + * @param id1 The second byte of the frame ID. + * @param id2 The third byte of the frame ID. + * @param id3 The fourth byte of the frame ID. + * @return Whether the frame should be decoded. + */ + boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3); + + } + private static final String TAG = "Id3Decoder"; /** @@ -50,6 +69,19 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + private final FramePredicate framePredicate; + + public Id3Decoder() { + this(null); + } + + /** + * @param framePredicate Determines which frames are decoded. May be null to decode all frames. + */ + public Id3Decoder(FramePredicate framePredicate) { + this.framePredicate = framePredicate; + } + @Override public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; @@ -94,7 +126,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, - frameHeaderSize); + frameHeaderSize, framePredicate); if (frame != null) { id3Frames.add(frame); } @@ -200,7 +232,7 @@ public final class Id3Decoder implements MetadataDecoder { } private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, - boolean unsignedIntFrameSizeHack, int frameHeaderSize) { + boolean unsignedIntFrameSizeHack, int frameHeaderSize, FramePredicate framePredicate) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); @@ -234,6 +266,13 @@ public final class Id3Decoder implements MetadataDecoder { return null; } + if (framePredicate != null + && !framePredicate.evaluate(majorVersion, frameId0, frameId1, frameId2, frameId3)) { + // Filtered by the predicate. + id3Data.setPosition(nextFramePosition); + return null; + } + // Frame flags. boolean isCompressed = false; boolean isEncrypted = false; @@ -302,10 +341,10 @@ public final class Id3Decoder implements MetadataDecoder { frame = decodeCommentFrame(id3Data, frameSize); } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') { frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') { frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); } else { String id = majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) @@ -513,8 +552,8 @@ public final class Id3Decoder implements MetadataDecoder { } private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) - throws UnsupportedEncodingException { + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, + FramePredicate framePredicate) throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition, @@ -536,7 +575,7 @@ public final class Id3Decoder implements MetadataDecoder { int limit = framePosition + frameSize; while (id3Data.getPosition() < limit) { Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); if (frame != null) { subFrames.add(frame); } @@ -548,8 +587,8 @@ public final class Id3Decoder implements MetadataDecoder { } private static ChapterTocFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) - throws UnsupportedEncodingException { + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, + FramePredicate framePredicate) throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition, @@ -573,7 +612,7 @@ public final class Id3Decoder implements MetadataDecoder { int limit = framePosition + frameSize; while (id3Data.getPosition() < limit) { Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); if (frame != null) { subFrames.add(frame); }