From 18ab96349eda2030ec295e92d5ffbe8ab3104e61 Mon Sep 17 00:00:00 2001 From: Alan Snyder Date: Fri, 2 Sep 2016 20:11:26 -0700 Subject: [PATCH 01/17] Support ID3/Apple metadata parsing in MP3 and MP4 files --- .../android/exoplayer2/demo/EventLogger.java | 19 +- .../metadata/id3/Id3DecoderTest.java | 10 +- .../com/google/android/exoplayer2/Format.java | 55 ++- .../android/exoplayer2/SimpleExoPlayer.java | 16 +- .../exoplayer2/extractor/GaplessInfo.java | 90 +++++ .../extractor/GaplessInfoHolder.java | 81 +--- .../exoplayer2/extractor/mp3/Id3Util.java | 242 ++---------- .../extractor/mp3/Mp3Extractor.java | 34 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 370 ++++++++++++++++-- .../extractor/mp4/Mp4Extractor.java | 22 +- .../android/exoplayer2/metadata/Metadata.java | 105 +++++ .../exoplayer2/metadata/MetadataBuilder.java | 42 ++ .../exoplayer2/metadata/id3/ApicFrame.java | 62 +++ .../exoplayer2/metadata/id3/BinaryFrame.java | 49 +++ .../exoplayer2/metadata/id3/CommentFrame.java | 83 ++++ .../exoplayer2/metadata/id3/GeobFrame.java | 63 +++ .../exoplayer2/metadata/id3/Id3Decoder.java | 273 +++++++++++-- .../exoplayer2/metadata/id3/Id3Frame.java | 14 +- .../exoplayer2/metadata/id3/PrivFrame.java | 52 +++ .../metadata/id3/TextInformationFrame.java | 47 +++ .../exoplayer2/metadata/id3/TxxxFrame.java | 52 +++ .../upstream/ContentDataSource.java | 24 +- .../exoplayer2/util/ParsableByteArray.java | 18 + 23 files changed, 1386 insertions(+), 437 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java 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 d52fc30cba..4cb0bf12bb 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 @@ -27,8 +27,11 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.ApicFrame; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.GeobFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.PrivFrame; @@ -54,7 +57,7 @@ import java.util.Locale; /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, - MappingTrackSelector.EventListener, MetadataRenderer.Output> { + MappingTrackSelector.EventListener, MetadataRenderer.Output { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -173,10 +176,11 @@ import java.util.Locale; Log.d(TAG, "]"); } - // MetadataRenderer.Output> + // MetadataRenderer.Output @Override - public void onMetadata(List id3Frames) { + public void onMetadata(Metadata metadata) { + List id3Frames = metadata.getFrames(); for (Id3Frame id3Frame : id3Frames) { if (id3Frame instanceof TxxxFrame) { TxxxFrame txxxFrame = (TxxxFrame) id3Frame; @@ -197,10 +201,19 @@ import java.util.Locale; TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame; Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id, textInformationFrame.description)); + } else if (id3Frame instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) id3Frame; + Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id, + commentFrame.language, commentFrame.text)); } else { Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id)); } } + GaplessInfo gaplessInfo = metadata.getGaplessInfo(); + if (gaplessInfo != null) { + Log.i(TAG, String.format("ID3 TimedMetadata encoder delay=%d padding=%d", + gaplessInfo.encoderDelay, gaplessInfo.encoderPadding)); + } } // AudioRendererEventListener 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 f9ec1ee92b..97ebc6dbbc 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.test.MoreAsserts; import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.metadata.Metadata; import java.util.List; import junit.framework.TestCase; @@ -30,7 +31,8 @@ public class Id3DecoderTest extends TestCase { 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0}; Id3Decoder decoder = new Id3Decoder(); - List id3Frames = decoder.decode(rawId3, rawId3.length); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + List id3Frames = metadata.getFrames(); assertEquals(1, id3Frames.size()); TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0); assertEquals("", txxxFrame.description); @@ -42,7 +44,8 @@ public class Id3DecoderTest extends TestCase { 3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; Id3Decoder decoder = new Id3Decoder(); - List id3Frames = decoder.decode(rawId3, rawId3.length); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + List id3Frames = metadata.getFrames(); assertEquals(1, id3Frames.size()); ApicFrame apicFrame = (ApicFrame) id3Frames.get(0); assertEquals("image/jpeg", apicFrame.mimeType); @@ -56,7 +59,8 @@ public class Id3DecoderTest extends TestCase { byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0, 3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}; Id3Decoder decoder = new Id3Decoder(); - List id3Frames = decoder.decode(rawId3, rawId3.length); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + List id3Frames = metadata.getFrames(); assertEquals(1, id3Frames.size()); TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0); assertEquals("TIT2", textInformationFrame.id); diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 9cfe019ef4..133569809b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -21,6 +21,8 @@ import android.media.MediaFormat; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; @@ -100,6 +102,11 @@ public final class Format implements Parcelable { * DRM initialization data if the stream is protected, or null otherwise. */ public final DrmInitData drmInitData; + /** + * Static metadata + */ + public final Metadata metadata; + // Video specific. @@ -196,7 +203,7 @@ public final class Format implements Parcelable { float frameRate, List initializationData) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, null); + NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, null, null); } public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, @@ -222,7 +229,7 @@ public final class Format implements Parcelable { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, - drmInitData); + drmInitData, null); } // Audio. @@ -233,7 +240,7 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, initializationData, - null); + null, null); } public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, @@ -260,7 +267,7 @@ public final class Format implements Parcelable { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, - initializationData, drmInitData); + initializationData, drmInitData, null); } // Text. @@ -269,7 +276,7 @@ public final class Format implements Parcelable { String sampleMimeType, String codecs, int bitrate, int selectionFlags, String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, null, null); + NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, null, null, null); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, @@ -283,7 +290,7 @@ public final class Format implements Parcelable { long subsampleOffsetUs) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, selectionFlags, language, subsampleOffsetUs, null, drmInitData); + NO_VALUE, selectionFlags, language, subsampleOffsetUs, null, drmInitData, null); } // Image. @@ -292,7 +299,7 @@ public final class Format implements Parcelable { int bitrate, List initializationData, String language, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData); + NO_VALUE, 0, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null); } // Generic. @@ -301,14 +308,14 @@ public final class Format implements Parcelable { String sampleMimeType, int bitrate) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, null); + NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, null, null); } public static Format createSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, drmInitData); + NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null); } /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs, @@ -316,7 +323,7 @@ public final class Format implements Parcelable { float pixelWidthHeightRatio, byte[] projectionData, int stereoMode, int channelCount, int sampleRate, int pcmEncoding, int encoderDelay, int encoderPadding, int selectionFlags, String language, long subsampleOffsetUs, List initializationData, - DrmInitData drmInitData) { + DrmInitData drmInitData, Metadata metadata) { this.id = id; this.containerMimeType = containerMimeType; this.sampleMimeType = sampleMimeType; @@ -341,6 +348,7 @@ public final class Format implements Parcelable { this.initializationData = initializationData == null ? Collections.emptyList() : initializationData; this.drmInitData = drmInitData; + this.metadata = metadata; } /* package */ Format(Parcel in) { @@ -372,20 +380,21 @@ public final class Format implements Parcelable { initializationData.add(in.createByteArray()); } drmInitData = in.readParcelable(DrmInitData.class.getClassLoader()); + metadata = in.readParcelable(Metadata.class.getClassLoader()); } public Format copyWithMaxInputSize(int maxInputSize) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithContainerInfo(String id, int bitrate, int width, int height, @@ -393,7 +402,7 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithManifestFormatInfo(Format manifestFormat, @@ -409,21 +418,32 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, - language, subsampleOffsetUs, initializationData, drmInitData); + language, subsampleOffsetUs, initializationData, drmInitData, null); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithDrmInitData(DrmInitData drmInitData) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + } + + public Format copyWithMetadata(Metadata metadata) { + GaplessInfo gaplessInfo = metadata.getGaplessInfo(); + int ed = gaplessInfo != null ? gaplessInfo.encoderDelay : encoderDelay; + int ep = gaplessInfo != null ? gaplessInfo.encoderPadding : encoderPadding; + + return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, + width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, + stereoMode, channelCount, sampleRate, pcmEncoding, ed, ep, + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } /** @@ -483,6 +503,7 @@ public final class Format implements Parcelable { result = 31 * result + sampleRate; result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode()); + result = 31 * result + (metadata == null ? 0 : metadata.hashCode()); hashCode = result; } return hashCode; @@ -510,6 +531,7 @@ public final class Format implements Parcelable { || !Util.areEqual(sampleMimeType, other.sampleMimeType) || !Util.areEqual(codecs, other.codecs) || !Util.areEqual(drmInitData, other.drmInitData) + || !Util.areEqual(metadata, other.metadata) || !Arrays.equals(projectionData, other.projectionData) || initializationData.size() != other.initializationData.size()) { return false; @@ -582,6 +604,7 @@ public final class Format implements Parcelable { dest.writeByteArray(initializationData.get(i)); } dest.writeParcelable(drmInitData, 0); + dest.writeParcelable(metadata, 0); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 18f8fc3942..b58bb95438 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -31,9 +31,9 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; @@ -100,7 +100,7 @@ public final class SimpleExoPlayer implements ExoPlayer { private SurfaceHolder surfaceHolder; private TextRenderer.Output textOutput; - private MetadataRenderer.Output> id3Output; + private MetadataRenderer.Output id3Output; private VideoListener videoListener; private AudioRendererEventListener audioDebugListener; private VideoRendererEventListener videoDebugListener; @@ -345,7 +345,7 @@ public final class SimpleExoPlayer implements ExoPlayer { * * @param output The output. */ - public void setId3Output(MetadataRenderer.Output> output) { + public void setId3Output(MetadataRenderer.Output output) { id3Output = output; } @@ -484,7 +484,7 @@ public final class SimpleExoPlayer implements ExoPlayer { Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper()); renderersList.add(textRenderer); - MetadataRenderer> id3Renderer = new MetadataRenderer<>(componentListener, + MetadataRenderer id3Renderer = new MetadataRenderer<>(componentListener, mainHandler.getLooper(), new Id3Decoder()); renderersList.add(id3Renderer); } @@ -565,7 +565,7 @@ public final class SimpleExoPlayer implements ExoPlayer { } private final class ComponentListener implements VideoRendererEventListener, - AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output>, + AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, SurfaceHolder.Callback { // VideoRendererEventListener implementation @@ -696,12 +696,12 @@ public final class SimpleExoPlayer implements ExoPlayer { } } - // MetadataRenderer.Output> implementation + // MetadataRenderer.Output implementation @Override - public void onMetadata(List id3Frames) { + public void onMetadata(Metadata metadata) { if (id3Output != null) { - id3Output.onMetadata(id3Frames); + id3Output.onMetadata(metadata); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java new file mode 100644 index 0000000000..7335d9103f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java @@ -0,0 +1,90 @@ +/* + * 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.extractor; + +import android.util.Log; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Gapless playback information. + */ +public final class GaplessInfo { + + 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})"); + + /** + * The number of samples to trim from the start of the decoded audio stream. + */ + public final int encoderDelay; + + /** + * The number of samples to trim from the end of the decoded audio stream. + */ + public final int encoderPadding; + + /** + * Parses gapless playback information 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 the gapless playback info, or null if the provided data is not valid. + */ + public static GaplessInfo createFromComment(String name, String data) { + if(!GAPLESS_COMMENT_ID.equals(name)) { + return null; + } else { + Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); + if(matcher.find()) { + try { + int encoderDelay = Integer.parseInt(matcher.group(1), 16); + int encoderPadding = Integer.parseInt(matcher.group(2), 16); + if(encoderDelay > 0 || encoderPadding > 0) { + Log.d("ExoplayerImpl", "Parsed gapless info: " + encoderDelay + " " + encoderPadding); + return new GaplessInfo(encoderDelay, encoderPadding); + } + } catch (NumberFormatException var5) { + ; + } + } + + // Ignore incorrectly formatted comments. + Log.d("ExoplayerImpl", "Unable to parse gapless info: " + data); + return null; + } + } + + /** + * Parses gapless playback information from an MP3 Xing header, if valid and non-zero. + * + * @param value The 24-bit value to decode. + * @return the gapless playback info, or null if the provided data is not valid. + */ + public static GaplessInfo createFromXingHeaderValue(int value) { + int encoderDelay = value >> 12; + int encoderPadding = value & 0x0FFF; + return encoderDelay > 0 || encoderPadding > 0 ? + new GaplessInfo(encoderDelay, encoderPadding) : + null; + } + + public GaplessInfo(int encoderDelay, int encoderPadding) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + } +} 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 6eb9bc50de..4f98ce4f7e 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 @@ -15,90 +15,11 @@ */ package com.google.android.exoplayer2.extractor; -import com.google.android.exoplayer2.Format; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * Holder for gapless playback information. */ public final class GaplessInfoHolder { - 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})"); - - /** - * The number of samples to trim from the start of the decoded audio stream, or - * {@link Format#NO_VALUE} if not set. - */ - public int encoderDelay; - - /** - * The number of samples to trim from the end of the decoded audio stream, or - * {@link Format#NO_VALUE} if not set. - */ - public int encoderPadding; - - /** - * Creates a new holder for gapless playback information. - */ - public GaplessInfoHolder() { - encoderDelay = Format.NO_VALUE; - encoderPadding = Format.NO_VALUE; - } - - /** - * Populates the holder with data from an MP3 Xing header, if valid and non-zero. - * - * @param value The 24-bit value to decode. - * @return Whether the holder was populated. - */ - public boolean setFromXingHeaderValue(int value) { - int encoderDelay = value >> 12; - int encoderPadding = value & 0x0FFF; - if (encoderDelay > 0 || encoderPadding > 0) { - this.encoderDelay = encoderDelay; - this.encoderPadding = encoderPadding; - return true; - } - return false; - } - - /** - * 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. - */ - public boolean setFromComment(String name, String data) { - if (!GAPLESS_COMMENT_ID.equals(name)) { - return false; - } - Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); - if (matcher.find()) { - try { - int encoderDelay = Integer.parseInt(matcher.group(1), 16); - int encoderPadding = Integer.parseInt(matcher.group(2), 16); - if (encoderDelay > 0 || encoderPadding > 0) { - this.encoderDelay = encoderDelay; - this.encoderPadding = encoderPadding; - return true; - } - } catch (NumberFormatException e) { - // Ignore incorrectly formatted comments. - } - } - return false; - } - - /** - * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set. - */ - public boolean hasGaplessInfo() { - return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE; - } + public GaplessInfo gaplessInfo; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java index 53f18df844..af08514889 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.extractor.mp3; -import android.util.Pair; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.nio.charset.Charset; /** * Utility for parsing ID3 version 2 metadata in MP3 files. @@ -34,19 +34,18 @@ import java.nio.charset.Charset; private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024; private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - private static final Charset[] CHARSET_BY_ENCODING = new Charset[] {Charset.forName("ISO-8859-1"), - Charset.forName("UTF-16LE"), Charset.forName("UTF-16BE"), Charset.forName("UTF-8")}; /** - * Peeks data from the input and parses ID3 metadata. + * Peeks data from the input and parses ID3 metadata, including gapless playback information. * * @param input The {@link ExtractorInput} from which data should be peeked. - * @param out The {@link GaplessInfoHolder} to populate. + * @return The metadata, if present, {@code null} otherwise. * @throws IOException If an error occurred peeking from the input. * @throws InterruptedException If the thread was interrupted. */ - public static void parseId3(ExtractorInput input, GaplessInfoHolder out) + public static Metadata parseId3(ExtractorInput input) throws IOException, InterruptedException { + Metadata result = null; ParsableByteArray scratch = new ParsableByteArray(10); int peekedId3Bytes = 0; while (true) { @@ -60,18 +59,26 @@ import java.nio.charset.Charset; int minorVersion = scratch.readUnsignedByte(); int flags = scratch.readUnsignedByte(); int length = scratch.readSynchSafeInt(); - if (!out.hasGaplessInfo() && canParseMetadata(majorVersion, minorVersion, flags, length)) { - byte[] frame = new byte[length]; - input.peekFully(frame, 0, length); - parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags, out); - } else { - input.advancePeekPosition(length); + int frameLength = length + 10; + + try { + if (canParseMetadata(majorVersion, minorVersion, flags, length)) { + input.resetPeekPosition(); + byte[] frame = new byte[frameLength]; + input.peekFully(frame, 0, frameLength); + return new Id3Decoder().decode(frame, frameLength); + } else { + input.advancePeekPosition(length); + } + } catch (MetadataDecoderException e) { + e.printStackTrace(); } - peekedId3Bytes += 10 + length; + peekedId3Bytes += frameLength; } input.resetPeekPosition(); input.advancePeekPosition(peekedId3Bytes); + return result; } private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags, @@ -83,211 +90,6 @@ import java.nio.charset.Charset; && !(majorVersion == 4 && (flags & 0x0F) != 0); } - private static void parseGaplessInfo(ParsableByteArray frame, int version, int flags, - GaplessInfoHolder out) { - unescape(frame, version, flags); - - // Skip any extended header. - frame.setPosition(0); - if (version == 3 && (flags & 0x40) != 0) { - if (frame.bytesLeft() < 4) { - return; - } - int extendedHeaderSize = frame.readUnsignedIntToInt(); - if (extendedHeaderSize > frame.bytesLeft()) { - return; - } - int paddingSize; - if (extendedHeaderSize >= 6) { - frame.skipBytes(2); // extended flags - paddingSize = frame.readUnsignedIntToInt(); - frame.setPosition(4); - frame.setLimit(frame.limit() - paddingSize); - if (frame.bytesLeft() < extendedHeaderSize) { - return; - } - } - frame.skipBytes(extendedHeaderSize); - } else if (version == 4 && (flags & 0x40) != 0) { - if (frame.bytesLeft() < 4) { - return; - } - int extendedHeaderSize = frame.readSynchSafeInt(); - if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) { - return; - } - frame.setPosition(extendedHeaderSize); - } - - // Extract gapless playback metadata stored in comments. - Pair comment; - while ((comment = findNextComment(version, frame)) != null) { - if (comment.first.length() > 3) { - if (out.setFromComment(comment.first.substring(3), comment.second)) { - break; - } - } - } - } - - private static Pair findNextComment(int majorVersion, ParsableByteArray data) { - int frameSize; - while (true) { - if (majorVersion == 2) { - if (data.bytesLeft() < 6) { - return null; - } - String id = data.readString(3, Charset.forName("US-ASCII")); - if (id.equals("\0\0\0")) { - return null; - } - frameSize = data.readUnsignedInt24(); - if (frameSize == 0 || frameSize > data.bytesLeft()) { - return null; - } - if (id.equals("COM")) { - break; - } - } else /* major == 3 || major == 4 */ { - if (data.bytesLeft() < 10) { - return null; - } - String id = data.readString(4, Charset.forName("US-ASCII")); - if (id.equals("\0\0\0\0")) { - return null; - } - frameSize = majorVersion == 4 ? data.readSynchSafeInt() : data.readUnsignedIntToInt(); - if (frameSize == 0 || frameSize > data.bytesLeft() - 2) { - return null; - } - int flags = data.readUnsignedShort(); - boolean compressedOrEncrypted = (majorVersion == 4 && (flags & 0x0C) != 0) - || (majorVersion == 3 && (flags & 0xC0) != 0); - if (!compressedOrEncrypted && id.equals("COMM")) { - break; - } - } - data.skipBytes(frameSize); - } - - // The comment tag is at the reading position in data. - int encoding = data.readUnsignedByte(); - if (encoding < 0 || encoding >= CHARSET_BY_ENCODING.length) { - return null; - } - Charset charset = CHARSET_BY_ENCODING[encoding]; - String[] commentFields = data.readString(frameSize - 1, charset).split("\0"); - return commentFields.length == 2 ? Pair.create(commentFields[0], commentFields[1]) : null; - } - - private static boolean unescape(ParsableByteArray frame, int version, int flags) { - if (version != 4) { - if ((flags & 0x80) != 0) { - // Remove unsynchronization on ID3 version < 2.4.0. - byte[] bytes = frame.data; - int newLength = bytes.length; - for (int i = 0; i + 1 < newLength; i++) { - if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) { - System.arraycopy(bytes, i + 2, bytes, i + 1, newLength - i - 2); - newLength--; - } - } - frame.setLimit(newLength); - } - } else { - // Remove unsynchronization on ID3 version 2.4.0. - if (canUnescapeVersion4(frame, false)) { - unescapeVersion4(frame, false); - } else if (canUnescapeVersion4(frame, true)) { - unescapeVersion4(frame, true); - } else { - return false; - } - } - return true; - } - - private static boolean canUnescapeVersion4(ParsableByteArray frame, - boolean unsignedIntDataSizeHack) { - frame.setPosition(0); - while (frame.bytesLeft() >= 10) { - if (frame.readInt() == 0) { - return true; - } - long dataSize = frame.readUnsignedInt(); - if (!unsignedIntDataSizeHack) { - // Parse the data size as a syncsafe integer. - if ((dataSize & 0x808080L) != 0) { - return false; - } - dataSize = (dataSize & 0x7F) | (((dataSize >> 8) & 0x7F) << 7) - | (((dataSize >> 16) & 0x7F) << 14) | (((dataSize >> 24) & 0x7F) << 21); - } - if (dataSize > frame.bytesLeft() - 2) { - return false; - } - int flags = frame.readUnsignedShort(); - if ((flags & 1) != 0) { - if (frame.bytesLeft() < 4) { - return false; - } - } - frame.skipBytes((int) dataSize); - } - return true; - } - - private static void unescapeVersion4(ParsableByteArray frame, boolean unsignedIntDataSizeHack) { - frame.setPosition(0); - byte[] bytes = frame.data; - while (frame.bytesLeft() >= 10) { - if (frame.readInt() == 0) { - return; - } - int dataSize = - unsignedIntDataSizeHack ? frame.readUnsignedIntToInt() : frame.readSynchSafeInt(); - int flags = frame.readUnsignedShort(); - int previousFlags = flags; - if ((flags & 1) != 0) { - // Strip data length indicator. - int offset = frame.getPosition(); - System.arraycopy(bytes, offset + 4, bytes, offset, frame.bytesLeft() - 4); - dataSize -= 4; - flags &= ~1; - frame.setLimit(frame.limit() - 4); - } - if ((flags & 2) != 0) { - // Unescape 0xFF00 to 0xFF in the next dataSize bytes. - int readOffset = frame.getPosition() + 1; - int writeOffset = readOffset; - for (int i = 0; i + 1 < dataSize; i++) { - if ((bytes[readOffset - 1] & 0xFF) == 0xFF && bytes[readOffset] == 0) { - readOffset++; - dataSize--; - } - bytes[writeOffset++] = bytes[readOffset++]; - } - frame.setLimit(frame.limit() - (readOffset - writeOffset)); - System.arraycopy(bytes, readOffset, bytes, writeOffset, frame.bytesLeft() - readOffset); - flags &= ~2; - } - if (flags != previousFlags || unsignedIntDataSizeHack) { - int dataSizeOffset = frame.getPosition() - 6; - writeSyncSafeInteger(bytes, dataSizeOffset, dataSize); - bytes[dataSizeOffset + 4] = (byte) (flags >> 8); - bytes[dataSizeOffset + 5] = (byte) (flags & 0xFF); - } - frame.skipBytes(dataSize); - } - } - - private static void writeSyncSafeInteger(byte[] bytes, int offset, int value) { - bytes[offset] = (byte) ((value >> 21) & 0x7F); - bytes[offset + 1] = (byte) ((value >> 14) & 0x7F); - bytes[offset + 2] = (byte) ((value >> 7) & 0x7F); - bytes[offset + 3] = (byte) (value & 0x7F); - } - private Id3Util() {} } 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 fdd037cde3..2004c0de19 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 @@ -22,11 +22,12 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; @@ -69,7 +70,7 @@ public final class Mp3Extractor implements Extractor { private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; - private final GaplessInfoHolder gaplessInfoHolder; + private Metadata metadata; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -99,7 +100,6 @@ public final class Mp3Extractor implements Extractor { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(4); synchronizedHeader = new MpegAudioHeader(); - gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; } @@ -137,10 +137,21 @@ public final class Mp3Extractor implements Extractor { if (seeker == null) { seeker = setupSeeker(input); extractorOutput.seekMap(seeker); - 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)); + + GaplessInfo gaplessInfo = metadata != null ? metadata.getGaplessInfo() : null; + + Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, + synchronizedHeader.sampleRate, Format.NO_VALUE, + gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE, + gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE, + null, null, 0, null); + + if (metadata != null) { + format = format.copyWithMetadata(metadata); + } + + trackOutput.format(format); } return readSample(input); } @@ -220,7 +231,7 @@ public final class Mp3Extractor implements Extractor { int peekedId3Bytes = 0; input.resetPeekPosition(); if (input.getPosition() == 0) { - Id3Util.parseId3(input, gaplessInfoHolder); + metadata = Id3Util.parseId3(input); peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); @@ -303,13 +314,16 @@ public final class Mp3Extractor implements Extractor { Seeker seeker = null; if (headerData == XING_HEADER || headerData == INFO_HEADER) { seeker = XingSeeker.create(synchronizedHeader, frame, position, length); - if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { + if (seeker != null && metadata == null || metadata.getGaplessInfo() == null) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); input.advancePeekPosition(xingBase + 141); input.peekFully(scratch.data, 0, 3); scratch.setPosition(0); - gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); + GaplessInfo gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24()); + metadata = metadata != null ? + metadata.withGaplessInfo(gaplessInfo) : new Metadata(null, gaplessInfo); + } input.skipFully(synchronizedHeader.frameSize); } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 11a25fe419..cfa56ba3da 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -21,7 +21,15 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataBuilder; +import com.google.android.exoplayer2.metadata.id3.BinaryFrame; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; +import com.google.android.exoplayer2.metadata.id3.Id3Frame; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -270,7 +278,7 @@ import java.util.List; flags = rechunkedResults.flags; } - if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) { + if (track.editListDurations == null || gaplessInfoHolder.gaplessInfo != null) { // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // This implementation does not support applying both gapless metadata and an edit list. Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); @@ -299,10 +307,9 @@ import java.util.List; track.format.sampleRate, track.timescale); long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, track.format.sampleRate, track.timescale); - if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE + if ((encoderDelay > 0 || encoderPadding > 0) && encoderDelay <= Integer.MAX_VALUE && encoderPadding <= Integer.MAX_VALUE) { - gaplessInfoHolder.encoderDelay = (int) encoderDelay; - gaplessInfoHolder.encoderPadding = (int) encoderPadding; + gaplessInfoHolder.gaplessInfo = new GaplessInfo((int) encoderDelay, (int) encoderPadding); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } @@ -387,17 +394,17 @@ import java.util.List; } /** - * Parses a udta atom. + * Parses a udta atom for metadata, including gapless playback information. * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. - * @param out {@link GaplessInfoHolder} to populate with gapless playback information. + * @return metadata stored in the user data, or {@code null} if not present. */ - public static void parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, GaplessInfoHolder out) { + public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // decode one. - return; + return null; } ParsableByteArray udtaData = udtaAtom.data; udtaData.setPosition(Atom.HEADER_SIZE); @@ -407,14 +414,15 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - parseMetaAtom(udtaData, out); + parseMetaAtom(udtaData); break; } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } + return null; } - private static void parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) { + private static Metadata parseMetaAtom(ParsableByteArray data) { data.skipBytes(Atom.FULL_HEADER_SIZE); ParsableByteArray ilst = new ParsableByteArray(); while (data.bytesLeft() >= Atom.HEADER_SIZE) { @@ -423,47 +431,333 @@ import java.util.List; if (atomType == Atom.TYPE_ilst) { ilst.reset(data.data, data.getPosition() + payloadSize); ilst.setPosition(data.getPosition()); - parseIlst(ilst, out); - if (out.hasGaplessInfo()) { - return; + Metadata result = parseIlst(ilst); + if (result != null) { + return result; } } data.skipBytes(payloadSize); } + return null; } - private static void parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) { + private static Metadata parseIlst(ParsableByteArray ilst) { + + MetadataBuilder builder = new MetadataBuilder(); + while (ilst.bytesLeft() > 0) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); - if (type == Atom.TYPE_DASHES) { - String lastCommentMean = null; - String lastCommentName = null; - String lastCommentData = null; - while (ilst.getPosition() < endPosition) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_mean) { - lastCommentMean = ilst.readString(length); - } else if (key == Atom.TYPE_name) { - lastCommentName = ilst.readString(length); - } else if (key == Atom.TYPE_data) { - ilst.skipBytes(4); - lastCommentData = ilst.readString(length - 4); - } else { - ilst.skipBytes(length); + parseIlstElement(ilst, type, endPosition, builder); + ilst.setPosition(endPosition); + } + + return builder.build(); + } + + private static final String P1 = "\u00a9"; + private static final String P2 = "\ufffd"; + private static final int TYPE_NAME_1 = Util.getIntegerCodeForString(P1 + "nam"); + private static final int TYPE_NAME_2 = Util.getIntegerCodeForString(P2 + "nam"); + private static final int TYPE_NAME_3 = Util.getIntegerCodeForString(P1 + "trk"); + private static final int TYPE_NAME_4 = Util.getIntegerCodeForString(P2 + "trk"); + private static final int TYPE_COMMENT_1 = Util.getIntegerCodeForString(P1 + "cmt"); + private static final int TYPE_COMMENT_2 = Util.getIntegerCodeForString(P2 + "cmt"); + private static final int TYPE_YEAR_1 = Util.getIntegerCodeForString(P1 + "day"); + private static final int TYPE_YEAR_2 = Util.getIntegerCodeForString(P2 + "day"); + private static final int TYPE_ARTIST_1 = Util.getIntegerCodeForString(P1 + "ART"); + private static final int TYPE_ARTIST_2 = Util.getIntegerCodeForString(P2 + "ART"); + private static final int TYPE_ENCODER_1 = Util.getIntegerCodeForString(P1 + "too"); + private static final int TYPE_ENCODER_2 = Util.getIntegerCodeForString(P2 + "too"); + private static final int TYPE_ALBUM_1 = Util.getIntegerCodeForString(P1 + "alb"); + private static final int TYPE_ALBUM_2 = Util.getIntegerCodeForString(P2 + "alb"); + private static final int TYPE_COMPOSER_1 = Util.getIntegerCodeForString(P1 + "com"); + private static final int TYPE_COMPOSER_2 = Util.getIntegerCodeForString(P2 + "com"); + private static final int TYPE_COMPOSER_3 = Util.getIntegerCodeForString(P1 + "wrt"); + private static final int TYPE_COMPOSER_4 = Util.getIntegerCodeForString(P2 + "wrt"); + private static final int TYPE_LYRICS_1 = Util.getIntegerCodeForString(P1 + "lyr"); + private static final int TYPE_LYRICS_2 = Util.getIntegerCodeForString(P2 + "lyr"); + private static final int TYPE_GENRE_1 = Util.getIntegerCodeForString(P1 + "gen"); + private static final int TYPE_GENRE_2 = Util.getIntegerCodeForString(P2 + "gen"); + private static final int TYPE_STANDARD_GENRE = Util.getIntegerCodeForString("gnre"); + private static final int TYPE_GROUPING_1 = Util.getIntegerCodeForString(P1 + "grp"); + private static final int TYPE_GROUPING_2 = Util.getIntegerCodeForString(P2 + "grp"); + private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); + private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); + private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); + private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); + private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); + private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); + private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); + private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); + private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); + private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); + private static final int TYPE_SORT_SHOW = Util.getIntegerCodeForString("sosn"); + private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); + private static final int TYPE_SHOW = Util.getIntegerCodeForString("tvsh"); + + // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes + + private static void parseIlstElement( + ParsableByteArray ilst, int type, int endPosition, MetadataBuilder builder) { + if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { + parseTextAttribute(builder, "TIT2", ilst, endPosition); + } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { + parseCommentAttribute(builder, "COMM", ilst, endPosition); + } else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) { + parseTextAttribute(builder, "TDRC", ilst, endPosition); + } else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) { + parseTextAttribute(builder, "TPE1", ilst, endPosition); + } else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) { + parseTextAttribute(builder, "TSSE", ilst, endPosition); + } else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) { + parseTextAttribute(builder, "TALB", ilst, endPosition); + } else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 || + type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { + parseTextAttribute(builder, "TCOM", ilst, endPosition); + } else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) { + parseTextAttribute(builder, "lyrics", ilst, endPosition); + } else if (type == TYPE_STANDARD_GENRE) { + parseStandardGenreAttribute(builder, "TCON", ilst, endPosition); + } else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) { + parseTextAttribute(builder, "TCON", ilst, endPosition); + } else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) { + parseTextAttribute(builder, "TIT1", ilst, endPosition); + } else if (type == TYPE_DISK_NUMBER) { + parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition); + } else if (type == TYPE_TRACK_NUMBER) { + parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition); + } else if (type == TYPE_TEMPO) { + parseIntegerAttribute(builder, "TBPM", ilst, endPosition); + } else if (type == TYPE_COMPILATION) { + parseBooleanAttribute(builder, "TCMP", ilst, endPosition); + } else if (type == TYPE_ALBUM_ARTIST) { + parseTextAttribute(builder, "TPE2", ilst, endPosition); + } else if (type == TYPE_SORT_TRACK_NAME) { + parseTextAttribute(builder, "TSOT", ilst, endPosition); + } else if (type == TYPE_SORT_ALBUM) { + parseTextAttribute(builder, "TSO2", ilst, endPosition); + } else if (type == TYPE_SORT_ARTIST) { + parseTextAttribute(builder, "TSOA", ilst, endPosition); + } else if (type == TYPE_SORT_ALBUM_ARTIST) { + parseTextAttribute(builder, "TSOP", ilst, endPosition); + } else if (type == TYPE_SORT_COMPOSER) { + parseTextAttribute(builder, "TSOC", ilst, endPosition); + } else if (type == TYPE_SORT_SHOW) { + parseTextAttribute(builder, "sortShow", ilst, endPosition); + } else if (type == TYPE_GAPLESS_ALBUM) { + parseBooleanAttribute(builder, "gaplessAlbum", ilst, endPosition); + } else if (type == TYPE_SHOW) { + parseTextAttribute(builder, "show", ilst, endPosition); + } else if (type == Atom.TYPE_DASHES) { + parseExtendedAttribute(builder, ilst, endPosition); + } + } + + private static void parseTextAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + ilst.skipBytes(4); + String value = ilst.readNullTerminatedString(length - 4); + Id3Frame frame = new TextInformationFrame(attributeName, value); + builder.add(frame); + } else { + ilst.skipBytes(length); + } + } + + private static void parseCommentAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + ilst.skipBytes(4); + String value = ilst.readNullTerminatedString(length - 4); + Id3Frame frame = new CommentFrame("eng", attributeName, value); + builder.add(frame); + } else { + ilst.skipBytes(length); + } + } + + private static void parseBooleanAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + Object value = parseDataBox(ilst, length); + if (value instanceof Integer) { + int n = (Integer) value; + String s = n == 0 ? "0" : "1"; + Id3Frame frame = new TextInformationFrame(attributeName, s); + builder.add(frame); + } + } else { + ilst.skipBytes(length); + } + } + + private static void parseIntegerAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + Object value = parseDataBox(ilst, length); + if (value instanceof Integer) { + int n = (Integer) value; + String s = "" + n; + Id3Frame frame = new TextInformationFrame(attributeName, s); + builder.add(frame); + } + } else { + ilst.skipBytes(length); + } + } + + private static void parseIndexAndCountAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + Object value = parseDataBox(ilst, length); + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + if (bytes.length == 8) { + int index = (bytes[2] << 8) + (bytes[3] & 0xFF); + int count = (bytes[4] << 8) + (bytes[5] & 0xFF); + if (index > 0) { + String s = "" + index; + if (count > 0) { + s = s + "/" + count; + } + Id3Frame frame = new TextInformationFrame(attributeName, s); + builder.add(frame); } } - if (lastCommentName != null && lastCommentData != null - && "com.apple.iTunes".equals(lastCommentMean)) { - out.setFromComment(lastCommentName, lastCommentData); - break; - } - } else { - ilst.setPosition(endPosition); } + } else { + ilst.skipBytes(length); + } + } + + private static void parseStandardGenreAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + Object value = parseDataBox(ilst, length); + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + if (bytes.length == 2) { + int code = (bytes[0] << 8) + (bytes[1] & 0xFF); + String s = Id3Decoder.decodeGenre(code); + if (s != null) { + Id3Frame frame = new TextInformationFrame(attributeName, s); + builder.add(frame); + } + } + } + } else { + ilst.skipBytes(length); + } + } + + private static void parseExtendedAttribute(MetadataBuilder builder, + ParsableByteArray ilst, + int endPosition) { + String domain = null; + String name = null; + Object value = null; + + while (ilst.getPosition() < endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_mean) { + domain = ilst.readNullTerminatedString(length); + } else if (key == Atom.TYPE_name) { + name = ilst.readNullTerminatedString(length); + } else if (key == Atom.TYPE_data) { + value = parseDataBox(ilst, length); + } else { + ilst.skipBytes(length); + } + } + + if (value != null) { + if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunSMPB")) { + String s = value instanceof byte[] ? new String((byte[]) value) : value.toString(); + builder.setGaplessInfo(GaplessInfo.createFromComment("iTunSMPB", s)); + } + + if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunNORM") && (value instanceof byte[])) { + String s = new String((byte[]) value); + Id3Frame frame = new CommentFrame("eng", "iTunNORM", s); + builder.add(frame); + } else if (domain != null && name != null) { + String extendedName = domain + "." + name; + if (value instanceof String) { + Id3Frame frame = new TextInformationFrame(extendedName, (String) value); + builder.add(frame); + } else if (value instanceof Integer) { + Id3Frame frame = new TextInformationFrame(extendedName, value.toString()); + builder.add(frame); + } else if (value instanceof byte[]) { + byte[] bb = (byte[]) value; + Id3Frame frame = new BinaryFrame(extendedName, bb); + builder.add(frame); + } + } + } + } + + private static Object parseDataBox(ParsableByteArray ilst, int length) { + int versionAndFlags = ilst.readInt(); + int flags = versionAndFlags & 0xFFFFFF; + boolean isText = (flags == 1); + boolean isData = (flags == 0); + boolean isImageData = (flags == 0xD); + boolean isInteger = (flags == 21); + int dataLength = length - 4; + if (isText) { + return ilst.readNullTerminatedString(dataLength); + } else if (isInteger) { + if (dataLength == 1) { + return ilst.readUnsignedByte(); + } else if (dataLength == 2) { + return ilst.readUnsignedShort(); + } else { + ilst.skipBytes(dataLength); + return null; + } + } else if (isData) { + byte[] bytes = new byte[dataLength]; + ilst.readBytes(bytes, 0, dataLength); + return bytes; + } else { + ilst.skipBytes(dataLength); + return null; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 467ec7a4fa..1f4461f21e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -22,11 +22,13 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -309,11 +311,16 @@ public final class Mp4Extractor implements Extractor, SeekMap { long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; + GaplessInfo gaplessInfo = null; + Metadata metadata = null; - GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { - AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder); + Metadata info = AtomParsers.parseUdta(udta, isQuickTime); + if (info != null) { + gaplessInfo = info.getGaplessInfo(); + metadata = info; + } } for (int i = 0; i < moov.containerChildren.size(); i++) { @@ -330,7 +337,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); + GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); + gaplessInfoHolder.gaplessInfo = gaplessInfo; TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); + gaplessInfo = gaplessInfoHolder.gaplessInfo; if (trackSampleTable.sampleCount == 0) { continue; } @@ -340,9 +350,11 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); - if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) { - format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding); + if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfo != null) { + format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); + } + if (metadata != null) { + format = format.copyWithMetadata(metadata); } mp4Track.trackOutput.format(format); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java new file mode 100644 index 0000000000..c30e7ddb57 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -0,0 +1,105 @@ +/* + * 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; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.id3.Id3Frame; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * ID3 style metadata, with convenient access to gapless playback information. + */ +public class Metadata implements Parcelable { + + private final List frames; + private final GaplessInfo gaplessInfo; + + public Metadata(List frames, GaplessInfo gaplessInfo) { + List theFrames = frames != null ? new ArrayList<>(frames) : new ArrayList(); + this.frames = Collections.unmodifiableList(theFrames); + this.gaplessInfo = gaplessInfo; + } + + public Metadata(Parcel in) { + int encoderDelay = in.readInt(); + int encoderPadding = in.readInt(); + gaplessInfo = encoderDelay > 0 || encoderPadding > 0 ? + new GaplessInfo(encoderDelay, encoderPadding) : null; + frames = Arrays.asList((Id3Frame[]) in.readArray(Id3Frame.class.getClassLoader())); + } + + public Metadata withGaplessInfo(GaplessInfo info) { + return new Metadata(frames, info); + } + + public List getFrames() { + return frames; + } + + public GaplessInfo getGaplessInfo() { + return gaplessInfo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Metadata that = (Metadata) o; + + if (!frames.equals(that.frames)) return false; + return gaplessInfo != null ? gaplessInfo.equals(that.gaplessInfo) : that.gaplessInfo == null; + } + + @Override + public int hashCode() { + int result = frames.hashCode(); + result = 31 * result + (gaplessInfo != null ? gaplessInfo.hashCode() : 0); + return result; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderDelay : -1); + dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderPadding : -1); + dest.writeArray(frames.toArray(new Id3Frame[frames.size()])); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Metadata createFromParcel(Parcel in) { + return new Metadata(in); + } + + @Override + public Metadata[] newArray(int size) { + return new Metadata[0]; + } + }; +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java new file mode 100644 index 0000000000..57f49e5b20 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java @@ -0,0 +1,42 @@ +/* + * 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; + +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.id3.Id3Frame; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builder for ID3 style metadata. + */ +public class MetadataBuilder { + private List frames = new ArrayList<>(); + private GaplessInfo gaplessInfo; + + public void add(Id3Frame frame) { + frames.add(frame); + } + + public void setGaplessInfo(GaplessInfo info) { + this.gaplessInfo = info; + } + + public Metadata build() { + return !frames.isEmpty() || gaplessInfo != null ? new Metadata(frames, gaplessInfo): null; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index d2a04bdb94..9acb6840a7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; + /** * APIC (Attached Picture) ID3 frame. */ @@ -35,4 +39,62 @@ public final class ApicFrame extends Id3Frame { this.pictureData = pictureData; } + public ApicFrame(Parcel in) { + super(in); + mimeType = in.readString(); + description = in.readString(); + pictureType = in.readInt(); + pictureData = in.createByteArray(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ApicFrame that = (ApicFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (pictureType != that.pictureType) return false; + if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) + return false; + if (description != null ? !description.equals(that.description) : that.description != null) + return false; + return Arrays.equals(pictureData, that.pictureData); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + pictureType; + result = 31 * result + Arrays.hashCode(pictureData); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(mimeType); + dest.writeString(description); + dest.writeInt(pictureType); + dest.writeByteArray(pictureData); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public ApicFrame createFromParcel(Parcel in) { + return new ApicFrame(in); + } + + @Override + public ApicFrame[] newArray(int size) { + return new ApicFrame[size]; + } + + }; + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java index 5bc4ce3829..a07bdf5934 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; + /** * Binary ID3 frame. */ @@ -27,4 +31,49 @@ public final class BinaryFrame extends Id3Frame { this.data = data; } + public BinaryFrame(Parcel in) { + super(in); + data = in.createByteArray(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BinaryFrame that = (BinaryFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) + return false; + return Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeByteArray(data); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public BinaryFrame createFromParcel(Parcel in) { + return new BinaryFrame(in); + } + + @Override + public BinaryFrame[] newArray(int size) { + return new BinaryFrame[size]; + } + + }; + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java new file mode 100644 index 0000000000..53b3b8212a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -0,0 +1,83 @@ +/* + * 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; + +/** + * Comment ID3 frame. + */ +public final class CommentFrame extends Id3Frame { + + public final String language; + public final String text; + + public CommentFrame(String language, String description, String text) { + super(description); + this.language = language; + this.text = text; + } + + public CommentFrame(Parcel in) { + super(in); + language = in.readString(); + text = in.readString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CommentFrame that = (CommentFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (language != null ? !language.equals(that.language) : that.language != null) return false; + return text != null ? text.equals(that.text) : that.text == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (language != null ? language.hashCode() : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(language); + dest.writeString(text); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public CommentFrame createFromParcel(Parcel in) { + return new CommentFrame(in); + } + + @Override + public CommentFrame[] newArray(int size) { + return new CommentFrame[size]; + } + + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java index 4b77a69b27..5e4aa70b14 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; + /** * GEOB (General Encapsulated Object) ID3 frame. */ @@ -35,4 +39,63 @@ public final class GeobFrame extends Id3Frame { this.data = data; } + public GeobFrame(Parcel in) { + super(in); + mimeType = in.readString(); + filename = in.readString(); + description = in.readString(); + data = in.createByteArray(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GeobFrame that = (GeobFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) + return false; + if (filename != null ? !filename.equals(that.filename) : that.filename != null) + return false; + if (description != null ? !description.equals(that.description) : that.description != null) + return false; + return Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); + result = 31 * result + (filename != null ? filename.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(mimeType); + dest.writeString(filename); + dest.writeString(description); + dest.writeByteArray(data); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public GeobFrame createFromParcel(Parcel in) { + return new GeobFrame(in); + } + + @Override + public GeobFrame[] newArray(int size) { + return new GeobFrame[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 92c6efb530..833162ab77 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 @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.metadata.id3; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.util.MimeTypes; @@ -23,69 +25,141 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Locale; /** * Decodes individual TXXX text frames from raw ID3 data. */ -public final class Id3Decoder implements MetadataDecoder> { +public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + private int majorVersion; + private int minorVersion; + private boolean isUnsynchronized; + private GaplessInfo gaplessInfo; + @Override public boolean canDecode(String mimeType) { return mimeType.equals(MimeTypes.APPLICATION_ID3); } @Override - public List decode(byte[] data, int size) throws MetadataDecoderException { + public Metadata decode(byte[] data, int size) throws MetadataDecoderException { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); int id3Size = decodeId3Header(id3Data); + if (isUnsynchronized) { + id3Data = removeUnsynchronization(id3Data, id3Size); + id3Size = id3Data.bytesLeft(); + } + while (id3Size > 0) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); - int frameId3 = id3Data.readUnsignedByte(); - int frameSize = id3Data.readSynchSafeInt(); + int frameId3 = majorVersion > 2 ? id3Data.readUnsignedByte() : 0; + int frameSize = majorVersion == 2 ? id3Data.readUnsignedInt24() : + majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt(); + if (frameSize <= 1) { break; } - // Skip frame flags. - id3Data.skipBytes(2); + // Frame flags. + boolean isCompressed = false; + boolean isEncrypted = false; + boolean isUnsynchronized = false; + boolean hasGroupIdentifier = false; + boolean hasDataLength = false; - try { - Id3Frame frame; - if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { - frame = decodeTxxxFrame(id3Data, frameSize); - } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { - frame = decodePrivFrame(id3Data, frameSize); - } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { - frame = decodeGeobFrame(id3Data, frameSize); - } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { - frame = decodeApicFrame(id3Data, frameSize); - } else if (frameId0 == 'T') { - String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); - frame = decodeTextInformationFrame(id3Data, frameSize, id); + if (majorVersion > 2) { + int flags = id3Data.readShort(); + if (majorVersion == 3) { + isCompressed = (flags & 0x0080) != 0; + isEncrypted = (flags & 0x0040) != 0; + hasDataLength = isCompressed; } else { - String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); - frame = decodeBinaryFrame(id3Data, frameSize, id); + isCompressed = (flags & 0x0008) != 0; + isEncrypted = (flags & 0x0004) != 0; + isUnsynchronized = (flags & 0x0002) != 0; + hasGroupIdentifier = (flags & 0x0040) != 0; + hasDataLength = (flags & 0x0001) != 0; + } + } + + int headerSize = majorVersion == 2 ? 6 : 10; + + if (hasGroupIdentifier) { + ++headerSize; + --frameSize; + id3Data.skipBytes(1); + } + + if (isEncrypted) { + ++headerSize; + --frameSize; + id3Data.skipBytes(1); + } + + if (hasDataLength) { + headerSize += 4; + frameSize -= 4; + id3Data.skipBytes(4); + } + + id3Size -= frameSize + headerSize; + + if (isCompressed || isEncrypted) { + id3Data.skipBytes(frameSize); + } else { + try { + Id3Frame frame; + ParsableByteArray frameData = id3Data; + if (isUnsynchronized) { + frameData = removeUnsynchronization(id3Data, frameSize); + frameSize = frameData.bytesLeft(); + } + + if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { + frame = decodeTxxxFrame(frameData, frameSize); + } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { + frame = decodePrivFrame(frameData, frameSize); + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { + frame = decodeGeobFrame(frameData, frameSize); + } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { + frame = decodeApicFrame(frameData, frameSize); + } else if (frameId0 == 'T') { + String id = frameId3 != 0 ? + String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : + String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + frame = decodeTextInformationFrame(frameData, frameSize, id); + } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && + (frameId3 == 'M' || frameId3 == 0)) { + CommentFrame commentFrame = decodeCommentFrame(frameData, frameSize); + frame = commentFrame; + if (gaplessInfo == null) { + gaplessInfo = GaplessInfo.createFromComment(commentFrame.id, commentFrame.text); + } + } else { + String id = frameId3 != 0 ? + String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : + String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + frame = decodeBinaryFrame(frameData, frameSize, id); + } + id3Frames.add(frame); + } catch (UnsupportedEncodingException e) { + throw new MetadataDecoderException("Unsupported character encoding"); } - id3Frames.add(frame); - id3Size -= frameSize + 10 /* header size */; - } catch (UnsupportedEncodingException e) { - throw new MetadataDecoderException("Unsupported encoding", e); } } - return Collections.unmodifiableList(id3Frames); + return new Metadata(id3Frames, null); } private static int indexOfEos(byte[] data, int fromIndex, int encoding) { @@ -96,7 +170,7 @@ public final class Id3Decoder implements MetadataDecoder> { return terminationPos; } - // Otherwise look for a second zero byte. + // Otherwise ensure an even index and look for a second zero byte. while (terminationPos < data.length - 1) { if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { return terminationPos; @@ -126,7 +200,7 @@ public final class Id3Decoder implements MetadataDecoder> { * @return The size of ID3 frames in bytes, excluding the header and footer. * @throws ParserException If ID3 file identifier != "ID3". */ - private static int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { + private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { int id1 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte(); @@ -134,23 +208,41 @@ public final class Id3Decoder implements MetadataDecoder> { throw new MetadataDecoderException(String.format(Locale.US, "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); } - id3Buffer.skipBytes(2); // Skip version. + + majorVersion = id3Buffer.readUnsignedByte(); + minorVersion = id3Buffer.readUnsignedByte(); int flags = id3Buffer.readUnsignedByte(); int id3Size = id3Buffer.readSynchSafeInt(); - // Check if extended header presents. - if ((flags & 0x2) != 0) { - int extendedHeaderSize = id3Buffer.readSynchSafeInt(); - if (extendedHeaderSize > 4) { - id3Buffer.skipBytes(extendedHeaderSize - 4); - } - id3Size -= extendedHeaderSize; + if (majorVersion < 4) { + // this flag is advisory in version 4, use the frame flags instead + isUnsynchronized = (flags & 0x80) != 0; } - // Check if footer presents. - if ((flags & 0x8) != 0) { - id3Size -= 10; + if (majorVersion == 3) { + // check for extended header + if ((flags & 0x40) != 0) { + int extendedHeaderSize = id3Buffer.readInt(); // size excluding size field + if (extendedHeaderSize == 6 || extendedHeaderSize == 10) { + id3Buffer.skipBytes(extendedHeaderSize); + id3Size -= (extendedHeaderSize + 4); + } + } + } else if (majorVersion >= 4) { + // check for extended header + if ((flags & 0x40) != 0) { + int extendedHeaderSize = id3Buffer.readSynchSafeInt(); // size including size field + if (extendedHeaderSize > 4) { + id3Buffer.skipBytes(extendedHeaderSize - 4); + } + id3Size -= extendedHeaderSize; + } + + // Check if footer presents. + if ((flags & 0x10) != 0) { + id3Size -= 10; + } } return id3Size; @@ -253,6 +345,28 @@ public final class Id3Decoder implements MetadataDecoder> { return new TextInformationFrame(id, description); } + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, + int frameSize) throws UnsupportedEncodingException { + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[3]; + id3Data.readBytes(data, 0, 3); + String language = new String(data, 0, 3); + + data = new byte[frameSize - 4]; + id3Data.readBytes(data, 0, frameSize - 4); + + int descriptionEndIndex = indexOfEos(data, 0, encoding); + String description = new String(data, 0, descriptionEndIndex, charset); + + int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); + int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); + String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); + + return new CommentFrame(language, description, value); + } + private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, String id) { byte[] frame = new byte[frameSize]; @@ -261,6 +375,37 @@ public final class Id3Decoder implements MetadataDecoder> { return new BinaryFrame(id, frame); } + /** + * Undo the unsynchronization applied to one or more frames. + * @param dataSource The original data, positioned at the beginning of a frame. + * @param count The number of valid bytes in the frames to be processed. + * @return replacement data for the frames. + */ + private static ParsableByteArray removeUnsynchronization(ParsableByteArray dataSource, int count) { + byte[] source = dataSource.data; + int sourceIndex = dataSource.getPosition(); + int limit = sourceIndex + count; + byte[] dest = new byte[count]; + int destIndex = 0; + + while (sourceIndex < limit) { + byte b = source[sourceIndex++]; + if ((b & 0xFF) == 0xFF) { + int nextIndex = sourceIndex+1; + if (nextIndex < limit) { + int b2 = source[nextIndex]; + if (b2 == 0) { + // skip the 0 byte + ++sourceIndex; + } + } + } + dest[destIndex++] = b; + } + + return new ParsableByteArray(dest, destIndex); + } + /** * Maps encoding byte from ID3v2 frame to a Charset. * @param encodingByte The value of encoding byte from ID3v2 frame. @@ -281,4 +426,52 @@ public final class Id3Decoder implements MetadataDecoder> { } } + private final static String[] standardGenres = new String[] { + + // These are the official ID3v1 genres. + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", + "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", + "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", + "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", + "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", + "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", + "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", + "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", + "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", + "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", + "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", + "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", + "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", + "Hard Rock", + + // These were made up by the authors of Winamp but backported into the ID3 spec. + "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", + "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", + "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", + "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", + "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", + "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", + "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", + "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", + "Dance Hall", + + // These were also invented by the Winamp folks but ignored by the ID3 authors. + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", + "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", + "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", + "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", + "Synthpop" + }; + + public static String decodeGenre(int n) + { + n--; + + if (n < 0 || n >= standardGenres.length) { + return null; + } + + return standardGenres[n]; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index 903b32da4f..ea4776d784 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -15,10 +15,13 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; + /** * Base class for ID3 frames. */ -public abstract class Id3Frame { +public abstract class Id3Frame implements Parcelable { /** * The frame ID. @@ -29,4 +32,13 @@ public abstract class Id3Frame { this.id = id; } + protected Id3Frame(Parcel in) { + id = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java index bbfbd96b84..b0f9cb528f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; + /** * PRIV (Private) ID3 frame. */ @@ -31,4 +35,52 @@ public final class PrivFrame extends Id3Frame { this.privateData = privateData; } + public PrivFrame(Parcel in) { + super(in); + owner = in.readString(); + privateData = in.createByteArray(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PrivFrame that = (PrivFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (owner != null ? !owner.equals(that.owner) : that.owner != null) return false; + return Arrays.equals(privateData, that.privateData); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (owner != null ? owner.hashCode() : 0); + result = 31 * result + Arrays.hashCode(privateData); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(owner); + dest.writeByteArray(privateData); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public PrivFrame createFromParcel(Parcel in) { + return new PrivFrame(in); + } + + @Override + public PrivFrame[] newArray(int size) { + return new PrivFrame[size]; + } + + }; + } 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 ec05a8ff4b..3c6409ca7d 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 @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; + /** * Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame. */ @@ -27,4 +30,48 @@ public final class TextInformationFrame extends Id3Frame { this.description = description; } + public TextInformationFrame(Parcel in) { + super(in); + description = in.readString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TextInformationFrame that = (TextInformationFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + return description != null ? description.equals(that.description) : that.description == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (description != null ? description.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(description); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public TextInformationFrame createFromParcel(Parcel in) { + return new TextInformationFrame(in); + } + + @Override + public TextInformationFrame[] newArray(int size) { + return new TextInformationFrame[size]; + } + + }; + } 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/TxxxFrame.java index 6593c2f120..25ff1e063d 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/TxxxFrame.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; + /** * TXXX (User defined text information) ID3 frame. */ @@ -31,4 +34,53 @@ public final class TxxxFrame extends Id3Frame { this.value = value; } + public TxxxFrame(Parcel in) { + super(in); + description = in.readString(); + value = in.readString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TxxxFrame that = (TxxxFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (description != null ? !description.equals(that.description) : that.description != null) + return false; + return value != null ? value.equals(that.value) : that.value == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(description); + dest.writeString(value); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public TxxxFrame createFromParcel(Parcel in) { + return new TxxxFrame(in); + } + + @Override + public TxxxFrame[] newArray(int size) { + return new TxxxFrame[size]; + } + + }; + } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 2a0ad01489..06242216ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -136,22 +136,13 @@ public final class ContentDataSource implements DataSource { @Override public void close() throws ContentDataSourceException { uri = null; - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException e) { - throw new ContentDataSourceException(e); - } finally { - inputStream = null; + if (inputStream != null) { try { - if (assetFileDescriptor != null) { - assetFileDescriptor.close(); - } + inputStream.close(); } catch (IOException e) { throw new ContentDataSourceException(e); } finally { - assetFileDescriptor = null; + inputStream = null; if (opened) { opened = false; if (listener != null) { @@ -160,6 +151,13 @@ public final class ContentDataSource implements DataSource { } } } - } + if (assetFileDescriptor != null) { + try { + assetFileDescriptor.close(); + } catch (Exception e) { + } + assetFileDescriptor = null; + } + } } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index a499dc8012..5361288263 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -416,6 +416,24 @@ public final class ParsableByteArray { return readString(length, Charset.defaultCharset()); } + /** + * Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is ignored, + * if present. + * + * @param length The number of bytes to read. + * @return The string encoded by the bytes. + */ + public String readNullTerminatedString(int length) { + int stringLength = length; + int lastIndex = position + length - 1; + if (lastIndex < limit && data[lastIndex] == 0) { + stringLength--; + } + String result = new String(data, position, stringLength, Charset.defaultCharset()); + position += length; + return result; + } + /** * Reads the next {@code length} bytes as characters in the specified {@link Charset}. * From 776da107251e2e51f12d23986b7e83a2c39a2bdd Mon Sep 17 00:00:00 2001 From: Alan Snyder Date: Mon, 5 Sep 2016 15:39:40 -0700 Subject: [PATCH 02/17] Fix merge issue --- .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index cfa56ba3da..62dd11876f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -414,8 +414,7 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - parseMetaAtom(udtaData); - break; + return parseMetaAtom(udtaData); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } From 6a3b66987a064119981c88c268c10a9ef5a19d85 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 9 Oct 2016 14:52:38 +0100 Subject: [PATCH 03/17] Revert unrelated ContentDataSource change --- .../upstream/ContentDataSource.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 241296742a..f806f47410 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -142,13 +142,22 @@ public final class ContentDataSource implements DataSource { @Override public void close() throws ContentDataSourceException { uri = null; - if (inputStream != null) { - try { + try { + if (inputStream != null) { inputStream.close(); + } + } catch (IOException e) { + throw new ContentDataSourceException(e); + } finally { + inputStream = null; + try { + if (assetFileDescriptor != null) { + assetFileDescriptor.close(); + } } catch (IOException e) { throw new ContentDataSourceException(e); } finally { - inputStream = null; + assetFileDescriptor = null; if (opened) { opened = false; if (listener != null) { @@ -157,13 +166,6 @@ public final class ContentDataSource implements DataSource { } } } - - if (assetFileDescriptor != null) { - try { - assetFileDescriptor.close(); - } catch (Exception e) { - } - assetFileDescriptor = null; - } } + } From 3b34f850f25389d1a4fc240e883e872ede0308c0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 9 Oct 2016 14:58:12 +0100 Subject: [PATCH 04/17] Clean up ID3 frame implementations --- .../exoplayer2/metadata/id3/ApicFrame.java | 53 +++++++++---------- .../exoplayer2/metadata/id3/BinaryFrame.java | 27 +++++----- .../exoplayer2/metadata/id3/CommentFrame.java | 34 +++++++----- .../exoplayer2/metadata/id3/GeobFrame.java | 51 ++++++++---------- .../exoplayer2/metadata/id3/Id3Decoder.java | 12 ++--- .../exoplayer2/metadata/id3/Id3Frame.java | 8 +-- .../exoplayer2/metadata/id3/PrivFrame.java | 47 ++++++++-------- .../metadata/id3/TextInformationFrame.java | 25 +++++---- .../exoplayer2/metadata/id3/TxxxFrame.java | 46 ++++++++-------- 9 files changed, 150 insertions(+), 153 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index 9acb6840a7..c64be24a31 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; /** @@ -39,8 +40,8 @@ public final class ApicFrame extends Id3Frame { this.pictureData = pictureData; } - public ApicFrame(Parcel in) { - super(in); + /* package */ ApicFrame(Parcel in) { + super(ID); mimeType = in.readString(); description = in.readString(); pictureType = in.readInt(); @@ -48,53 +49,49 @@ public final class ApicFrame extends Id3Frame { } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ApicFrame that = (ApicFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (pictureType != that.pictureType) return false; - if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { return false; - if (description != null ? !description.equals(that.description) : that.description != null) - return false; - return Arrays.equals(pictureData, that.pictureData); + } + ApicFrame other = (ApicFrame) obj; + return pictureType == other.pictureType && Util.areEqual(mimeType, other.mimeType) + && Util.areEqual(description, other.description) + && Arrays.equals(pictureData, other.pictureData); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; + result = 31 * result + pictureType; result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); result = 31 * result + (description != null ? description.hashCode() : 0); - result = 31 * result + pictureType; result = 31 * result + Arrays.hashCode(pictureData); return result; } @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); dest.writeString(mimeType); dest.writeString(description); dest.writeInt(pictureType); dest.writeByteArray(pictureData); } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public ApicFrame createFromParcel(Parcel in) { - return new ApicFrame(in); - } + @Override + public ApicFrame createFromParcel(Parcel in) { + return new ApicFrame(in); + } - @Override - public ApicFrame[] newArray(int size) { - return new ApicFrame[size]; - } + @Override + public ApicFrame[] newArray(int size) { + return new ApicFrame[size]; + } - }; + }; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java index a07bdf5934..f662c1d06f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java @@ -26,31 +26,32 @@ public final class BinaryFrame extends Id3Frame { public final byte[] data; - public BinaryFrame(String type, byte[] data) { - super(type); + public BinaryFrame(String id, byte[] data) { + super(id); this.data = data; } - public BinaryFrame(Parcel in) { - super(in); + /* package */ BinaryFrame(Parcel in) { + super(in.readString()); data = in.createByteArray(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - BinaryFrame that = (BinaryFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { return false; - return Arrays.equals(data, that.data); + } + BinaryFrame other = (BinaryFrame) obj; + return id.equals(other.id) && Arrays.equals(data, other.data); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; + result = 31 * result + id.hashCode(); result = 31 * result + Arrays.hashCode(data); return result; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java index 53b3b8212a..b7cc937ac4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -17,43 +17,51 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; /** * Comment ID3 frame. */ public final class CommentFrame extends Id3Frame { + public static final String ID = "COMM"; + public final String language; + public final String description; public final String text; public CommentFrame(String language, String description, String text) { - super(description); + super(ID); this.language = language; + this.description = description; this.text = text; } - public CommentFrame(Parcel in) { - super(in); + /* package */ CommentFrame(Parcel in) { + super(ID); language = in.readString(); + description = in.readString(); text = in.readString(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - CommentFrame that = (CommentFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (language != null ? !language.equals(that.language) : that.language != null) return false; - return text != null ? text.equals(that.text) : that.text == null; + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CommentFrame other = (CommentFrame) obj; + return Util.areEqual(description, other.description) && Util.areEqual(language, other.language) + && Util.areEqual(text, other.text); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; result = 31 * result + (language != null ? language.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); result = 31 * result + (text != null ? text.hashCode() : 0); return result; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java index 5e4aa70b14..79e145fc7c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; /** @@ -39,8 +40,8 @@ public final class GeobFrame extends Id3Frame { this.data = data; } - public GeobFrame(Parcel in) { - super(in); + /* package */ GeobFrame(Parcel in) { + super(ID); mimeType = in.readString(); filename = in.readString(); description = in.readString(); @@ -48,25 +49,21 @@ public final class GeobFrame extends Id3Frame { } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - GeobFrame that = (GeobFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { return false; - if (filename != null ? !filename.equals(that.filename) : that.filename != null) - return false; - if (description != null ? !description.equals(that.description) : that.description != null) - return false; - return Arrays.equals(data, that.data); + } + GeobFrame other = (GeobFrame) obj; + return Util.areEqual(mimeType, other.mimeType) && Util.areEqual(filename, other.filename) + && Util.areEqual(description, other.description) && Arrays.equals(data, other.data); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); result = 31 * result + (filename != null ? filename.hashCode() : 0); result = 31 * result + (description != null ? description.hashCode() : 0); @@ -76,26 +73,24 @@ public final class GeobFrame extends Id3Frame { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); dest.writeString(mimeType); dest.writeString(filename); dest.writeString(description); dest.writeByteArray(data); } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public GeobFrame createFromParcel(Parcel in) { - return new GeobFrame(in); - } + @Override + public GeobFrame createFromParcel(Parcel in) { + return new GeobFrame(in); + } - @Override - public GeobFrame[] newArray(int size) { - return new GeobFrame[size]; - } + @Override + public GeobFrame[] newArray(int size) { + return new GeobFrame[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 833162ab77..2c234a6042 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 @@ -345,8 +345,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, description); } - private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, - int frameSize) throws UnsupportedEncodingException { + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); @@ -360,11 +360,11 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); - int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); - String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); + int textStartIndex = descriptionEndIndex + delimiterLength(encoding); + int textEndIndex = indexOfEos(data, textStartIndex, encoding); + String text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset); - return new CommentFrame(language, description, value); + return new CommentFrame(language, description, text); } private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index ea4776d784..41c4ae4e03 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; -import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Assertions; /** * Base class for ID3 frames. @@ -29,11 +29,7 @@ public abstract class Id3Frame implements Parcelable { public final String id; public Id3Frame(String id) { - this.id = id; - } - - protected Id3Frame(Parcel in) { - id = in.readString(); + this.id = Assertions.checkNotNull(id); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java index b0f9cb528f..fe55f5ddc0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; /** @@ -35,27 +36,27 @@ public final class PrivFrame extends Id3Frame { this.privateData = privateData; } - public PrivFrame(Parcel in) { - super(in); + /* package */ PrivFrame(Parcel in) { + super(ID); owner = in.readString(); privateData = in.createByteArray(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PrivFrame that = (PrivFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (owner != null ? !owner.equals(that.owner) : that.owner != null) return false; - return Arrays.equals(privateData, that.privateData); + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PrivFrame other = (PrivFrame) obj; + return Util.areEqual(owner, other.owner) && Arrays.equals(privateData, other.privateData); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; result = 31 * result + (owner != null ? owner.hashCode() : 0); result = 31 * result + Arrays.hashCode(privateData); return result; @@ -63,24 +64,22 @@ public final class PrivFrame extends Id3Frame { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); dest.writeString(owner); dest.writeByteArray(privateData); } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public PrivFrame createFromParcel(Parcel in) { - return new PrivFrame(in); - } + @Override + public PrivFrame createFromParcel(Parcel in) { + return new PrivFrame(in); + } - @Override - public PrivFrame[] newArray(int size) { - return new PrivFrame[size]; - } + @Override + public PrivFrame[] newArray(int size) { + return new PrivFrame[size]; + } - }; + }; } 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 3c6409ca7d..b8c061fd0a 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; /** * Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame. @@ -30,25 +31,27 @@ public final class TextInformationFrame extends Id3Frame { this.description = description; } - public TextInformationFrame(Parcel in) { - super(in); + /* package */ TextInformationFrame(Parcel in) { + super(in.readString()); description = in.readString(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - TextInformationFrame that = (TextInformationFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - return description != null ? description.equals(that.description) : that.description == null; + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + TextInformationFrame other = (TextInformationFrame) obj; + return id.equals(other.id) && Util.areEqual(description, other.description); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; + result = 31 * result + id.hashCode(); result = 31 * result + (description != null ? description.hashCode() : 0); return result; } 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/TxxxFrame.java index 25ff1e063d..5c24e70ef4 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/TxxxFrame.java @@ -17,6 +17,7 @@ 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. @@ -34,28 +35,27 @@ public final class TxxxFrame extends Id3Frame { this.value = value; } - public TxxxFrame(Parcel in) { - super(in); + /* package */ TxxxFrame(Parcel in) { + super(ID); description = in.readString(); value = in.readString(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - TxxxFrame that = (TxxxFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (description != null ? !description.equals(that.description) : that.description != null) + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { return false; - return value != null ? value.equals(that.value) : that.value == null; + } + TxxxFrame other = (TxxxFrame) obj; + return Util.areEqual(description, other.description) && Util.areEqual(value, other.value); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; result = 31 * result + (description != null ? description.hashCode() : 0); result = 31 * result + (value != null ? value.hashCode() : 0); return result; @@ -63,24 +63,22 @@ public final class TxxxFrame extends Id3Frame { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); dest.writeString(description); dest.writeString(value); } - 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 TxxxFrame createFromParcel(Parcel in) { + return new TxxxFrame(in); + } - @Override - public TxxxFrame[] newArray(int size) { - return new TxxxFrame[size]; - } + @Override + public TxxxFrame[] newArray(int size) { + return new TxxxFrame[size]; + } - }; + }; } From ba1da140c6e3bf6573688fc52c058f3bfe4b22b4 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 9 Oct 2016 16:27:58 +0100 Subject: [PATCH 05/17] Further modifications to ID3 support - Lots of misc cleanup - Remove GaplessInfo from Metadata. IMO it doesn't quite belong there, and means it ends up being represented twice inside Format. - Note: Changes untested, but will be tested in due course! --- .../android/exoplayer2/demo/EventLogger.java | 43 +++-- .../google/android/exoplayer2/FormatTest.java | 7 +- .../metadata/id3/Id3DecoderTest.java | 15 +- .../com/google/android/exoplayer2/Format.java | 13 +- .../android/exoplayer2/SimpleExoPlayer.java | 29 ++-- .../exoplayer2/extractor/GaplessInfo.java | 90 ----------- .../extractor/GaplessInfoHolder.java | 82 +++++++++- .../extractor/mp3/Mp3Extractor.java | 41 +++-- .../exoplayer2/extractor/mp4/AtomParsers.java | 152 ++++++++---------- .../extractor/mp4/Mp4Extractor.java | 19 +-- .../android/exoplayer2/metadata/Metadata.java | 117 ++++++++------ .../exoplayer2/metadata/MetadataBuilder.java | 42 ----- .../exoplayer2/metadata/MetadataDecoder.java | 6 +- .../exoplayer2/metadata/MetadataRenderer.java | 25 ++- .../exoplayer2/metadata/id3/Id3Decoder.java | 15 +- .../exoplayer2/metadata/id3/Id3Frame.java | 4 +- 16 files changed, 318 insertions(+), 382 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java delete mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java 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 f3c19da8d1..595f14e784 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 @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.ApicFrame; @@ -45,12 +44,10 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelections; -import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; import java.text.NumberFormat; -import java.util.List; import java.util.Locale; /** @@ -59,7 +56,7 @@ import java.util.Locale; /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, - MappingTrackSelector.EventListener, MetadataRenderer.Output { + MappingTrackSelector.EventListener, MetadataRenderer.Output { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -179,44 +176,40 @@ import java.util.Locale; Log.d(TAG, "]"); } - // MetadataRenderer.Output + // MetadataRenderer.Output @Override public void onMetadata(Metadata metadata) { - List id3Frames = metadata.getFrames(); - for (Id3Frame id3Frame : id3Frames) { - if (id3Frame instanceof TxxxFrame) { - TxxxFrame txxxFrame = (TxxxFrame) id3Frame; + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof TxxxFrame) { + TxxxFrame txxxFrame = (TxxxFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", txxxFrame.id, txxxFrame.description, txxxFrame.value)); - } else if (id3Frame instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) id3Frame; + } else if (entry instanceof PrivFrame) { + PrivFrame privFrame = (PrivFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner)); - } else if (id3Frame instanceof GeobFrame) { - GeobFrame geobFrame = (GeobFrame) id3Frame; + } else if (entry instanceof GeobFrame) { + GeobFrame geobFrame = (GeobFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s", geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); - } else if (id3Frame instanceof ApicFrame) { - ApicFrame apicFrame = (ApicFrame) id3Frame; + } else if (entry instanceof ApicFrame) { + ApicFrame apicFrame = (ApicFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s", apicFrame.id, apicFrame.mimeType, apicFrame.description)); - } else if (id3Frame instanceof TextInformationFrame) { - TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame; + } else if (entry instanceof TextInformationFrame) { + TextInformationFrame textInformationFrame = (TextInformationFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id, textInformationFrame.description)); - } else if (id3Frame instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) id3Frame; + } else if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id, commentFrame.language, commentFrame.text)); - } else { + } else if (entry instanceof Id3Frame) { + Id3Frame id3Frame = (Id3Frame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id)); } } - GaplessInfo gaplessInfo = metadata.getGaplessInfo(); - if (gaplessInfo != null) { - Log.i(TAG, String.format("ID3 TimedMetadata encoder delay=%d padding=%d", - gaplessInfo.encoderDelay, gaplessInfo.encoderPadding)); - } } // AudioRendererEventListener 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 9bdf330b02..1eb38de40f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java @@ -24,6 +24,8 @@ import android.annotation.TargetApi; import android.media.MediaFormat; import android.os.Parcel; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -56,11 +58,14 @@ public final class FormatTest extends TestCase { TestUtil.buildTestData(128, 1 /* data seed */)); 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")); 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, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, - drmInitData); + drmInitData, metadata); Parcel parcel = Parcel.obtain(); formatToParcel.writeToParcel(parcel, 0); 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 97ebc6dbbc..8ec966967e 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,8 @@ public class Id3DecoderTest extends TestCase { 54, 52, 95, 115, 116, 97, 114, 116, 0}; Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - List id3Frames = metadata.getFrames(); - assertEquals(1, id3Frames.size()); - TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0); + assertEquals(1, metadata.length()); + TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0); assertEquals("", txxxFrame.description); assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value); } @@ -45,9 +44,8 @@ public class Id3DecoderTest extends TestCase { 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - List id3Frames = metadata.getFrames(); - assertEquals(1, id3Frames.size()); - ApicFrame apicFrame = (ApicFrame) id3Frames.get(0); + assertEquals(1, metadata.length()); + ApicFrame apicFrame = (ApicFrame) metadata.get(0); assertEquals("image/jpeg", apicFrame.mimeType); assertEquals(16, apicFrame.pictureType); assertEquals("Hello World", apicFrame.description); @@ -60,9 +58,8 @@ public class Id3DecoderTest extends TestCase { 3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}; Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - List id3Frames = metadata.getFrames(); - assertEquals(1, id3Frames.size()); - TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0); + assertEquals(1, metadata.length()); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertEquals("TIT2", textInformationFrame.id); assertEquals("Hello World", textInformationFrame.description); } diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 078fbf98bd..65e797c8fe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -21,7 +21,6 @@ import android.media.MediaFormat; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -411,7 +410,7 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, - language, subsampleOffsetUs, initializationData, drmInitData, null); + language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { @@ -429,14 +428,10 @@ public final class Format implements Parcelable { } public Format copyWithMetadata(Metadata metadata) { - GaplessInfo gaplessInfo = metadata.getGaplessInfo(); - int ed = gaplessInfo != null ? gaplessInfo.encoderDelay : encoderDelay; - int ep = gaplessInfo != null ? gaplessInfo.encoderPadding : encoderPadding; - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, ed, ep, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, + stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 5f43971de8..4829b44d25 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -111,7 +111,7 @@ public final class SimpleExoPlayer implements ExoPlayer { private SurfaceHolder surfaceHolder; private TextureView textureView; private TextRenderer.Output textOutput; - private MetadataRenderer.Output id3Output; + private MetadataRenderer.Output metadataOutput; private VideoListener videoListener; private AudioRendererEventListener audioDebugListener; private VideoRendererEventListener videoDebugListener; @@ -389,12 +389,21 @@ public final class SimpleExoPlayer implements ExoPlayer { } /** - * Sets a listener to receive ID3 metadata events. + * @deprecated Use {@link #setMetadataOutput(MetadataRenderer.Output)} instead. + * @param output The output. + */ + @Deprecated + public void setId3Output(MetadataRenderer.Output output) { + setMetadataOutput(output); + } + + /** + * Sets a listener to receive metadata events. * * @param output The output. */ - public void setId3Output(MetadataRenderer.Output output) { - id3Output = output; + public void setMetadataOutput(MetadataRenderer.Output output) { + metadataOutput = output; } // ExoPlayer implementation @@ -539,9 +548,9 @@ public final class SimpleExoPlayer implements ExoPlayer { Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper()); renderersList.add(textRenderer); - MetadataRenderer id3Renderer = new MetadataRenderer<>(componentListener, + MetadataRenderer metadataRenderer = new MetadataRenderer(componentListener, mainHandler.getLooper(), new Id3Decoder()); - renderersList.add(id3Renderer); + renderersList.add(metadataRenderer); } private void buildExtensionRenderers(ArrayList renderersList, @@ -636,7 +645,7 @@ public final class SimpleExoPlayer implements ExoPlayer { } private final class ComponentListener implements VideoRendererEventListener, - AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, + AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, SurfaceHolder.Callback, TextureView.SurfaceTextureListener, TrackSelector.EventListener { @@ -768,12 +777,12 @@ public final class SimpleExoPlayer implements ExoPlayer { } } - // MetadataRenderer.Output implementation + // MetadataRenderer.Output implementation @Override public void onMetadata(Metadata metadata) { - if (id3Output != null) { - id3Output.onMetadata(metadata); + if (metadataOutput != null) { + metadataOutput.onMetadata(metadata); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java deleted file mode 100644 index 7335d9103f..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.extractor; - -import android.util.Log; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Gapless playback information. - */ -public final class GaplessInfo { - - 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})"); - - /** - * The number of samples to trim from the start of the decoded audio stream. - */ - public final int encoderDelay; - - /** - * The number of samples to trim from the end of the decoded audio stream. - */ - public final int encoderPadding; - - /** - * Parses gapless playback information 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 the gapless playback info, or null if the provided data is not valid. - */ - public static GaplessInfo createFromComment(String name, String data) { - if(!GAPLESS_COMMENT_ID.equals(name)) { - return null; - } else { - Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); - if(matcher.find()) { - try { - int encoderDelay = Integer.parseInt(matcher.group(1), 16); - int encoderPadding = Integer.parseInt(matcher.group(2), 16); - if(encoderDelay > 0 || encoderPadding > 0) { - Log.d("ExoplayerImpl", "Parsed gapless info: " + encoderDelay + " " + encoderPadding); - return new GaplessInfo(encoderDelay, encoderPadding); - } - } catch (NumberFormatException var5) { - ; - } - } - - // Ignore incorrectly formatted comments. - Log.d("ExoplayerImpl", "Unable to parse gapless info: " + data); - return null; - } - } - - /** - * Parses gapless playback information from an MP3 Xing header, if valid and non-zero. - * - * @param value The 24-bit value to decode. - * @return the gapless playback info, or null if the provided data is not valid. - */ - public static GaplessInfo createFromXingHeaderValue(int value) { - int encoderDelay = value >> 12; - int encoderPadding = value & 0x0FFF; - return encoderDelay > 0 || encoderPadding > 0 ? - new GaplessInfo(encoderDelay, encoderPadding) : - null; - } - - public GaplessInfo(int encoderDelay, int encoderPadding) { - this.encoderDelay = encoderDelay; - this.encoderPadding = encoderPadding; - } -} 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 4f98ce4f7e..72d2e1abdf 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 @@ -15,11 +15,91 @@ */ package com.google.android.exoplayer2.extractor; +import com.google.android.exoplayer2.Format; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Holder for gapless playback information. */ public final class GaplessInfoHolder { - public GaplessInfo gaplessInfo; + 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})"); + + /** + * The number of samples to trim from the start of the decoded audio stream, or + * {@link Format#NO_VALUE} if not set. + */ + public int encoderDelay; + + /** + * The number of samples to trim from the end of the decoded audio stream, or + * {@link Format#NO_VALUE} if not set. + */ + public int encoderPadding; + + /** + * Creates a new holder for gapless playback information. + */ + public GaplessInfoHolder() { + encoderDelay = Format.NO_VALUE; + encoderPadding = Format.NO_VALUE; + } + + /** + * Populates the holder with data from an MP3 Xing header, if valid and non-zero. + * + * @param value The 24-bit value to decode. + * @return Whether the holder was populated. + */ + public boolean setFromXingHeaderValue(int value) { + int encoderDelay = value >> 12; + int encoderPadding = value & 0x0FFF; + if (encoderDelay > 0 || encoderPadding > 0) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + return true; + } + return false; + } + + /** + * 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. + */ + public boolean setFromComment(String name, String data) { + if (!GAPLESS_COMMENT_ID.equals(name)) { + return false; + } + Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); + if (matcher.find()) { + try { + int encoderDelay = Integer.parseInt(matcher.group(1), 16); + int encoderPadding = Integer.parseInt(matcher.group(2), 16); + if (encoderDelay > 0 || encoderPadding > 0) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + return true; + } + } catch (NumberFormatException e) { + // Ignore incorrectly formatted comments. + } + } + return false; + } + + /** + * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set. + */ + public boolean hasGaplessInfo() { + return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE; + } } 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 00f8e27ad2..a107b11b2e 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 @@ -22,14 +22,18 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; + +import org.w3c.dom.Comment; + import java.io.EOFException; import java.io.IOException; @@ -70,7 +74,7 @@ public final class Mp3Extractor implements Extractor { private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; - private Metadata metadata; + private final GaplessInfoHolder gaplessInfoHolder; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -78,6 +82,7 @@ public final class Mp3Extractor implements Extractor { private int synchronizedHeaderData; + private Metadata metadata; private Seeker seeker; private long basisTimeUs; private long samplesRead; @@ -100,6 +105,7 @@ public final class Mp3Extractor implements Extractor { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(4); synchronizedHeader = new MpegAudioHeader(); + gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; } @@ -141,20 +147,13 @@ public final class Mp3Extractor implements Extractor { if (seeker == null) { seeker = setupSeeker(input); extractorOutput.seekMap(seeker); - - GaplessInfo gaplessInfo = metadata != null ? metadata.getGaplessInfo() : null; - Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, - Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, - synchronizedHeader.sampleRate, Format.NO_VALUE, - gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE, - gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE, - null, null, 0, null); - + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, + synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding, null, null, 0, null); if (metadata != null) { format = format.copyWithMetadata(metadata); } - trackOutput.format(format); } return readSample(input); @@ -211,6 +210,17 @@ public final class Mp3Extractor implements Extractor { input.resetPeekPosition(); if (input.getPosition() == 0) { metadata = Id3Util.parseId3(input); + if (!gaplessInfoHolder.hasGaplessInfo()) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; + if (gaplessInfoHolder.setFromComment(commentFrame.description, commentFrame.text)) { + break; + } + } + } + } peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); @@ -296,16 +306,13 @@ public final class Mp3Extractor implements Extractor { } if (headerData == XING_HEADER || headerData == INFO_HEADER) { seeker = XingSeeker.create(synchronizedHeader, frame, position, length); - if (seeker != null && metadata == null || metadata.getGaplessInfo() == null) { + if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); input.advancePeekPosition(xingBase + 141); input.peekFully(scratch.data, 0, 3); scratch.setPosition(0); - GaplessInfo gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24()); - metadata = metadata != null ? - metadata.withGaplessInfo(gaplessInfo) : new Metadata(null, gaplessInfo); - + gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); } input.skipFully(synchronizedHeader.frameSize); } else if (frame.limit() >= 40) { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 358c815098..05e20102fc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -22,10 +22,8 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataBuilder; import com.google.android.exoplayer2.metadata.id3.BinaryFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; @@ -38,6 +36,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.AvcConfig; import com.google.android.exoplayer2.video.HevcConfig; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -95,8 +95,8 @@ import java.util.List; Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, - stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, - stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); + stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, + stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); } /** @@ -286,7 +286,7 @@ import java.util.List; flags = rechunkedResults.flags; } - if (track.editListDurations == null || gaplessInfoHolder.gaplessInfo != null) { + if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) { // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // This implementation does not support applying both gapless metadata and an edit list. Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); @@ -315,9 +315,10 @@ import java.util.List; track.format.sampleRate, track.timescale); long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, track.format.sampleRate, track.timescale); - if ((encoderDelay > 0 || encoderPadding > 0) && encoderDelay <= Integer.MAX_VALUE + if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE && encoderPadding <= Integer.MAX_VALUE) { - gaplessInfoHolder.gaplessInfo = new GaplessInfo((int) encoderDelay, (int) encoderPadding); + gaplessInfoHolder.encoderDelay = (int) encoderDelay; + gaplessInfoHolder.encoderPadding = (int) encoderPadding; Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } @@ -402,13 +403,14 @@ import java.util.List; } /** - * Parses a udta atom for metadata, including gapless playback information. + * Parses a udta atom. * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. - * @return metadata stored in the user data, or {@code null} if not present. + * @param out {@link GaplessInfoHolder} to populate with gapless playback information. */ - public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { + public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, + GaplessInfoHolder out) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // decode one. @@ -422,14 +424,14 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - return parseMetaAtom(udtaData); + return parseMetaAtom(udtaData, out); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray data) { + private static Metadata parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) { data.skipBytes(Atom.FULL_HEADER_SIZE); ParsableByteArray ilst = new ParsableByteArray(); while (data.bytesLeft() >= Atom.HEADER_SIZE) { @@ -438,9 +440,9 @@ import java.util.List; if (atomType == Atom.TYPE_ilst) { ilst.reset(data.data, data.getPosition() + payloadSize); ilst.setPosition(data.getPosition()); - Metadata result = parseIlst(ilst); - if (result != null) { - return result; + Metadata metadata = parseIlst(ilst, out); + if (metadata != null) { + return metadata; } } data.skipBytes(payloadSize); @@ -448,19 +450,16 @@ import java.util.List; return null; } - private static Metadata parseIlst(ParsableByteArray ilst) { - - MetadataBuilder builder = new MetadataBuilder(); - + private static Metadata parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) { + ArrayList entries = new ArrayList<>(); while (ilst.bytesLeft() > 0) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); - parseIlstElement(ilst, type, endPosition, builder); + parseIlstElement(ilst, type, endPosition, entries, out); ilst.setPosition(endPosition); } - - return builder.build(); + return entries.isEmpty() ? null : new Metadata(entries); } private static final String P1 = "\u00a9"; @@ -506,66 +505,64 @@ import java.util.List; // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes - private static void parseIlstElement( - ParsableByteArray ilst, int type, int endPosition, MetadataBuilder builder) { + private static void parseIlstElement(ParsableByteArray ilst, int type, int endPosition, + List builder, GaplessInfoHolder out) { if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { - parseTextAttribute(builder, "TIT2", ilst, endPosition); + parseTextAttribute(builder, "TIT2", ilst); } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { - parseCommentAttribute(builder, "COMM", ilst, endPosition); + parseCommentAttribute(builder, "COMM", ilst); } else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) { - parseTextAttribute(builder, "TDRC", ilst, endPosition); + parseTextAttribute(builder, "TDRC", ilst); } else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) { - parseTextAttribute(builder, "TPE1", ilst, endPosition); + parseTextAttribute(builder, "TPE1", ilst); } else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) { - parseTextAttribute(builder, "TSSE", ilst, endPosition); + parseTextAttribute(builder, "TSSE", ilst); } else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) { - parseTextAttribute(builder, "TALB", ilst, endPosition); + parseTextAttribute(builder, "TALB", ilst); } else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 || - type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { - parseTextAttribute(builder, "TCOM", ilst, endPosition); + type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { + parseTextAttribute(builder, "TCOM", ilst); } else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) { - parseTextAttribute(builder, "lyrics", ilst, endPosition); + parseTextAttribute(builder, "lyrics", ilst); } else if (type == TYPE_STANDARD_GENRE) { - parseStandardGenreAttribute(builder, "TCON", ilst, endPosition); + parseStandardGenreAttribute(builder, "TCON", ilst); } else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) { - parseTextAttribute(builder, "TCON", ilst, endPosition); + parseTextAttribute(builder, "TCON", ilst); } else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) { - parseTextAttribute(builder, "TIT1", ilst, endPosition); + parseTextAttribute(builder, "TIT1", ilst); } else if (type == TYPE_DISK_NUMBER) { parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition); } else if (type == TYPE_TRACK_NUMBER) { parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition); } else if (type == TYPE_TEMPO) { - parseIntegerAttribute(builder, "TBPM", ilst, endPosition); + parseIntegerAttribute(builder, "TBPM", ilst); } else if (type == TYPE_COMPILATION) { - parseBooleanAttribute(builder, "TCMP", ilst, endPosition); + parseBooleanAttribute(builder, "TCMP", ilst); } else if (type == TYPE_ALBUM_ARTIST) { - parseTextAttribute(builder, "TPE2", ilst, endPosition); + parseTextAttribute(builder, "TPE2", ilst); } else if (type == TYPE_SORT_TRACK_NAME) { - parseTextAttribute(builder, "TSOT", ilst, endPosition); + parseTextAttribute(builder, "TSOT", ilst); } else if (type == TYPE_SORT_ALBUM) { - parseTextAttribute(builder, "TSO2", ilst, endPosition); + parseTextAttribute(builder, "TSO2", ilst); } else if (type == TYPE_SORT_ARTIST) { - parseTextAttribute(builder, "TSOA", ilst, endPosition); + parseTextAttribute(builder, "TSOA", ilst); } else if (type == TYPE_SORT_ALBUM_ARTIST) { - parseTextAttribute(builder, "TSOP", ilst, endPosition); + parseTextAttribute(builder, "TSOP", ilst); } else if (type == TYPE_SORT_COMPOSER) { - parseTextAttribute(builder, "TSOC", ilst, endPosition); + parseTextAttribute(builder, "TSOC", ilst); } else if (type == TYPE_SORT_SHOW) { - parseTextAttribute(builder, "sortShow", ilst, endPosition); + parseTextAttribute(builder, "sortShow", ilst); } else if (type == TYPE_GAPLESS_ALBUM) { - parseBooleanAttribute(builder, "gaplessAlbum", ilst, endPosition); + parseBooleanAttribute(builder, "gaplessAlbum", ilst); } else if (type == TYPE_SHOW) { - parseTextAttribute(builder, "show", ilst, endPosition); + parseTextAttribute(builder, "show", ilst); } else if (type == Atom.TYPE_DASHES) { - parseExtendedAttribute(builder, ilst, endPosition); + parseExtendedAttribute(builder, ilst, endPosition, out); } } - private static void parseTextAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseTextAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -579,10 +576,8 @@ import java.util.List; } } - private static void parseCommentAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseCommentAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -596,10 +591,8 @@ import java.util.List; } } - private static void parseBooleanAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseBooleanAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -616,10 +609,8 @@ import java.util.List; } } - private static void parseIntegerAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseIntegerAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -636,10 +627,8 @@ import java.util.List; } } - private static void parseIndexAndCountAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseIndexAndCountAttribute(List builder, + String attributeName, ParsableByteArray ilst, int endPosition) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -654,7 +643,7 @@ import java.util.List; String s = "" + index; if (count > 0) { s = s + "/" + count; - } + } Id3Frame frame = new TextInformationFrame(attributeName, s); builder.add(frame); } @@ -665,10 +654,8 @@ import java.util.List; } } - private static void parseStandardGenreAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseStandardGenreAttribute(List builder, + String attributeName, ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -690,9 +677,8 @@ import java.util.List; } } - private static void parseExtendedAttribute(MetadataBuilder builder, - ParsableByteArray ilst, - int endPosition) { + private static void parseExtendedAttribute(List builder, ParsableByteArray ilst, + int endPosition, GaplessInfoHolder out) { String domain = null; String name = null; Object value = null; @@ -713,9 +699,9 @@ import java.util.List; } if (value != null) { - if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunSMPB")) { + if (!out.hasGaplessInfo() && Util.areEqual(domain, "com.apple.iTunes")) { String s = value instanceof byte[] ? new String((byte[]) value) : value.toString(); - builder.setGaplessInfo(GaplessInfo.createFromComment("iTunSMPB", s)); + out.setFromComment(name, s); } if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunNORM") && (value instanceof byte[])) { @@ -889,12 +875,12 @@ import java.util.List; /** * Parses a stsd atom (defined in 14496-12). * - * @param stsd The stsd atom to decode. - * @param trackId The track's identifier in its container. + * @param stsd The stsd atom to decode. + * @param trackId The track's identifier in its container. * @param rotationDegrees The rotation of the track in degrees. - * @param language The language of the track. - * @param drmInitData {@link DrmInitData} to be included in the format. - * @param isQuickTime True for QuickTime media. False otherwise. + * @param language The language of the track. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @param isQuickTime True for QuickTime media. False otherwise. * @return An object containing the parsed data. */ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees, diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 1f4461f21e..303b4671cf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; @@ -311,16 +310,12 @@ public final class Mp4Extractor implements Extractor, SeekMap { long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; - GaplessInfo gaplessInfo = null; - Metadata metadata = null; + Metadata metadata = null; + GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { - Metadata info = AtomParsers.parseUdta(udta, isQuickTime); - if (info != null) { - gaplessInfo = info.getGaplessInfo(); - metadata = info; - } + metadata = AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder); } for (int i = 0; i < moov.containerChildren.size(); i++) { @@ -337,10 +332,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); - GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); - gaplessInfoHolder.gaplessInfo = gaplessInfo; TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); - gaplessInfo = gaplessInfoHolder.gaplessInfo; if (trackSampleTable.sampleCount == 0) { continue; } @@ -350,8 +342,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); - if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfo != null) { - format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); + if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) { + format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding); } if (metadata != null) { format = format.copyWithMetadata(metadata); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index c30e7ddb57..40c05a5602 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -17,65 +17,79 @@ package com.google.android.exoplayer2.metadata; import android.os.Parcel; import android.os.Parcelable; - -import com.google.android.exoplayer2.extractor.GaplessInfo; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; - -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; /** - * ID3 style metadata, with convenient access to gapless playback information. + * A collection of metadata entries. */ -public class Metadata implements Parcelable { +public final class Metadata implements Parcelable { - private final List frames; - private final GaplessInfo gaplessInfo; + /** + * A metadata entry. + */ + public interface Entry extends Parcelable {} - public Metadata(List frames, GaplessInfo gaplessInfo) { - List theFrames = frames != null ? new ArrayList<>(frames) : new ArrayList(); - this.frames = Collections.unmodifiableList(theFrames); - this.gaplessInfo = gaplessInfo; + private final Entry[] entries; + + /** + * @param entries The metadata entries. + */ + public Metadata(Entry... entries) { + this.entries = entries == null ? new Entry[0] : entries; } - public Metadata(Parcel in) { - int encoderDelay = in.readInt(); - int encoderPadding = in.readInt(); - gaplessInfo = encoderDelay > 0 || encoderPadding > 0 ? - new GaplessInfo(encoderDelay, encoderPadding) : null; - frames = Arrays.asList((Id3Frame[]) in.readArray(Id3Frame.class.getClassLoader())); + /** + * @param entries The metadata entries. + */ + public Metadata(List entries) { + if (entries != null) { + this.entries = new Entry[entries.size()]; + entries.toArray(this.entries); + } else { + this.entries = new Entry[0]; + } } - public Metadata withGaplessInfo(GaplessInfo info) { - return new Metadata(frames, info); + /* package */ Metadata(Parcel in) { + entries = new Metadata.Entry[in.readInt()]; + for (int i = 0; i < entries.length; i++) { + entries[i] = in.readParcelable(Entry.class.getClassLoader()); + } } - public List getFrames() { - return frames; + /** + * Returns the number of metadata entries. + */ + public int length() { + return entries.length; } - public GaplessInfo getGaplessInfo() { - return gaplessInfo; + /** + * Returns the entry at the specified index. + * + * @param index The index of the entry. + * @return The entry at the specified index. + */ + public Metadata.Entry get(int index) { + return entries[index]; } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Metadata that = (Metadata) o; - - if (!frames.equals(that.frames)) return false; - return gaplessInfo != null ? gaplessInfo.equals(that.gaplessInfo) : that.gaplessInfo == null; + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Metadata other = (Metadata) obj; + return Arrays.equals(entries, other.entries); } @Override public int hashCode() { - int result = frames.hashCode(); - result = 31 * result + (gaplessInfo != null ? gaplessInfo.hashCode() : 0); - return result; + return Arrays.hashCode(entries); } @Override @@ -85,21 +99,22 @@ public class Metadata implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderDelay : -1); - dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderPadding : -1); - dest.writeArray(frames.toArray(new Id3Frame[frames.size()])); + dest.writeInt(entries.length); + for (Entry entry : entries) { + dest.writeParcelable(entry, 0); + } } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - @Override - public Metadata createFromParcel(Parcel in) { - return new Metadata(in); - } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public Metadata createFromParcel(Parcel in) { + return new Metadata(in); + } + + @Override + public Metadata[] newArray(int size) { + return new Metadata[0]; + } + }; - @Override - public Metadata[] newArray(int size) { - return new Metadata[0]; - } - }; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java deleted file mode 100644 index 57f49e5b20..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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; - -import com.google.android.exoplayer2.extractor.GaplessInfo; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; - -import java.util.ArrayList; -import java.util.List; - -/** - * Builder for ID3 style metadata. - */ -public class MetadataBuilder { - private List frames = new ArrayList<>(); - private GaplessInfo gaplessInfo; - - public void add(Id3Frame frame) { - frames.add(frame); - } - - public void setGaplessInfo(GaplessInfo info) { - this.gaplessInfo = info; - } - - public Metadata build() { - return !frames.isEmpty() || gaplessInfo != null ? new Metadata(frames, gaplessInfo): null; - } -} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java index 7cde1f243d..a73311f16b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java @@ -17,10 +17,8 @@ package com.google.android.exoplayer2.metadata; /** * Decodes metadata from binary data. - * - * @param The type of the metadata. */ -public interface MetadataDecoder { +public interface MetadataDecoder { /** * Checks whether the decoder supports a given mime type. @@ -38,6 +36,6 @@ public interface MetadataDecoder { * @return The decoded metadata object. * @throws MetadataDecoderException If a problem occurred decoding the data. */ - T decode(byte[] data, int size) throws MetadataDecoderException; + Metadata decode(byte[] data, int size) throws MetadataDecoderException; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index aca38a1258..ff1364610b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -30,38 +30,34 @@ import java.nio.ByteBuffer; /** * A renderer for metadata. - * - * @param The type of the metadata. */ -public final class MetadataRenderer extends BaseRenderer implements Callback { +public final class MetadataRenderer extends BaseRenderer implements Callback { /** * Receives output from a {@link MetadataRenderer}. - * - * @param The type of the metadata. */ - public interface Output { + public interface Output { /** * Called each time there is a metadata associated with current playback time. * * @param metadata The metadata. */ - void onMetadata(T metadata); + void onMetadata(Metadata metadata); } private static final int MSG_INVOKE_RENDERER = 0; - private final MetadataDecoder metadataDecoder; - private final Output output; + private final MetadataDecoder metadataDecoder; + private final Output output; private final Handler outputHandler; private final FormatHolder formatHolder; private final DecoderInputBuffer buffer; private boolean inputStreamEnded; private long pendingMetadataTimestamp; - private T pendingMetadata; + private Metadata pendingMetadata; /** * @param output The output. @@ -72,8 +68,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback * called directly on the player's internal rendering thread. * @param metadataDecoder A decoder for the metadata. */ - public MetadataRenderer(Output output, Looper outputLooper, - MetadataDecoder metadataDecoder) { + public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) { super(C.TRACK_TYPE_METADATA); this.output = Assertions.checkNotNull(output); this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); @@ -137,7 +132,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback return true; } - private void invokeRenderer(T metadata) { + private void invokeRenderer(Metadata metadata) { if (outputHandler != null) { outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); } else { @@ -150,13 +145,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_INVOKE_RENDERER: - invokeRendererInternal((T) msg.obj); + invokeRendererInternal((Metadata) msg.obj); return true; } return false; } - private void invokeRendererInternal(T metadata) { + private void invokeRendererInternal(Metadata metadata) { output.onMetadata(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 2c234a6042..8887af8675 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 @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.metadata.id3; -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; @@ -31,7 +29,7 @@ import java.util.Locale; /** * Decodes individual TXXX text frames from raw ID3 data. */ -public final class Id3Decoder implements MetadataDecoder { +public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; @@ -41,7 +39,6 @@ public final class Id3Decoder implements MetadataDecoder { private int majorVersion; private int minorVersion; private boolean isUnsynchronized; - private GaplessInfo gaplessInfo; @Override public boolean canDecode(String mimeType) { @@ -141,11 +138,7 @@ public final class Id3Decoder implements MetadataDecoder { frame = decodeTextInformationFrame(frameData, frameSize, id); } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && (frameId3 == 'M' || frameId3 == 0)) { - CommentFrame commentFrame = decodeCommentFrame(frameData, frameSize); - frame = commentFrame; - if (gaplessInfo == null) { - gaplessInfo = GaplessInfo.createFromComment(commentFrame.id, commentFrame.text); - } + frame = decodeCommentFrame(frameData, frameSize); } else { String id = frameId3 != 0 ? String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : @@ -159,7 +152,7 @@ public final class Id3Decoder implements MetadataDecoder { } } - return new Metadata(id3Frames, null); + return new Metadata(id3Frames); } private static int indexOfEos(byte[] data, int fromIndex, int encoding) { @@ -198,7 +191,7 @@ public final class Id3Decoder implements MetadataDecoder { /** * @param id3Buffer A {@link ParsableByteArray} from which data should be read. * @return The size of ID3 frames in bytes, excluding the header and footer. - * @throws ParserException If ID3 file identifier != "ID3". + * @throws MetadataDecoderException If ID3 file identifier != "ID3". */ private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { int id1 = id3Buffer.readUnsignedByte(); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index 41c4ae4e03..dc41d2250c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -16,12 +16,14 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcelable; + +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; /** * Base class for ID3 frames. */ -public abstract class Id3Frame implements Parcelable { +public abstract class Id3Frame implements Metadata.Entry { /** * The frame ID. From 97e7fb85a7eeec52f343c8e8f2934f82ff90e10c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 9 Oct 2016 18:00:59 +0100 Subject: [PATCH 06/17] ID3: Clean up logging + only add to audio track for MP4 --- .../android/exoplayer2/demo/EventLogger.java | 18 ++++++++++-------- .../exoplayer2/extractor/mp4/Mp4Extractor.java | 14 ++++++++------ 2 files changed, 18 insertions(+), 14 deletions(-) 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 595f14e784..74c777c4ee 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 @@ -180,36 +180,38 @@ import java.util.Locale; @Override public void onMetadata(Metadata metadata) { + Log.i(TAG, "metadata ["); for (int i = 0; i < metadata.length(); i++) { Metadata.Entry entry = metadata.get(i); if (entry instanceof TxxxFrame) { TxxxFrame txxxFrame = (TxxxFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", txxxFrame.id, + Log.i(TAG, String.format(" %s: description=%s, value=%s", txxxFrame.id, txxxFrame.description, txxxFrame.value)); } else if (entry instanceof PrivFrame) { PrivFrame privFrame = (PrivFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner)); + Log.i(TAG, String.format(" %s: owner=%s", privFrame.id, privFrame.owner)); } else if (entry instanceof GeobFrame) { GeobFrame geobFrame = (GeobFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s", + Log.i(TAG, String.format(" %s: mimeType=%s, filename=%s, description=%s", geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); } else if (entry instanceof ApicFrame) { ApicFrame apicFrame = (ApicFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s", + Log.i(TAG, String.format(" %s: mimeType=%s, description=%s", apicFrame.id, apicFrame.mimeType, apicFrame.description)); } else if (entry instanceof TextInformationFrame) { TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id, + Log.i(TAG, String.format(" %s: description=%s", textInformationFrame.id, textInformationFrame.description)); } else if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id, - commentFrame.language, commentFrame.text)); + Log.i(TAG, String.format(" %s: language=%s description=%s", commentFrame.id, + commentFrame.language, commentFrame.description)); } else if (entry instanceof Id3Frame) { Id3Frame id3Frame = (Id3Frame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id)); + Log.i(TAG, String.format(" %s", id3Frame.id)); } } + Log.i(TAG, "]"); } // AudioRendererEventListener diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 303b4671cf..6107a9ad75 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -342,12 +342,14 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); - if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) { - format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding); - } - if (metadata != null) { - format = format.copyWithMetadata(metadata); + if (track.type == C.TRACK_TYPE_AUDIO) { + if (gaplessInfoHolder.hasGaplessInfo()) { + format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding); + } + if (metadata != null) { + format = format.copyWithMetadata(metadata); + } } mp4Track.trackOutput.format(format); From bffffb0fac093d7fb6934d266f48c59b4ecb6264 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 12 Oct 2016 17:27:54 +0100 Subject: [PATCH 07/17] Minor ID3 tweaks --- .../com/google/android/exoplayer2/Format.java | 15 +++++++-------- .../exoplayer2/extractor/GaplessInfoHolder.java | 1 - .../exoplayer2/extractor/mp3/Mp3Extractor.java | 11 ++--------- .../android/exoplayer2/metadata/id3/Id3Frame.java | 2 -- 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 65e797c8fe..9528536296 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -58,6 +58,10 @@ public final class Format implements Parcelable { * Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ public final String codecs; + /** + * Metadata, or null if unknown or not applicable. + */ + public final Metadata metadata; // Container specific. @@ -87,11 +91,6 @@ public final class Format implements Parcelable { * DRM initialization data if the stream is protected, or null otherwise. */ public final DrmInitData drmInitData; - /** - * Static metadata - */ - public final Metadata metadata; - // Video specific. @@ -245,18 +244,18 @@ public final class Format implements Parcelable { @C.SelectionFlags int selectionFlags, String language) { return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData, - selectionFlags, language); + selectionFlags, language, null); } public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, int maxInputSize, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding, List initializationData, DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, String language) { + @C.SelectionFlags int selectionFlags, String language, Metadata metadata) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, - initializationData, drmInitData, null); + initializationData, drmInitData, metadata); } // Text. 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 72d2e1abdf..6eb9bc50de 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.Format; - import java.util.regex.Matcher; import java.util.regex.Pattern; 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 a107b11b2e..54c4219e5a 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 @@ -31,9 +31,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; - -import org.w3c.dom.Comment; - import java.io.EOFException; import java.io.IOException; @@ -147,14 +144,10 @@ public final class Mp3Extractor implements Extractor { if (seeker == null) { seeker = setupSeeker(input); extractorOutput.seekMap(seeker); - Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, + 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); - if (metadata != null) { - format = format.copyWithMetadata(metadata); - } - trackOutput.format(format); + gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); } return readSample(input); } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index dc41d2250c..9948f730eb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.metadata.id3; -import android.os.Parcelable; - import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; From 50aeb20cc2f58ef6f5a0ad9ef52093b4be328ec3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 17 Oct 2016 14:38:56 +0100 Subject: [PATCH 08/17] Make Id3Decoder stateless again One issue with the previous implementation was that isUnsynchronized would not be set back to false if previously set to true and if the next header has majorVersion >= 4. In general, having the decoder be stateless is clearer (and thread safe, albeit that this property is not required). --- .../exoplayer2/metadata/id3/Id3Decoder.java | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) 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 8887af8675..a30adb2bdc 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 @@ -36,10 +36,6 @@ 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 int majorVersion; - private int minorVersion; - private boolean isUnsynchronized; - @Override public boolean canDecode(String mimeType) { return mimeType.equals(MimeTypes.APPLICATION_ID3); @@ -49,20 +45,21 @@ public final class Id3Decoder implements MetadataDecoder { public Metadata decode(byte[] data, int size) throws MetadataDecoderException { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); - int id3Size = decodeId3Header(id3Data); + Id3Header id3Header = decodeId3Header(id3Data); - if (isUnsynchronized) { - id3Data = removeUnsynchronization(id3Data, id3Size); - id3Size = id3Data.bytesLeft(); + int framesBytesLeft = id3Header.framesSize; + if (id3Header.isUnsynchronized) { + id3Data = removeUnsynchronization(id3Data, id3Header.framesSize); + framesBytesLeft = id3Data.bytesLeft(); } - while (id3Size > 0) { + while (framesBytesLeft > 0) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); - int frameId3 = majorVersion > 2 ? id3Data.readUnsignedByte() : 0; - int frameSize = majorVersion == 2 ? id3Data.readUnsignedInt24() : - majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt(); + int frameId3 = id3Header.majorVersion > 2 ? id3Data.readUnsignedByte() : 0; + int frameSize = id3Header.majorVersion == 2 ? id3Data.readUnsignedInt24() : + id3Header.majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt(); if (frameSize <= 1) { break; @@ -75,9 +72,9 @@ public final class Id3Decoder implements MetadataDecoder { boolean hasGroupIdentifier = false; boolean hasDataLength = false; - if (majorVersion > 2) { + if (id3Header.majorVersion > 2) { int flags = id3Data.readShort(); - if (majorVersion == 3) { + if (id3Header.majorVersion == 3) { isCompressed = (flags & 0x0080) != 0; isEncrypted = (flags & 0x0040) != 0; hasDataLength = isCompressed; @@ -90,7 +87,7 @@ public final class Id3Decoder implements MetadataDecoder { } } - int headerSize = majorVersion == 2 ? 6 : 10; + int headerSize = id3Header.majorVersion == 2 ? 6 : 10; if (hasGroupIdentifier) { ++headerSize; @@ -110,7 +107,7 @@ public final class Id3Decoder implements MetadataDecoder { id3Data.skipBytes(4); } - id3Size -= frameSize + headerSize; + framesBytesLeft -= frameSize + headerSize; if (isCompressed || isEncrypted) { id3Data.skipBytes(frameSize); @@ -190,10 +187,11 @@ public final class Id3Decoder implements MetadataDecoder { /** * @param id3Buffer A {@link ParsableByteArray} from which data should be read. - * @return The size of ID3 frames in bytes, excluding the header and footer. + * @return The parsed header. * @throws MetadataDecoderException If ID3 file identifier != "ID3". */ - private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { + private static Id3Header decodeId3Header(ParsableByteArray id3Buffer) + throws MetadataDecoderException { int id1 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte(); @@ -202,11 +200,12 @@ public final class Id3Decoder implements MetadataDecoder { "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); } - majorVersion = id3Buffer.readUnsignedByte(); - minorVersion = id3Buffer.readUnsignedByte(); + int majorVersion = id3Buffer.readUnsignedByte(); + id3Buffer.skipBytes(1); // Skip minor version. + boolean isUnsynchronized = false; int flags = id3Buffer.readUnsignedByte(); - int id3Size = id3Buffer.readSynchSafeInt(); + int framesSize = id3Buffer.readSynchSafeInt(); if (majorVersion < 4) { // this flag is advisory in version 4, use the frame flags instead @@ -219,7 +218,7 @@ public final class Id3Decoder implements MetadataDecoder { int extendedHeaderSize = id3Buffer.readInt(); // size excluding size field if (extendedHeaderSize == 6 || extendedHeaderSize == 10) { id3Buffer.skipBytes(extendedHeaderSize); - id3Size -= (extendedHeaderSize + 4); + framesSize -= (extendedHeaderSize + 4); } } } else if (majorVersion >= 4) { @@ -229,16 +228,16 @@ public final class Id3Decoder implements MetadataDecoder { if (extendedHeaderSize > 4) { id3Buffer.skipBytes(extendedHeaderSize - 4); } - id3Size -= extendedHeaderSize; + framesSize -= extendedHeaderSize; } // Check if footer presents. if ((flags & 0x10) != 0) { - id3Size -= 10; + framesSize -= 10; } } - return id3Size; + return new Id3Header(majorVersion, isUnsynchronized, framesSize); } private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) @@ -419,8 +418,25 @@ public final class Id3Decoder implements MetadataDecoder { } } - private final static String[] standardGenres = new String[] { + public static String decodeGenre(int code) { + return (0 < code && code <= standardGenres.length) ? standardGenres[code - 1] : null; + } + private static final class Id3Header { + + private final int majorVersion; + private final boolean isUnsynchronized; + private final int framesSize; + + public Id3Header(int majorVersion, boolean isUnsynchronized, int framesSize) { + this.majorVersion = majorVersion; + this.isUnsynchronized = isUnsynchronized; + this.framesSize = framesSize; + } + + } + + private static final String[] standardGenres = new String[] { // These are the official ID3v1 genres. "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", @@ -436,7 +452,6 @@ public final class Id3Decoder implements MetadataDecoder { "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", - // These were made up by the authors of Winamp but backported into the ID3 spec. "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", @@ -447,7 +462,6 @@ public final class Id3Decoder implements MetadataDecoder { "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall", - // These were also invented by the Winamp folks but ignored by the ID3 authors. "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", @@ -456,15 +470,4 @@ public final class Id3Decoder implements MetadataDecoder { "Synthpop" }; - public static String decodeGenre(int n) - { - n--; - - if (n < 0 || n >= standardGenres.length) { - return null; - } - - return standardGenres[n]; - } - } From 110c8f6f1f4459b05e9943fd6f5c8521735bbfbe Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 17 Oct 2016 22:35:21 +0100 Subject: [PATCH 09/17] Improvements to ID3 decoder --- .../exoplayer2/metadata/id3/Id3Decoder.java | 394 +++++++++--------- 1 file changed, 202 insertions(+), 192 deletions(-) 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 a30adb2bdc..8c6d449b07 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.util.Log; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; @@ -31,6 +32,8 @@ import java.util.Locale; */ public final class Id3Decoder implements MetadataDecoder { + private static final String TAG = "Id3Decoder"; + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; @@ -45,201 +48,187 @@ public final class Id3Decoder implements MetadataDecoder { public Metadata decode(byte[] data, int size) throws MetadataDecoderException { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); - Id3Header id3Header = decodeId3Header(id3Data); - int framesBytesLeft = id3Header.framesSize; - if (id3Header.isUnsynchronized) { - id3Data = removeUnsynchronization(id3Data, id3Header.framesSize); - framesBytesLeft = id3Data.bytesLeft(); + Id3Header id3Header = decodeHeader(id3Data); + if (id3Header == null) { + return null; } - while (framesBytesLeft > 0) { - int frameId0 = id3Data.readUnsignedByte(); - int frameId1 = id3Data.readUnsignedByte(); - int frameId2 = id3Data.readUnsignedByte(); - int frameId3 = id3Header.majorVersion > 2 ? id3Data.readUnsignedByte() : 0; - int frameSize = id3Header.majorVersion == 2 ? id3Data.readUnsignedInt24() : - id3Header.majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt(); + int startPosition = id3Data.getPosition(); + int framesSize = id3Header.framesSize; + if (id3Header.isUnsynchronized) { + framesSize = removeUnsynchronization(id3Data, id3Header.framesSize); + } + id3Data.setLimit(startPosition + framesSize); - if (frameSize <= 1) { - break; - } - - // Frame flags. - boolean isCompressed = false; - boolean isEncrypted = false; - boolean isUnsynchronized = false; - boolean hasGroupIdentifier = false; - boolean hasDataLength = false; - - if (id3Header.majorVersion > 2) { - int flags = id3Data.readShort(); - if (id3Header.majorVersion == 3) { - isCompressed = (flags & 0x0080) != 0; - isEncrypted = (flags & 0x0040) != 0; - hasDataLength = isCompressed; - } else { - isCompressed = (flags & 0x0008) != 0; - isEncrypted = (flags & 0x0004) != 0; - isUnsynchronized = (flags & 0x0002) != 0; - hasGroupIdentifier = (flags & 0x0040) != 0; - hasDataLength = (flags & 0x0001) != 0; - } - } - - int headerSize = id3Header.majorVersion == 2 ? 6 : 10; - - if (hasGroupIdentifier) { - ++headerSize; - --frameSize; - id3Data.skipBytes(1); - } - - if (isEncrypted) { - ++headerSize; - --frameSize; - id3Data.skipBytes(1); - } - - if (hasDataLength) { - headerSize += 4; - frameSize -= 4; - id3Data.skipBytes(4); - } - - framesBytesLeft -= frameSize + headerSize; - - if (isCompressed || isEncrypted) { - id3Data.skipBytes(frameSize); - } else { - try { - Id3Frame frame; - ParsableByteArray frameData = id3Data; - if (isUnsynchronized) { - frameData = removeUnsynchronization(id3Data, frameSize); - frameSize = frameData.bytesLeft(); - } - - if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { - frame = decodeTxxxFrame(frameData, frameSize); - } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { - frame = decodePrivFrame(frameData, frameSize); - } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { - frame = decodeGeobFrame(frameData, frameSize); - } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { - frame = decodeApicFrame(frameData, frameSize); - } else if (frameId0 == 'T') { - String id = frameId3 != 0 ? - String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : - String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); - frame = decodeTextInformationFrame(frameData, frameSize, id); - } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && - (frameId3 == 'M' || frameId3 == 0)) { - frame = decodeCommentFrame(frameData, frameSize); - } else { - String id = frameId3 != 0 ? - String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : - String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); - frame = decodeBinaryFrame(frameData, frameSize, id); - } - id3Frames.add(frame); - } catch (UnsupportedEncodingException e) { - throw new MetadataDecoderException("Unsupported character encoding"); - } + int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; + while (id3Data.bytesLeft() >= frameHeaderSize) { + Id3Frame frame = decodeFrame(id3Header, id3Data); + if (frame != null) { + id3Frames.add(frame); } } return new Metadata(id3Frames); } - private static int indexOfEos(byte[] data, int fromIndex, int encoding) { - int terminationPos = indexOfZeroByte(data, fromIndex); - - // For single byte encoding charsets, we're done. - if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { - return terminationPos; - } - - // Otherwise ensure an even index and look for a second zero byte. - while (terminationPos < data.length - 1) { - if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { - return terminationPos; - } - terminationPos = indexOfZeroByte(data, terminationPos + 1); - } - - return data.length; - } - - private static int indexOfZeroByte(byte[] data, int fromIndex) { - for (int i = fromIndex; i < data.length; i++) { - if (data[i] == (byte) 0) { - return i; - } - } - return data.length; - } - - private static int delimiterLength(int encodingByte) { - return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) - ? 1 : 2; - } - /** - * @param id3Buffer A {@link ParsableByteArray} from which data should be read. - * @return The parsed header. - * @throws MetadataDecoderException If ID3 file identifier != "ID3". + * @param data A {@link ParsableByteArray} from which the header should be read. + * @return The parsed header, or null if the ID3 tag is unsupported. + * @throws MetadataDecoderException If the first three bytes differ from "ID3". */ - private static Id3Header decodeId3Header(ParsableByteArray id3Buffer) + private static Id3Header decodeHeader(ParsableByteArray data) throws MetadataDecoderException { - int id1 = id3Buffer.readUnsignedByte(); - int id2 = id3Buffer.readUnsignedByte(); - int id3 = id3Buffer.readUnsignedByte(); + int id1 = data.readUnsignedByte(); + int id2 = data.readUnsignedByte(); + int id3 = data.readUnsignedByte(); if (id1 != 'I' || id2 != 'D' || id3 != '3') { throw new MetadataDecoderException(String.format(Locale.US, - "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); + "Unexpected ID3 tag identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); } - int majorVersion = id3Buffer.readUnsignedByte(); - id3Buffer.skipBytes(1); // Skip minor version. - boolean isUnsynchronized = false; + int majorVersion = data.readUnsignedByte(); + data.skipBytes(1); // Skip minor version. + int flags = data.readUnsignedByte(); + int framesSize = data.readSynchSafeInt(); - int flags = id3Buffer.readUnsignedByte(); - int framesSize = id3Buffer.readSynchSafeInt(); - - if (majorVersion < 4) { - // this flag is advisory in version 4, use the frame flags instead - isUnsynchronized = (flags & 0x80) != 0; - } - - if (majorVersion == 3) { - // check for extended header - if ((flags & 0x40) != 0) { - int extendedHeaderSize = id3Buffer.readInt(); // size excluding size field - if (extendedHeaderSize == 6 || extendedHeaderSize == 10) { - id3Buffer.skipBytes(extendedHeaderSize); - framesSize -= (extendedHeaderSize + 4); - } + if (majorVersion == 2) { + boolean isCompressed = (flags & 0x40) != 0; + if (isCompressed) { + Log.w(TAG, "Skipped ID3 tag with majorVersion=1 and undefined compression scheme"); + return null; } - } else if (majorVersion >= 4) { - // check for extended header - if ((flags & 0x40) != 0) { - int extendedHeaderSize = id3Buffer.readSynchSafeInt(); // size including size field - if (extendedHeaderSize > 4) { - id3Buffer.skipBytes(extendedHeaderSize - 4); - } + } else if (majorVersion == 3) { + boolean hasExtendedHeader = (flags & 0x40) != 0; + if (hasExtendedHeader) { + int extendedHeaderSize = data.readInt(); // Size excluding size field. + data.skipBytes(extendedHeaderSize); + framesSize -= (extendedHeaderSize + 4); + } + } else if (majorVersion == 4) { + boolean hasExtendedHeader = (flags & 0x40) != 0; + if (hasExtendedHeader) { + int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field. + data.skipBytes(extendedHeaderSize - 4); framesSize -= extendedHeaderSize; } - - // Check if footer presents. - if ((flags & 0x10) != 0) { + boolean hasFooter = (flags & 0x10) != 0; + if (hasFooter) { framesSize -= 10; } + } else { + Log.w(TAG, "Skipped ID3 tag with unsupported majorVersion=" + majorVersion); + return null; } + // isUnsynchronized is advisory only in version 4. Frame level flags are used instead. + boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0; return new Id3Header(majorVersion, isUnsynchronized, framesSize); } + private Id3Frame decodeFrame(Id3Header id3Header, ParsableByteArray id3Data) + throws MetadataDecoderException { + int frameId0 = id3Data.readUnsignedByte(); + int frameId1 = id3Data.readUnsignedByte(); + int frameId2 = id3Data.readUnsignedByte(); + int frameId3 = id3Header.majorVersion >= 3 ? id3Data.readUnsignedByte() : 0; + + int frameSize; + if (id3Header.majorVersion == 4) { + frameSize = id3Data.readUnsignedIntToInt(); + if ((frameSize & 0x808080L) == 0) { + // Parse the frame size as a syncsafe integer, as per the spec. + frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) + | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); + } else { + // Proceed using the frame size read as an unsigned integer. + Log.w(TAG, "Frame size not specified as syncsafe integer"); + } + } else if (id3Header.majorVersion == 3) { + frameSize = id3Data.readUnsignedIntToInt(); + } else /* id3Header.majorVersion == 2 */ { + frameSize = id3Data.readUnsignedInt24(); + } + + int flags = id3Header.majorVersion >= 2 ? id3Data.readShort() : 0; + if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0 + && flags == 0) { + // We must be reading zero padding at the end of the tag. + id3Data.setPosition(id3Data.limit()); + return null; + } + + int nextFramePosition = id3Data.getPosition() + frameSize; + + // Frame flags. + boolean isCompressed = false; + boolean isEncrypted = false; + boolean isUnsynchronized = false; + boolean hasDataLength = false; + boolean hasGroupIdentifier = false; + if (id3Header.majorVersion == 3) { + isCompressed = (flags & 0x0080) != 0; + isEncrypted = (flags & 0x0040) != 0; + hasGroupIdentifier = (flags & 0x0020) != 0; + hasDataLength = isCompressed; + } else if (id3Header.majorVersion == 4) { + hasGroupIdentifier = (flags & 0x0040) != 0; + isCompressed = (flags & 0x0008) != 0; + isEncrypted = (flags & 0x0004) != 0; + isUnsynchronized = (flags & 0x0002) != 0; + hasDataLength = (flags & 0x0001) != 0; + } + + if (isCompressed || isEncrypted) { + Log.w(TAG, "Skipping unsupported compressed or encrypted frame"); + id3Data.setPosition(nextFramePosition); + return null; + } + + if (hasGroupIdentifier) { + frameSize--; + id3Data.skipBytes(1); + } + if (hasDataLength) { + frameSize -= 4; + id3Data.skipBytes(4); + } + if (isUnsynchronized) { + frameSize = removeUnsynchronization(id3Data, frameSize); + } + + try { + Id3Frame frame; + if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { + frame = decodeTxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { + frame = decodePrivFrame(id3Data, frameSize); + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { + frame = decodeGeobFrame(id3Data, frameSize); + } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { + frame = decodeApicFrame(id3Data, frameSize); + } else if (frameId0 == 'T') { + String id = frameId3 != 0 ? + String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : + String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + frame = decodeTextInformationFrame(id3Data, frameSize, id); + } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && + (frameId3 == 'M' || frameId3 == 0)) { + frame = decodeCommentFrame(id3Data, frameSize); + } else { + String id = frameId3 != 0 ? + String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : + String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + frame = decodeBinaryFrame(id3Data, frameSize, id); + } + return frame; + } catch (UnsupportedEncodingException e) { + throw new MetadataDecoderException("Unsupported character encoding"); + } finally { + id3Data.setPosition(nextFramePosition); + } + } + private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); @@ -368,34 +357,22 @@ public final class Id3Decoder implements MetadataDecoder { } /** - * Undo the unsynchronization applied to one or more frames. - * @param dataSource The original data, positioned at the beginning of a frame. - * @param count The number of valid bytes in the frames to be processed. - * @return replacement data for the frames. + * 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. + * @return The length of the data after processing. */ - private static ParsableByteArray removeUnsynchronization(ParsableByteArray dataSource, int count) { - byte[] source = dataSource.data; - int sourceIndex = dataSource.getPosition(); - int limit = sourceIndex + count; - byte[] dest = new byte[count]; - int destIndex = 0; - - while (sourceIndex < limit) { - byte b = source[sourceIndex++]; - if ((b & 0xFF) == 0xFF) { - int nextIndex = sourceIndex+1; - if (nextIndex < limit) { - int b2 = source[nextIndex]; - if (b2 == 0) { - // skip the 0 byte - ++sourceIndex; - } - } + private static int removeUnsynchronization(ParsableByteArray data, int length) { + byte[] bytes = data.data; + for (int i = data.getPosition(); i + 1 < length; i++) { + if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) { + System.arraycopy(bytes, i + 2, bytes, i + 1, length - i - 2); + length--; } - dest[destIndex++] = b; } - - return new ParsableByteArray(dest, destIndex); + return length; } /** @@ -418,6 +395,39 @@ public final class Id3Decoder implements MetadataDecoder { } } + private static int indexOfEos(byte[] data, int fromIndex, int encoding) { + int terminationPos = indexOfZeroByte(data, fromIndex); + + // For single byte encoding charsets, we're done. + if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { + return terminationPos; + } + + // Otherwise ensure an even index and look for a second zero byte. + while (terminationPos < data.length - 1) { + if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { + return terminationPos; + } + terminationPos = indexOfZeroByte(data, terminationPos + 1); + } + + return data.length; + } + + private static int indexOfZeroByte(byte[] data, int fromIndex) { + for (int i = fromIndex; i < data.length; i++) { + if (data[i] == (byte) 0) { + return i; + } + } + return data.length; + } + + private static int delimiterLength(int encodingByte) { + return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) + ? 1 : 2; + } + public static String decodeGenre(int code) { return (0 < code && code <= standardGenres.length) ? standardGenres[code - 1] : null; } From 4391014a7af98b227377b0f4bf75cac245914f33 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 17 Oct 2016 22:45:09 +0100 Subject: [PATCH 10/17] Split genres into separate util class --- .../exoplayer2/extractor/mp4/AtomParsers.java | 5 +- .../exoplayer2/metadata/id3/Id3Decoder.java | 38 ----------- .../exoplayer2/metadata/id3/Id3Util.java | 63 +++++++++++++++++++ 3 files changed, 65 insertions(+), 41 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index f829c7b4ee..d91d677f87 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.BinaryFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.metadata.id3.Id3Util; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; @@ -36,7 +36,6 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.AvcConfig; import com.google.android.exoplayer2.video.HevcConfig; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -666,7 +665,7 @@ import java.util.List; byte[] bytes = (byte[]) value; if (bytes.length == 2) { int code = (bytes[0] << 8) + (bytes[1] & 0xFF); - String s = Id3Decoder.decodeGenre(code); + String s = Id3Util.decodeGenre(code); if (s != null) { Id3Frame frame = new TextInformationFrame(attributeName, s); builder.add(frame); 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 8c6d449b07..46b7dbde76 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 @@ -428,10 +428,6 @@ public final class Id3Decoder implements MetadataDecoder { ? 1 : 2; } - public static String decodeGenre(int code) { - return (0 < code && code <= standardGenres.length) ? standardGenres[code - 1] : null; - } - private static final class Id3Header { private final int majorVersion; @@ -446,38 +442,4 @@ public final class Id3Decoder implements MetadataDecoder { } - private static final String[] standardGenres = new String[] { - // These are the official ID3v1 genres. - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", - "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", - "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", - "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", - "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", - "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", - "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", - "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", - "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", - "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", - "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", - "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", - "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", - "Hard Rock", - // These were made up by the authors of Winamp but backported into the ID3 spec. - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", - "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", - "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", - "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", - "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", - "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", - "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", - "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", - "Dance Hall", - // These were also invented by the Winamp folks but ignored by the ID3 authors. - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", - "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", - "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", - "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", - "Synthpop" - }; - } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java new file mode 100644 index 0000000000..9a7a6e7cb8 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java @@ -0,0 +1,63 @@ +/* + * 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; + +/** + * ID3 utility methods. + */ +public class Id3Util { + + private static final String[] STANDARD_GENRES = new String[] { + // These are the official ID3v1 genres. + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", + "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", + "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", + "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", + "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", + "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", + "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", + "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", + "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", + "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", + "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", + "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", + "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", + "Hard Rock", + // These were made up by the authors of Winamp but backported into the ID3 spec. + "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", + "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", + "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", + "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", + "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", + "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", + "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", + "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", + "Dance Hall", + // These were also invented by the Winamp folks but ignored by the ID3 authors. + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", + "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", + "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", + "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", + "Synthpop" + }; + + private Id3Util() {} + + public static String decodeGenre(int code) { + return (0 < code && code <= STANDARD_GENRES.length) ? STANDARD_GENRES[code - 1] : null; + } + +} From 66652f65bbad83df8b6bef20a7d24943e7f6a17a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 17 Oct 2016 22:47:03 +0100 Subject: [PATCH 11/17] Make Id3Util final --- .../com/google/android/exoplayer2/metadata/id3/Id3Util.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java index 9a7a6e7cb8..64f2ce9908 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer2.metadata.id3; /** * ID3 utility methods. */ -public class Id3Util { +public final class Id3Util { private static final String[] STANDARD_GENRES = new String[] { // These are the official ID3v1 genres. From 7594f5b78ba216f6085675972f830b2ebfe230ee Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Oct 2016 15:02:35 +0100 Subject: [PATCH 12/17] Further enhance ID3 decoder + support --- .../extractor/GaplessInfoHolder.java | 21 +++ .../exoplayer2/extractor/mp3/Id3Util.java | 95 ---------- .../extractor/mp3/Mp3Extractor.java | 78 +++++++-- .../exoplayer2/metadata/id3/Id3Decoder.java | 162 ++++++++++-------- 4 files changed, 175 insertions(+), 181 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java 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 6eb9bc50de..4b5fa977ee 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 @@ -16,6 +16,8 @@ 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 java.util.regex.Matcher; import java.util.regex.Pattern; @@ -65,6 +67,25 @@ public final class GaplessInfoHolder { return false; } + /** + * Populates the holder with data parsed from ID3 {@link Metadata}. + * + * @param metadata The metadata from which to parse the gapless information. + * @return Whether the holder was populated. + */ + public boolean setFromMetadata(Metadata metadata) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; + if (setFromComment(commentFrame.description, commentFrame.text)) { + return true; + } + } + } + return false; + } + /** * 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. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java deleted file mode 100644 index af08514889..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.extractor.mp3; - -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; -import com.google.android.exoplayer2.metadata.id3.Id3Decoder; -import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; - -/** - * Utility for parsing ID3 version 2 metadata in MP3 files. - */ -/* package */ final class Id3Util { - - /** - * The maximum valid length for metadata in bytes. - */ - private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024; - - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - - /** - * Peeks data from the input and parses ID3 metadata, including gapless playback information. - * - * @param input The {@link ExtractorInput} from which data should be peeked. - * @return The metadata, if present, {@code null} otherwise. - * @throws IOException If an error occurred peeking from the input. - * @throws InterruptedException If the thread was interrupted. - */ - public static Metadata parseId3(ExtractorInput input) - throws IOException, InterruptedException { - Metadata result = null; - ParsableByteArray scratch = new ParsableByteArray(10); - int peekedId3Bytes = 0; - while (true) { - input.peekFully(scratch.data, 0, 10); - scratch.setPosition(0); - if (scratch.readUnsignedInt24() != ID3_TAG) { - break; - } - - int majorVersion = scratch.readUnsignedByte(); - int minorVersion = scratch.readUnsignedByte(); - int flags = scratch.readUnsignedByte(); - int length = scratch.readSynchSafeInt(); - int frameLength = length + 10; - - try { - if (canParseMetadata(majorVersion, minorVersion, flags, length)) { - input.resetPeekPosition(); - byte[] frame = new byte[frameLength]; - input.peekFully(frame, 0, frameLength); - return new Id3Decoder().decode(frame, frameLength); - } else { - input.advancePeekPosition(length); - } - } catch (MetadataDecoderException e) { - e.printStackTrace(); - } - - peekedId3Bytes += frameLength; - } - input.resetPeekPosition(); - input.advancePeekPosition(peekedId3Bytes); - return result; - } - - private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags, - int length) { - return minorVersion != 0xFF && majorVersion >= 2 && majorVersion <= 4 - && length <= MAXIMUM_METADATA_SIZE - && !(majorVersion == 2 && ((flags & 0x3F) != 0 || (flags & 0x40) != 0)) - && !(majorVersion == 3 && (flags & 0x1F) != 0) - && !(majorVersion == 4 && (flags & 0x0F) != 0); - } - - private Id3Util() {} - -} 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 54c4219e5a..acec0c5567 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -28,7 +29,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.CommentFrame; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; @@ -51,6 +53,8 @@ public final class Mp3Extractor implements Extractor { }; + private static final String TAG = "Mp3Extractor"; + /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -59,6 +63,18 @@ public final class Mp3Extractor implements Extractor { * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. */ private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + /** + * First three bytes of a well formed ID3 tag header. + */ + private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + /** + * Length of an ID3 tag header. + */ + private static final int ID3_HEADER_LENGTH = 10; + /** + * Maximum length of data read into {@link #scratch}. + */ + private static final int SCRATCH_LENGTH = 10; /** * Mask that includes the audio header values that must match between frames. @@ -100,7 +116,7 @@ public final class Mp3Extractor implements Extractor { */ public Mp3Extractor(long forcedFirstSampleTimestampUs) { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; - scratch = new ParsableByteArray(4); + scratch = new ParsableByteArray(SCRATCH_LENGTH); synchronizedHeader = new MpegAudioHeader(); gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; @@ -147,7 +163,7 @@ 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, null)); } return readSample(input); } @@ -202,18 +218,7 @@ public final class Mp3Extractor implements Extractor { int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES; input.resetPeekPosition(); if (input.getPosition() == 0) { - metadata = Id3Util.parseId3(input); - if (!gaplessInfoHolder.hasGaplessInfo()) { - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry entry = metadata.get(i); - if (entry instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) entry; - if (gaplessInfoHolder.setFromComment(commentFrame.description, commentFrame.text)) { - break; - } - } - } - } + peekId3Data(input); peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); @@ -267,6 +272,49 @@ public final class Mp3Extractor implements Extractor { return true; } + /** + * Peeks ID3 data from the input, including gapless playback information. + * + * @param input The {@link ExtractorInput} from which data should be peeked. + * @throws IOException If an error occurred peeking from the input. + * @throws InterruptedException If the thread was interrupted. + */ + private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException { + int peekedId3Bytes = 0; + while (true) { + input.peekFully(scratch.data, 0, ID3_HEADER_LENGTH); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + // Not an ID3 tag. + break; + } + scratch.skipBytes(3); // Skip major version, minor version and flags. + int framesLength = scratch.readSynchSafeInt(); + int tagLength = ID3_HEADER_LENGTH + framesLength; + + try { + if (metadata == null) { + byte[] id3Data = new byte[tagLength]; + System.arraycopy(scratch.data, 0, id3Data, 0, ID3_HEADER_LENGTH); + input.peekFully(id3Data, ID3_HEADER_LENGTH, framesLength); + metadata = new Id3Decoder().decode(id3Data, tagLength); + if (metadata != null) { + gaplessInfoHolder.setFromMetadata(metadata); + } + } else { + input.advancePeekPosition(framesLength); + } + } catch (MetadataDecoderException e) { + Log.e(TAG, "Failed to decode ID3 tag", e); + } + + peekedId3Bytes += tagLength; + } + + input.resetPeekPosition(); + input.advancePeekPosition(peekedId3Bytes); + } + /** * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide * data from the start of the first frame in the stream. On returning, the input's position will 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 46b7dbde76..3e1bbe159a 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 @@ -63,7 +63,7 @@ public final class Id3Decoder implements MetadataDecoder { int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; while (id3Data.bytesLeft() >= frameHeaderSize) { - Id3Frame frame = decodeFrame(id3Header, id3Data); + Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data); if (frame != null) { id3Frames.add(frame); } @@ -72,6 +72,40 @@ public final class Id3Decoder implements MetadataDecoder { return new Metadata(id3Frames); } + // TODO: Move the following three methods nearer to the bottom of the file. + private static int indexOfEos(byte[] data, int fromIndex, int encoding) { + int terminationPos = indexOfZeroByte(data, fromIndex); + + // For single byte encoding charsets, we're done. + if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { + return terminationPos; + } + + // Otherwise ensure an even index and look for a second zero byte. + while (terminationPos < data.length - 1) { + if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { + return terminationPos; + } + terminationPos = indexOfZeroByte(data, terminationPos + 1); + } + + return data.length; + } + + private static int indexOfZeroByte(byte[] data, int fromIndex) { + for (int i = fromIndex; i < data.length; i++) { + if (data[i] == (byte) 0) { + return i; + } + } + return data.length; + } + + private static int delimiterLength(int encodingByte) { + return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) + ? 1 : 2; + } + /** * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. @@ -126,15 +160,15 @@ public final class Id3Decoder implements MetadataDecoder { return new Id3Header(majorVersion, isUnsynchronized, framesSize); } - private Id3Frame decodeFrame(Id3Header id3Header, ParsableByteArray id3Data) + private Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data) throws MetadataDecoderException { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); - int frameId3 = id3Header.majorVersion >= 3 ? id3Data.readUnsignedByte() : 0; + int frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0; int frameSize; - if (id3Header.majorVersion == 4) { + if (majorVersion == 4) { frameSize = id3Data.readUnsignedIntToInt(); if ((frameSize & 0x808080L) == 0) { // Parse the frame size as a syncsafe integer, as per the spec. @@ -144,13 +178,13 @@ public final class Id3Decoder implements MetadataDecoder { // Proceed using the frame size read as an unsigned integer. Log.w(TAG, "Frame size not specified as syncsafe integer"); } - } else if (id3Header.majorVersion == 3) { + } else if (majorVersion == 3) { frameSize = id3Data.readUnsignedIntToInt(); } else /* id3Header.majorVersion == 2 */ { frameSize = id3Data.readUnsignedInt24(); } - int flags = id3Header.majorVersion >= 2 ? id3Data.readShort() : 0; + int flags = majorVersion >= 3 ? id3Data.readShort() : 0; if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0 && flags == 0) { // We must be reading zero padding at the end of the tag. @@ -159,6 +193,9 @@ public final class Id3Decoder implements MetadataDecoder { } int nextFramePosition = id3Data.getPosition() + frameSize; + if (nextFramePosition > id3Data.limit()) { + return null; + } // Frame flags. boolean isCompressed = false; @@ -166,12 +203,12 @@ public final class Id3Decoder implements MetadataDecoder { boolean isUnsynchronized = false; boolean hasDataLength = false; boolean hasGroupIdentifier = false; - if (id3Header.majorVersion == 3) { + if (majorVersion == 3) { isCompressed = (flags & 0x0080) != 0; isEncrypted = (flags & 0x0040) != 0; hasGroupIdentifier = (flags & 0x0020) != 0; hasDataLength = isCompressed; - } else if (id3Header.majorVersion == 4) { + } else if (majorVersion == 4) { hasGroupIdentifier = (flags & 0x0040) != 0; isCompressed = (flags & 0x0008) != 0; isEncrypted = (flags & 0x0004) != 0; @@ -199,26 +236,29 @@ public final class Id3Decoder implements MetadataDecoder { try { Id3Frame frame; - if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { + if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' + && (majorVersion == 2 || frameId3 == 'X')) { frame = decodeTxxxFrame(id3Data, frameSize); } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { frame = decodePrivFrame(id3Data, frameSize); - } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' + && (frameId3 == 'B' || majorVersion == 2)) { frame = decodeGeobFrame(id3Data, frameSize); - } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { - frame = decodeApicFrame(id3Data, frameSize); + } 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 = frameId3 != 0 ? - String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : - String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + 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' || frameId3 == 0)) { + } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' + && (frameId3 == 'M' || majorVersion == 2)) { frame = decodeCommentFrame(id3Data, frameSize); } else { - String id = frameId3 != 0 ? - String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : - String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + 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 = decodeBinaryFrame(id3Data, frameSize, id); } return frame; @@ -288,16 +328,29 @@ public final class Id3Decoder implements MetadataDecoder { return new GeobFrame(mimeType, filename, description, objectData); } - private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize) - throws UnsupportedEncodingException { + private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize, + int majorVersion) throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); - int mimeTypeEndIndex = indexOfZeroByte(data, 0); - String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); + String mimeType; + int mimeTypeEndIndex; + if (majorVersion == 2) { + mimeTypeEndIndex = 2; + mimeType = "image/" + new String(data, 0, 3, "ISO-8859-1").toLowerCase(); + if (mimeType.equals("image/jpg")) { + mimeType = "image/jpeg"; + } + } else { + mimeTypeEndIndex = indexOfZeroByte(data, 0); + mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1").toLowerCase(); + if (mimeType.indexOf('/') == -1) { + mimeType = "image/" + mimeType; + } + } int pictureType = data[mimeTypeEndIndex + 1] & 0xFF; @@ -312,20 +365,6 @@ public final class Id3Decoder implements MetadataDecoder { return new ApicFrame(mimeType, description, pictureType, pictureData); } - private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, - int frameSize, String id) 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); - - return new TextInformationFrame(id, description); - } - private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); @@ -348,6 +387,20 @@ public final class Id3Decoder implements MetadataDecoder { return new CommentFrame(language, description, text); } + private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, + int frameSize, String id) 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); + + return new TextInformationFrame(id, description); + } + private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, String id) { byte[] frame = new byte[frameSize]; @@ -395,39 +448,6 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static int indexOfEos(byte[] data, int fromIndex, int encoding) { - int terminationPos = indexOfZeroByte(data, fromIndex); - - // For single byte encoding charsets, we're done. - if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { - return terminationPos; - } - - // Otherwise ensure an even index and look for a second zero byte. - while (terminationPos < data.length - 1) { - if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { - return terminationPos; - } - terminationPos = indexOfZeroByte(data, terminationPos + 1); - } - - return data.length; - } - - private static int indexOfZeroByte(byte[] data, int fromIndex) { - for (int i = fromIndex; i < data.length; i++) { - if (data[i] == (byte) 0) { - return i; - } - } - return data.length; - } - - private static int delimiterLength(int encodingByte) { - return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) - ? 1 : 2; - } - private static final class Id3Header { private final int majorVersion; From 7e352295d7f966b6e5b2b7520bd766840be8423d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Oct 2016 15:17:01 +0100 Subject: [PATCH 13/17] Propagate ID3 data for MP3 --- .../android/exoplayer2/demo/EventLogger.java | 79 +++++++++++-------- .../metadata/id3/Id3DecoderTest.java | 3 +- .../extractor/mp3/Mp3Extractor.java | 2 +- 3 files changed, 46 insertions(+), 38 deletions(-) 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 bde8aa6220..c3fc5b9549 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 @@ -40,10 +40,10 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelections; +import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; @@ -56,7 +56,7 @@ import java.util.Locale; /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, - MappingTrackSelector.EventListener, MetadataRenderer.Output { + TrackSelector.EventListener, MetadataRenderer.Output { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -183,38 +183,9 @@ import java.util.Locale; @Override public void onMetadata(Metadata metadata) { - Log.i(TAG, "metadata ["); - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry entry = metadata.get(i); - if (entry instanceof TxxxFrame) { - TxxxFrame txxxFrame = (TxxxFrame) entry; - Log.i(TAG, String.format(" %s: description=%s, value=%s", txxxFrame.id, - txxxFrame.description, txxxFrame.value)); - } else if (entry instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) entry; - Log.i(TAG, String.format(" %s: owner=%s", privFrame.id, privFrame.owner)); - } else if (entry instanceof GeobFrame) { - GeobFrame geobFrame = (GeobFrame) entry; - Log.i(TAG, String.format(" %s: mimeType=%s, filename=%s, description=%s", - geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); - } else if (entry instanceof ApicFrame) { - ApicFrame apicFrame = (ApicFrame) entry; - Log.i(TAG, String.format(" %s: mimeType=%s, description=%s", - apicFrame.id, apicFrame.mimeType, apicFrame.description)); - } else if (entry instanceof TextInformationFrame) { - TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.i(TAG, String.format(" %s: description=%s", textInformationFrame.id, - textInformationFrame.description)); - } else if (entry instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) entry; - Log.i(TAG, String.format(" %s: language=%s description=%s", commentFrame.id, - commentFrame.language, commentFrame.description)); - } else if (entry instanceof Id3Frame) { - Id3Frame id3Frame = (Id3Frame) entry; - Log.i(TAG, String.format(" %s", id3Frame.id)); - } - } - Log.i(TAG, "]"); + Log.d(TAG, "onMetadata ["); + printMetadata(metadata); + Log.d(TAG, "]"); } // AudioRendererEventListener @@ -237,8 +208,13 @@ import java.util.Locale; @Override public void onAudioInputFormatChanged(Format format) { + boolean hasMetadata = format.metadata != null; Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format) - + "]"); + + (hasMetadata ? "" : "]")); + if (hasMetadata) { + printMetadata(format.metadata); + Log.d(TAG, "]"); + } } @Override @@ -359,6 +335,39 @@ import java.util.Locale; Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); } + private void printMetadata(Metadata metadata) { + 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, String.format(" %s: description=%s, value=%s", txxxFrame.id, + txxxFrame.description, txxxFrame.value)); + } else if (entry instanceof PrivFrame) { + PrivFrame privFrame = (PrivFrame) entry; + Log.d(TAG, String.format(" %s: owner=%s", privFrame.id, privFrame.owner)); + } else if (entry instanceof GeobFrame) { + GeobFrame geobFrame = (GeobFrame) entry; + Log.d(TAG, String.format(" %s: mimeType=%s, filename=%s, description=%s", + geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); + } else if (entry instanceof ApicFrame) { + ApicFrame apicFrame = (ApicFrame) entry; + Log.d(TAG, 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, String.format(" %s: description=%s", textInformationFrame.id, + textInformationFrame.description)); + } else if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; + Log.d(TAG, String.format(" %s: language=%s description=%s", commentFrame.id, + commentFrame.language, commentFrame.description)); + } else if (entry instanceof Id3Frame) { + Id3Frame id3Frame = (Id3Frame) entry; + Log.d(TAG, String.format(" %s", id3Frame.id)); + } + } + } + private String getSessionTimeString() { return getTimeString(SystemClock.elapsedRealtime() - startTimeMs); } 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 8ec966967e..6bfa6fccfc 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 @@ -16,9 +16,8 @@ package com.google.android.exoplayer2.metadata.id3; import android.test.MoreAsserts; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.Metadata; -import java.util.List; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; import junit.framework.TestCase; /** 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 acec0c5567..9d3a7c541f 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 @@ -163,7 +163,7 @@ 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, null)); + gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); } return readSample(input); } From 3e3248d712cffabd203c5547cdfff80b636feb69 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 19 Oct 2016 17:18:17 +0100 Subject: [PATCH 14/17] Yet more misc ID3 improvements --- .../extractor/mp3/Mp3Extractor.java | 40 ++----- .../exoplayer2/metadata/id3/Id3Decoder.java | 108 ++++++++++++++---- 2 files changed, 97 insertions(+), 51 deletions(-) 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 9d3a7c541f..02b92f2077 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.extractor.mp3; -import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -29,7 +28,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -53,8 +51,6 @@ public final class Mp3Extractor implements Extractor { }; - private static final String TAG = "Mp3Extractor"; - /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -63,14 +59,6 @@ public final class Mp3Extractor implements Extractor { * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. */ private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; - /** - * First three bytes of a well formed ID3 tag header. - */ - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - /** - * Length of an ID3 tag header. - */ - private static final int ID3_HEADER_LENGTH = 10; /** * Maximum length of data read into {@link #scratch}. */ @@ -282,30 +270,26 @@ public final class Mp3Extractor implements Extractor { private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException { int peekedId3Bytes = 0; while (true) { - input.peekFully(scratch.data, 0, ID3_HEADER_LENGTH); + input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH); scratch.setPosition(0); - if (scratch.readUnsignedInt24() != ID3_TAG) { + if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) { // Not an ID3 tag. break; } scratch.skipBytes(3); // Skip major version, minor version and flags. int framesLength = scratch.readSynchSafeInt(); - int tagLength = ID3_HEADER_LENGTH + framesLength; + int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength; - try { - if (metadata == null) { - byte[] id3Data = new byte[tagLength]; - System.arraycopy(scratch.data, 0, id3Data, 0, ID3_HEADER_LENGTH); - input.peekFully(id3Data, ID3_HEADER_LENGTH, framesLength); - metadata = new Id3Decoder().decode(id3Data, tagLength); - if (metadata != null) { - gaplessInfoHolder.setFromMetadata(metadata); - } - } else { - input.advancePeekPosition(framesLength); + if (metadata == null) { + 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); + if (metadata != null) { + gaplessInfoHolder.setFromMetadata(metadata); } - } catch (MetadataDecoderException e) { - Log.e(TAG, "Failed to decode ID3 tag", e); + } else { + input.advancePeekPosition(framesLength); } peekedId3Bytes += tagLength; 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 3e1bbe159a..05bff672a4 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 @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.metadata.id3; import android.util.Log; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; @@ -28,12 +28,21 @@ import java.util.List; import java.util.Locale; /** - * Decodes individual TXXX text frames from raw ID3 data. + * Decodes ID3 tags. */ public final class Id3Decoder implements MetadataDecoder { private static final String TAG = "Id3Decoder"; + /** + * The first three bytes of a well formed ID3 tag header. + */ + public static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + /** + * Length of an ID3 tag header. + */ + public static final int ID3_HEADER_LENGTH = 10; + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; @@ -45,7 +54,7 @@ public final class Id3Decoder implements MetadataDecoder { } @Override - public Metadata decode(byte[] data, int size) throws MetadataDecoderException { + public Metadata decode(byte[] data, int size) { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); @@ -61,9 +70,21 @@ public final class Id3Decoder implements MetadataDecoder { } id3Data.setLimit(startPosition + framesSize); + boolean unsignedIntFrameSizeHack = false; + if (id3Header.majorVersion == 4) { + if (!validateV4Frames(id3Data, false)) { + if (validateV4Frames(id3Data, true)) { + unsignedIntFrameSizeHack = true; + } else { + Log.w(TAG, "Failed to validate V4 ID3 tag"); + return null; + } + } + } + int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; while (id3Data.bytesLeft() >= frameHeaderSize) { - Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data); + Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack); if (frame != null) { id3Frames.add(frame); } @@ -109,16 +130,17 @@ public final class Id3Decoder implements MetadataDecoder { /** * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. - * @throws MetadataDecoderException If the first three bytes differ from "ID3". */ - private static Id3Header decodeHeader(ParsableByteArray data) - throws MetadataDecoderException { - int id1 = data.readUnsignedByte(); - int id2 = data.readUnsignedByte(); - int id3 = data.readUnsignedByte(); - if (id1 != 'I' || id2 != 'D' || id3 != '3') { - throw new MetadataDecoderException(String.format(Locale.US, - "Unexpected ID3 tag identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); + private static Id3Header decodeHeader(ParsableByteArray data) { + if (data.bytesLeft() < ID3_HEADER_LENGTH) { + Log.w(TAG, "Data too short to be an ID3 tag"); + return null; + } + + int id = data.readUnsignedInt24(); + if (id != ID3_TAG) { + Log.w(TAG, "Unexpected first three bytes of ID3 tag header: " + id); + return null; } int majorVersion = data.readUnsignedByte(); @@ -129,7 +151,7 @@ public final class Id3Decoder implements MetadataDecoder { if (majorVersion == 2) { boolean isCompressed = (flags & 0x40) != 0; if (isCompressed) { - Log.w(TAG, "Skipped ID3 tag with majorVersion=1 and undefined compression scheme"); + Log.w(TAG, "Skipped ID3 tag with majorVersion=2 and undefined compression scheme"); return null; } } else if (majorVersion == 3) { @@ -160,8 +182,49 @@ public final class Id3Decoder implements MetadataDecoder { return new Id3Header(majorVersion, isUnsynchronized, framesSize); } - private Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data) - throws MetadataDecoderException { + private static boolean validateV4Frames(ParsableByteArray id3Data, + boolean unsignedIntFrameSizeHack) { + int startPosition = id3Data.getPosition(); + try { + while (id3Data.bytesLeft() >= 10) { + int id = id3Data.readInt(); + int frameSize = id3Data.readUnsignedIntToInt(); + int flags = id3Data.readUnsignedShort(); + if (id == 0 && frameSize == 0 && flags == 0) { + return true; + } else { + if (!unsignedIntFrameSizeHack) { + // Parse the data size as a synchsafe integer, as per the spec. + if ((frameSize & 0x808080L) != 0) { + return false; + } + frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) + | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); + } + int minimumFrameSize = 0; + if ((flags & 0x0040) != 0 /* hasGroupIdentifier */) { + minimumFrameSize++; + } + if ((flags & 0x0001) != 0 /* hasDataLength */) { + minimumFrameSize += 4; + } + if (frameSize < minimumFrameSize) { + return false; + } + if (id3Data.bytesLeft() < frameSize) { + return false; + } + id3Data.skipBytes(frameSize); // flags + } + } + return true; + } finally { + id3Data.setPosition(startPosition); + } + } + + private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, + boolean unsignedIntFrameSizeHack) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); @@ -170,13 +233,9 @@ public final class Id3Decoder implements MetadataDecoder { int frameSize; if (majorVersion == 4) { frameSize = id3Data.readUnsignedIntToInt(); - if ((frameSize & 0x808080L) == 0) { - // Parse the frame size as a syncsafe integer, as per the spec. + if (!unsignedIntFrameSizeHack) { frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); - } else { - // Proceed using the frame size read as an unsigned integer. - Log.w(TAG, "Frame size not specified as syncsafe integer"); } } else if (majorVersion == 3) { frameSize = id3Data.readUnsignedIntToInt(); @@ -184,7 +243,7 @@ public final class Id3Decoder implements MetadataDecoder { frameSize = id3Data.readUnsignedInt24(); } - int flags = majorVersion >= 3 ? id3Data.readShort() : 0; + int flags = majorVersion >= 3 ? id3Data.readUnsignedShort() : 0; if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0 && flags == 0) { // We must be reading zero padding at the end of the tag. @@ -194,6 +253,8 @@ public final class Id3Decoder implements MetadataDecoder { int nextFramePosition = id3Data.getPosition() + frameSize; if (nextFramePosition > id3Data.limit()) { + Log.w(TAG, "Frame size exceeds remaining tag data"); + id3Data.setPosition(id3Data.limit()); return null; } @@ -263,7 +324,8 @@ public final class Id3Decoder implements MetadataDecoder { } return frame; } catch (UnsupportedEncodingException e) { - throw new MetadataDecoderException("Unsupported character encoding"); + Log.w(TAG, "Unsupported character encoding"); + return null; } finally { id3Data.setPosition(nextFramePosition); } From 00f9fc6728f961dc55fcfff1db880811b5252966 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 24 Oct 2016 18:01:48 +0100 Subject: [PATCH 15/17] Don't propagate GaplessInfoHolder when parsing mp4 metadata --- .../extractor/GaplessInfoHolder.java | 2 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 29 +++++++------------ .../extractor/mp4/Mp4Extractor.java | 5 +++- 3 files changed, 16 insertions(+), 20 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 4b5fa977ee..7e2a1b4a23 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 @@ -94,7 +94,7 @@ public final class GaplessInfoHolder { * @param data The comment's payload data. * @return Whether the holder was populated. */ - public boolean setFromComment(String name, String data) { + private boolean setFromComment(String name, String data) { if (!GAPLESS_COMMENT_ID.equals(name)) { return false; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index d91d677f87..da29305311 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -407,10 +407,8 @@ import java.util.List; * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. - * @param out {@link GaplessInfoHolder} to populate with gapless playback information. */ - public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, - GaplessInfoHolder out) { + public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // decode one. @@ -424,14 +422,14 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - return parseMetaAtom(udtaData, out); + return parseMetaAtom(udtaData); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) { + private static Metadata parseMetaAtom(ParsableByteArray data) { data.skipBytes(Atom.FULL_HEADER_SIZE); ParsableByteArray ilst = new ParsableByteArray(); while (data.bytesLeft() >= Atom.HEADER_SIZE) { @@ -440,7 +438,7 @@ import java.util.List; if (atomType == Atom.TYPE_ilst) { ilst.reset(data.data, data.getPosition() + payloadSize); ilst.setPosition(data.getPosition()); - Metadata metadata = parseIlst(ilst, out); + Metadata metadata = parseIlst(ilst); if (metadata != null) { return metadata; } @@ -450,13 +448,13 @@ import java.util.List; return null; } - private static Metadata parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) { + private static Metadata parseIlst(ParsableByteArray ilst) { ArrayList entries = new ArrayList<>(); while (ilst.bytesLeft() > 0) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); - parseIlstElement(ilst, type, endPosition, entries, out); + parseIlstElement(ilst, type, endPosition, entries); ilst.setPosition(endPosition); } return entries.isEmpty() ? null : new Metadata(entries); @@ -506,7 +504,7 @@ import java.util.List; // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes private static void parseIlstElement(ParsableByteArray ilst, int type, int endPosition, - List builder, GaplessInfoHolder out) { + List builder) { if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { parseTextAttribute(builder, "TIT2", ilst); } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { @@ -557,7 +555,7 @@ import java.util.List; } else if (type == TYPE_SHOW) { parseTextAttribute(builder, "show", ilst); } else if (type == Atom.TYPE_DASHES) { - parseExtendedAttribute(builder, ilst, endPosition, out); + parseExtendedAttribute(builder, ilst, endPosition); } } @@ -678,7 +676,7 @@ import java.util.List; } private static void parseExtendedAttribute(List builder, ParsableByteArray ilst, - int endPosition, GaplessInfoHolder out) { + int endPosition) { String domain = null; String name = null; Object value = null; @@ -699,14 +697,9 @@ import java.util.List; } if (value != null) { - if (!out.hasGaplessInfo() && Util.areEqual(domain, "com.apple.iTunes")) { - String s = value instanceof byte[] ? new String((byte[]) value) : value.toString(); - out.setFromComment(name, s); - } - - if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunNORM") && (value instanceof byte[])) { + if (Util.areEqual(domain, "com.apple.iTunes")) { String s = new String((byte[]) value); - Id3Frame frame = new CommentFrame("eng", "iTunNORM", s); + Id3Frame frame = new CommentFrame("eng", name, s); builder.add(frame); } else if (domain != null && name != null) { String extendedName = domain + "." + name; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 6107a9ad75..4c52622c78 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -315,7 +315,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { - metadata = AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder); + metadata = AtomParsers.parseUdta(udta, isQuickTime); + if (metadata != null) { + gaplessInfoHolder.setFromMetadata(metadata); + } } for (int i = 0; i < moov.containerChildren.size(); i++) { From 1b39d21ed41ebaaac47fe4a5f61ba40b77bee1dc Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 24 Oct 2016 19:22:41 +0100 Subject: [PATCH 16/17] Fix indentation and missing javadoc --- .../exoplayer2/extractor/mp4/AtomParsers.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index da29305311..b8456ecb4d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -95,8 +95,8 @@ import java.util.List; Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, - stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, - stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); + stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, + stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); } /** @@ -407,6 +407,7 @@ import java.util.List; * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. + * @return Parsed metadata, or null. */ public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { if (isQuickTime) { @@ -868,12 +869,12 @@ import java.util.List; /** * Parses a stsd atom (defined in 14496-12). * - * @param stsd The stsd atom to decode. - * @param trackId The track's identifier in its container. + * @param stsd The stsd atom to decode. + * @param trackId The track's identifier in its container. * @param rotationDegrees The rotation of the track in degrees. - * @param language The language of the track. - * @param drmInitData {@link DrmInitData} to be included in the format. - * @param isQuickTime True for QuickTime media. False otherwise. + * @param language The language of the track. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @param isQuickTime True for QuickTime media. False otherwise. * @return An object containing the parsed data. */ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees, From 8caaf0b5d992d5a31e7771ca49ec76ff5232e84b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 26 Oct 2016 23:45:50 +0100 Subject: [PATCH 17/17] Big cleanup of mp4 metadata extraction --- .../android/exoplayer2/demo/EventLogger.java | 39 +- .../exoplayer2/extractor/mp4/Atom.java | 3 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 337 +----------------- .../extractor/mp4/MetadataUtil.java | 323 +++++++++++++++++ .../exoplayer2/metadata/id3/Id3Util.java | 63 ---- .../exoplayer2/util/ParsableByteArray.java | 6 +- 6 files changed, 369 insertions(+), 402 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java delete mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java 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 c3fc5b9549..5e0b76f68b 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 @@ -154,6 +154,18 @@ import java.util.Locale; } Log.d(TAG, " ]"); } + // Log metadata for at most one of the tracks selected for the renderer. + if (trackSelection != null) { + for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) { + Metadata metadata = trackSelection.getFormat(selectionIndex).metadata; + if (metadata != null) { + Log.d(TAG, " Metadata ["); + printMetadata(metadata, " "); + Log.d(TAG, " ]"); + break; + } + } + } Log.d(TAG, " ]"); } } @@ -184,7 +196,7 @@ import java.util.Locale; @Override public void onMetadata(Metadata metadata) { Log.d(TAG, "onMetadata ["); - printMetadata(metadata); + printMetadata(metadata, " "); Log.d(TAG, "]"); } @@ -208,13 +220,8 @@ import java.util.Locale; @Override public void onAudioInputFormatChanged(Format format) { - boolean hasMetadata = format.metadata != null; Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format) - + (hasMetadata ? "" : "]")); - if (hasMetadata) { - printMetadata(format.metadata); - Log.d(TAG, "]"); - } + + "]"); } @Override @@ -335,35 +342,35 @@ import java.util.Locale; Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); } - private void printMetadata(Metadata metadata) { + 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, String.format(" %s: description=%s, value=%s", txxxFrame.id, + Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id, txxxFrame.description, txxxFrame.value)); } else if (entry instanceof PrivFrame) { PrivFrame privFrame = (PrivFrame) entry; - Log.d(TAG, String.format(" %s: owner=%s", privFrame.id, privFrame.owner)); + Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); } else if (entry instanceof GeobFrame) { GeobFrame geobFrame = (GeobFrame) entry; - Log.d(TAG, String.format(" %s: mimeType=%s, filename=%s, description=%s", + Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s", geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); } else if (entry instanceof ApicFrame) { ApicFrame apicFrame = (ApicFrame) entry; - Log.d(TAG, String.format(" %s: mimeType=%s, description=%s", + 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, String.format(" %s: description=%s", textInformationFrame.id, + 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, String.format(" %s: language=%s description=%s", commentFrame.id, - commentFrame.language, commentFrame.description)); + Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id, + commentFrame.language, commentFrame.description, commentFrame.text)); } else if (entry instanceof Id3Frame) { Id3Frame id3Frame = (Id3Frame) entry; - Log.d(TAG, String.format(" %s", id3Frame.id)); + Log.d(TAG, prefix + String.format("%s", id3Frame.id)); } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index e93e9e3d9c..749c9b3542 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -132,7 +132,6 @@ import java.util.List; public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08"); public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); - public static final int TYPE_DASHES = Util.getIntegerCodeForString("----"); public final int type; @@ -299,7 +298,7 @@ import java.util.List; * @return The corresponding four character string. */ public static String getAtomTypeString(int type) { - return "" + (char) (type >> 24) + return "" + (char) ((type >> 24) & 0xFF) + (char) ((type >> 16) & 0xFF) + (char) ((type >> 8) & 0xFF) + (char) (type & 0xFF); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index b8456ecb4d..47cb3262e1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -24,11 +24,6 @@ import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.BinaryFrame; -import com.google.android.exoplayer2.metadata.id3.CommentFrame; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.Id3Util; -import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -418,336 +413,45 @@ import java.util.List; ParsableByteArray udtaData = udtaAtom.data; udtaData.setPosition(Atom.HEADER_SIZE); while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) { + int atomPosition = udtaData.getPosition(); int atomSize = udtaData.readInt(); int atomType = udtaData.readInt(); if (atomType == Atom.TYPE_meta) { - udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); - udtaData.setLimit(udtaData.getPosition() + atomSize); - return parseMetaAtom(udtaData); + udtaData.setPosition(atomPosition); + return parseMetaAtom(udtaData, atomPosition + atomSize); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray data) { - data.skipBytes(Atom.FULL_HEADER_SIZE); - ParsableByteArray ilst = new ParsableByteArray(); - while (data.bytesLeft() >= Atom.HEADER_SIZE) { - int payloadSize = data.readInt() - Atom.HEADER_SIZE; - int atomType = data.readInt(); + private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) { + meta.skipBytes(Atom.FULL_HEADER_SIZE); + while (meta.getPosition() < limit) { + int atomPosition = meta.getPosition(); + int atomSize = meta.readInt(); + int atomType = meta.readInt(); if (atomType == Atom.TYPE_ilst) { - ilst.reset(data.data, data.getPosition() + payloadSize); - ilst.setPosition(data.getPosition()); - Metadata metadata = parseIlst(ilst); - if (metadata != null) { - return metadata; - } + meta.setPosition(atomPosition); + return parseIlst(meta, atomPosition + atomSize); } - data.skipBytes(payloadSize); + meta.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseIlst(ParsableByteArray ilst) { + private static Metadata parseIlst(ParsableByteArray ilst, int limit) { + ilst.skipBytes(Atom.HEADER_SIZE); ArrayList entries = new ArrayList<>(); - while (ilst.bytesLeft() > 0) { - int position = ilst.getPosition(); - int endPosition = position + ilst.readInt(); - int type = ilst.readInt(); - parseIlstElement(ilst, type, endPosition, entries); - ilst.setPosition(endPosition); + while (ilst.getPosition() < limit) { + Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst); + if (entry != null) { + entries.add(entry); + } } return entries.isEmpty() ? null : new Metadata(entries); } - private static final String P1 = "\u00a9"; - private static final String P2 = "\ufffd"; - private static final int TYPE_NAME_1 = Util.getIntegerCodeForString(P1 + "nam"); - private static final int TYPE_NAME_2 = Util.getIntegerCodeForString(P2 + "nam"); - private static final int TYPE_NAME_3 = Util.getIntegerCodeForString(P1 + "trk"); - private static final int TYPE_NAME_4 = Util.getIntegerCodeForString(P2 + "trk"); - private static final int TYPE_COMMENT_1 = Util.getIntegerCodeForString(P1 + "cmt"); - private static final int TYPE_COMMENT_2 = Util.getIntegerCodeForString(P2 + "cmt"); - private static final int TYPE_YEAR_1 = Util.getIntegerCodeForString(P1 + "day"); - private static final int TYPE_YEAR_2 = Util.getIntegerCodeForString(P2 + "day"); - private static final int TYPE_ARTIST_1 = Util.getIntegerCodeForString(P1 + "ART"); - private static final int TYPE_ARTIST_2 = Util.getIntegerCodeForString(P2 + "ART"); - private static final int TYPE_ENCODER_1 = Util.getIntegerCodeForString(P1 + "too"); - private static final int TYPE_ENCODER_2 = Util.getIntegerCodeForString(P2 + "too"); - private static final int TYPE_ALBUM_1 = Util.getIntegerCodeForString(P1 + "alb"); - private static final int TYPE_ALBUM_2 = Util.getIntegerCodeForString(P2 + "alb"); - private static final int TYPE_COMPOSER_1 = Util.getIntegerCodeForString(P1 + "com"); - private static final int TYPE_COMPOSER_2 = Util.getIntegerCodeForString(P2 + "com"); - private static final int TYPE_COMPOSER_3 = Util.getIntegerCodeForString(P1 + "wrt"); - private static final int TYPE_COMPOSER_4 = Util.getIntegerCodeForString(P2 + "wrt"); - private static final int TYPE_LYRICS_1 = Util.getIntegerCodeForString(P1 + "lyr"); - private static final int TYPE_LYRICS_2 = Util.getIntegerCodeForString(P2 + "lyr"); - private static final int TYPE_GENRE_1 = Util.getIntegerCodeForString(P1 + "gen"); - private static final int TYPE_GENRE_2 = Util.getIntegerCodeForString(P2 + "gen"); - private static final int TYPE_STANDARD_GENRE = Util.getIntegerCodeForString("gnre"); - private static final int TYPE_GROUPING_1 = Util.getIntegerCodeForString(P1 + "grp"); - private static final int TYPE_GROUPING_2 = Util.getIntegerCodeForString(P2 + "grp"); - private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); - private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); - private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); - private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); - private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); - private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); - private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); - private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); - private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); - private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); - private static final int TYPE_SORT_SHOW = Util.getIntegerCodeForString("sosn"); - private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); - private static final int TYPE_SHOW = Util.getIntegerCodeForString("tvsh"); - - // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes - - private static void parseIlstElement(ParsableByteArray ilst, int type, int endPosition, - List builder) { - if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { - parseTextAttribute(builder, "TIT2", ilst); - } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { - parseCommentAttribute(builder, "COMM", ilst); - } else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) { - parseTextAttribute(builder, "TDRC", ilst); - } else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) { - parseTextAttribute(builder, "TPE1", ilst); - } else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) { - parseTextAttribute(builder, "TSSE", ilst); - } else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) { - parseTextAttribute(builder, "TALB", ilst); - } else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 || - type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { - parseTextAttribute(builder, "TCOM", ilst); - } else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) { - parseTextAttribute(builder, "lyrics", ilst); - } else if (type == TYPE_STANDARD_GENRE) { - parseStandardGenreAttribute(builder, "TCON", ilst); - } else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) { - parseTextAttribute(builder, "TCON", ilst); - } else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) { - parseTextAttribute(builder, "TIT1", ilst); - } else if (type == TYPE_DISK_NUMBER) { - parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition); - } else if (type == TYPE_TRACK_NUMBER) { - parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition); - } else if (type == TYPE_TEMPO) { - parseIntegerAttribute(builder, "TBPM", ilst); - } else if (type == TYPE_COMPILATION) { - parseBooleanAttribute(builder, "TCMP", ilst); - } else if (type == TYPE_ALBUM_ARTIST) { - parseTextAttribute(builder, "TPE2", ilst); - } else if (type == TYPE_SORT_TRACK_NAME) { - parseTextAttribute(builder, "TSOT", ilst); - } else if (type == TYPE_SORT_ALBUM) { - parseTextAttribute(builder, "TSO2", ilst); - } else if (type == TYPE_SORT_ARTIST) { - parseTextAttribute(builder, "TSOA", ilst); - } else if (type == TYPE_SORT_ALBUM_ARTIST) { - parseTextAttribute(builder, "TSOP", ilst); - } else if (type == TYPE_SORT_COMPOSER) { - parseTextAttribute(builder, "TSOC", ilst); - } else if (type == TYPE_SORT_SHOW) { - parseTextAttribute(builder, "sortShow", ilst); - } else if (type == TYPE_GAPLESS_ALBUM) { - parseBooleanAttribute(builder, "gaplessAlbum", ilst); - } else if (type == TYPE_SHOW) { - parseTextAttribute(builder, "show", ilst); - } else if (type == Atom.TYPE_DASHES) { - parseExtendedAttribute(builder, ilst, endPosition); - } - } - - private static void parseTextAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - ilst.skipBytes(4); - String value = ilst.readNullTerminatedString(length - 4); - Id3Frame frame = new TextInformationFrame(attributeName, value); - builder.add(frame); - } else { - ilst.skipBytes(length); - } - } - - private static void parseCommentAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - ilst.skipBytes(4); - String value = ilst.readNullTerminatedString(length - 4); - Id3Frame frame = new CommentFrame("eng", attributeName, value); - builder.add(frame); - } else { - ilst.skipBytes(length); - } - } - - private static void parseBooleanAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof Integer) { - int n = (Integer) value; - String s = n == 0 ? "0" : "1"; - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseIntegerAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof Integer) { - int n = (Integer) value; - String s = "" + n; - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseIndexAndCountAttribute(List builder, - String attributeName, ParsableByteArray ilst, int endPosition) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof byte[]) { - byte[] bytes = (byte[]) value; - if (bytes.length == 8) { - int index = (bytes[2] << 8) + (bytes[3] & 0xFF); - int count = (bytes[4] << 8) + (bytes[5] & 0xFF); - if (index > 0) { - String s = "" + index; - if (count > 0) { - s = s + "/" + count; - } - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseStandardGenreAttribute(List builder, - String attributeName, ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof byte[]) { - byte[] bytes = (byte[]) value; - if (bytes.length == 2) { - int code = (bytes[0] << 8) + (bytes[1] & 0xFF); - String s = Id3Util.decodeGenre(code); - if (s != null) { - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseExtendedAttribute(List builder, ParsableByteArray ilst, - int endPosition) { - String domain = null; - String name = null; - Object value = null; - - while (ilst.getPosition() < endPosition) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_mean) { - domain = ilst.readNullTerminatedString(length); - } else if (key == Atom.TYPE_name) { - name = ilst.readNullTerminatedString(length); - } else if (key == Atom.TYPE_data) { - value = parseDataBox(ilst, length); - } else { - ilst.skipBytes(length); - } - } - - if (value != null) { - if (Util.areEqual(domain, "com.apple.iTunes")) { - String s = new String((byte[]) value); - Id3Frame frame = new CommentFrame("eng", name, s); - builder.add(frame); - } else if (domain != null && name != null) { - String extendedName = domain + "." + name; - if (value instanceof String) { - Id3Frame frame = new TextInformationFrame(extendedName, (String) value); - builder.add(frame); - } else if (value instanceof Integer) { - Id3Frame frame = new TextInformationFrame(extendedName, value.toString()); - builder.add(frame); - } else if (value instanceof byte[]) { - byte[] bb = (byte[]) value; - Id3Frame frame = new BinaryFrame(extendedName, bb); - builder.add(frame); - } - } - } - } - - private static Object parseDataBox(ParsableByteArray ilst, int length) { - int versionAndFlags = ilst.readInt(); - int flags = versionAndFlags & 0xFFFFFF; - boolean isText = (flags == 1); - boolean isData = (flags == 0); - boolean isImageData = (flags == 0xD); - boolean isInteger = (flags == 21); - int dataLength = length - 4; - if (isText) { - return ilst.readNullTerminatedString(dataLength); - } else if (isInteger) { - if (dataLength == 1) { - return ilst.readUnsignedByte(); - } else if (dataLength == 2) { - return ilst.readUnsignedShort(); - } else { - ilst.skipBytes(dataLength); - return null; - } - } else if (isData) { - byte[] bytes = new byte[dataLength]; - ilst.readBytes(bytes, 0, dataLength); - return bytes; - } else { - ilst.skipBytes(dataLength); - return null; - } - } - /** * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie. * @@ -756,12 +460,9 @@ import java.util.List; */ private static long parseMvhd(ParsableByteArray mvhd) { mvhd.setPosition(Atom.HEADER_SIZE); - int fullAtom = mvhd.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); - mvhd.skipBytes(version == 0 ? 8 : 16); - return mvhd.readUnsignedInt(); } 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 new file mode 100644 index 0000000000..4bfef85d10 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -0,0 +1,323 @@ +/* + * 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.extractor.mp4; + +import android.util.Log; +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.TextInformationFrame; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; + +/** + * Parses metadata items stored in ilst atoms. + */ +/* package */ final class MetadataUtil { + + private static final String TAG = "MetadataUtil"; + + // Codes that start with the copyright character (omitted) and have equivalent ID3 frames. + private static final int SHORT_TYPE_NAME_1 = Util.getIntegerCodeForString("nam"); + private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk"); + private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt"); + private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day"); + private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART"); + private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too"); + private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb"); + private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com"); + private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt"); + private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr"); + private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen"); + + // Codes that have equivalent ID3 frames. + private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr"); + private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre"); + private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp"); + private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); + private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); + private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); + private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); + private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); + private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); + private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); + private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); + private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); + private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); + + // Types that do not have equivalent ID3 frames. + private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng"); + private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); + private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn"); + private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh"); + + // Type for items that are intended for internal use by the player. + private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----"); + + // Standard genres. + private static final String[] STANDARD_GENRES = new String[] { + // These are the official ID3v1 genres. + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", + "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", + "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", + "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", + "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", + "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", + "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", + "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", + "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", + "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", + "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", + "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", + "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", + "Hard Rock", + // These were made up by the authors of Winamp but backported into the ID3 spec. + "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", + "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", + "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", + "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", + "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", + "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", + "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", + "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", + "Dance Hall", + // These were also invented by the Winamp folks but ignored by the ID3 authors. + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", + "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", + "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", + "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", + "Synthpop" + }; + + private static final String LANGUAGE_UNDEFINED = "und"; + + private MetadataUtil() {} + + /** + * Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting + * from the current position of the {@link ParsableByteArray}, and the position is advanced by + * the size of the element. The position is advanced even if the element's type is unrecognized. + * + * @param ilst Holds the data to be parsed. + * @return The parsed element, or null if the element's type was not recognized. + */ + public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) { + int position = ilst.getPosition(); + int endPosition = position + ilst.readInt(); + int type = ilst.readInt(); + int typeTopByte = (type >> 24) & 0xFF; + try { + if (typeTopByte == '\u00A9' /* Copyright char */ + || typeTopByte == '\uFFFD' /* Replacement char */) { + int shortType = type & 0x00FFFFFF; + if (shortType == SHORT_TYPE_COMMENT) { + return parseCommentAttribute(type, ilst); + } else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) { + return parseTextAttribute(type, "TIT2", ilst); + } else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) { + return parseTextAttribute(type, "TCOM", ilst); + } else if (shortType == SHORT_TYPE_YEAR) { + return parseTextAttribute(type, "TDRC", ilst); + } else if (shortType == SHORT_TYPE_ARTIST) { + return parseTextAttribute(type, "TPE1", ilst); + } else if (shortType == SHORT_TYPE_ENCODER) { + return parseTextAttribute(type, "TSSE", ilst); + } else if (shortType == SHORT_TYPE_ALBUM) { + return parseTextAttribute(type, "TALB", ilst); + } else if (shortType == SHORT_TYPE_LYRICS) { + return parseTextAttribute(type, "USLT", ilst); + } else if (shortType == SHORT_TYPE_GENRE) { + return parseTextAttribute(type, "TCON", ilst); + } else if (shortType == TYPE_GROUPING) { + return parseTextAttribute(type, "TIT1", ilst); + } + } else if (type == TYPE_GENRE) { + return parseStandardGenreAttribute(ilst); + } else if (type == TYPE_DISK_NUMBER) { + return parseIndexAndCountAttribute(type, "TPOS", ilst); + } else if (type == TYPE_TRACK_NUMBER) { + return parseIndexAndCountAttribute(type, "TRCK", ilst); + } else if (type == TYPE_TEMPO) { + return parseUint8Attribute(type, "TBPM", ilst, true, false); + } else if (type == TYPE_COMPILATION) { + return parseUint8Attribute(type, "TCMP", ilst, true, true); + } else if (type == TYPE_COVER_ART) { + return parseCoverArt(ilst); + } else if (type == TYPE_ALBUM_ARTIST) { + return parseTextAttribute(type, "TPE2", ilst); + } else if (type == TYPE_SORT_TRACK_NAME) { + return parseTextAttribute(type, "TSOT", ilst); + } else if (type == TYPE_SORT_ALBUM) { + return parseTextAttribute(type, "TSO2", ilst); + } else if (type == TYPE_SORT_ARTIST) { + return parseTextAttribute(type, "TSOA", ilst); + } else if (type == TYPE_SORT_ALBUM_ARTIST) { + return parseTextAttribute(type, "TSOP", ilst); + } else if (type == TYPE_SORT_COMPOSER) { + return parseTextAttribute(type, "TSOC", ilst); + } else if (type == TYPE_RATING) { + return parseUint8Attribute(type, "ITUNESADVISORY", ilst, false, false); + } else if (type == TYPE_GAPLESS_ALBUM) { + return parseUint8Attribute(type, "ITUNESGAPLESS", ilst, false, true); + } else if (type == TYPE_TV_SORT_SHOW) { + return parseTextAttribute(type, "TVSHOWSORT", ilst); + } else if (type == TYPE_TV_SHOW) { + return parseTextAttribute(type, "TVSHOW", ilst); + } else if (type == TYPE_INTERNAL) { + return parseInternalAttribute(ilst, endPosition); + } + Log.d(TAG, "Skipped unknown metadata entry: " + Atom.getAtomTypeString(type)); + return null; + } finally { + ilst.setPosition(endPosition); + } + } + + private static TextInformationFrame parseTextAttribute(int type, String id, + ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + 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); + } + Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(atomSize - 16); + return new CommentFrame(LANGUAGE_UNDEFINED, value, value); + } + Log.w(TAG, "Failed to parse comment attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + private static Id3Frame parseUint8Attribute(int type, String id, ParsableByteArray data, + boolean isTextInformationFrame, boolean isBoolean) { + int value = parseUint8AttributeValue(data); + if (isBoolean) { + value = Math.min(1, value); + } + if (value >= 0) { + return isTextInformationFrame ? new TextInformationFrame(id, Integer.toString(value)) + : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value)); + } + Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + private static TextInformationFrame parseIndexAndCountAttribute(int type, String attributeName, + ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data && atomSize >= 22) { + data.skipBytes(10); // version (1), flags (3), empty (4), empty (2) + int index = data.readUnsignedShort(); + if (index > 0) { + String description = "" + index; + int count = data.readUnsignedShort(); + if (count > 0) { + description += "/" + count; + } + return new TextInformationFrame(attributeName, description); + } + } + Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) { + int genreCode = parseUint8AttributeValue(data); + String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) + ? STANDARD_GENRES[genreCode - 1] : null; + if (genreString != null) { + return new TextInformationFrame("TCON", genreString); + } + Log.w(TAG, "Failed to parse standard genre code"); + return null; + } + + private static ApicFrame parseCoverArt(ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + int fullVersionInt = data.readInt(); + int flags = Atom.parseFullAtomFlags(fullVersionInt); + String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null; + if (mimeType == null) { + Log.w(TAG, "Unrecognized cover art flags: " + flags); + return null; + } + data.skipBytes(4); // empty (4) + byte[] pictureData = new byte[atomSize - 16]; + data.readBytes(pictureData, 0, pictureData.length); + return new ApicFrame(mimeType, null, 3 /* Cover (front) */, pictureData); + } + Log.w(TAG, "Failed to parse cover art attribute"); + return null; + } + + private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) { + String domain = null; + String name = null; + int dataAtomPosition = -1; + int dataAtomSize = -1; + while (data.getPosition() < endPosition) { + int atomPosition = data.getPosition(); + int atomSize = data.readInt(); + int atomType = data.readInt(); + data.skipBytes(4); // version (1), flags (3) + if (atomType == Atom.TYPE_mean) { + domain = data.readNullTerminatedString(atomSize - 12); + } else if (atomType == Atom.TYPE_name) { + name = data.readNullTerminatedString(atomSize - 12); + } else { + if (atomType == Atom.TYPE_data) { + dataAtomPosition = atomPosition; + dataAtomSize = atomSize; + } + data.skipBytes(atomSize - 12); + } + } + if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) { + // We're only interested in iTunSMPB. + 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); + } + + private static int parseUint8AttributeValue(ParsableByteArray data) { + data.skipBytes(4); // atomSize + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + return data.readUnsignedByte(); + } + Log.w(TAG, "Failed to parse uint8 attribute value"); + return -1; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java deleted file mode 100644 index 64f2ce9908..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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; - -/** - * ID3 utility methods. - */ -public final class Id3Util { - - private static final String[] STANDARD_GENRES = new String[] { - // These are the official ID3v1 genres. - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", - "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", - "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", - "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", - "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", - "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", - "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", - "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", - "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", - "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", - "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", - "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", - "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", - "Hard Rock", - // These were made up by the authors of Winamp but backported into the ID3 spec. - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", - "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", - "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", - "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", - "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", - "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", - "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", - "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", - "Dance Hall", - // These were also invented by the Winamp folks but ignored by the ID3 authors. - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", - "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", - "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", - "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", - "Synthpop" - }; - - private Id3Util() {} - - public static String decodeGenre(int code) { - return (0 < code && code <= STANDARD_GENRES.length) ? STANDARD_GENRES[code - 1] : null; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 7691899ade..05c29ca032 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -300,9 +300,9 @@ public final class ParsableByteArray { */ public int readLittleEndianInt() { return (data[position++] & 0xFF) - | (data[position++] & 0xFF) << 8 - | (data[position++] & 0xFF) << 16 - | (data[position++] & 0xFF) << 24; + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF) << 16 + | (data[position++] & 0xFF) << 24; } /**