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 8c12290d35..f3c19da8d1 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; @@ -38,6 +41,7 @@ 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; @@ -55,7 +59,7 @@ import java.util.Locale; /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, - TrackSelector.EventListener, MetadataRenderer.Output> { + MappingTrackSelector.EventListener, MetadataRenderer.Output { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -175,10 +179,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; @@ -199,10 +204,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 550e6ab1d8..078fbf98bd 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; @@ -86,6 +88,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. @@ -185,7 +192,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, @@ -211,7 +218,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. @@ -222,7 +229,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, @@ -250,7 +257,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. @@ -260,7 +267,7 @@ public final class Format implements Parcelable { 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, @@ -274,7 +281,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. @@ -283,7 +290,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. @@ -292,14 +299,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, @@ -307,7 +314,8 @@ public final class Format implements Parcelable { float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding, @C.SelectionFlags int selectionFlags, String language, - long subsampleOffsetUs, List initializationData, DrmInitData drmInitData) { + long subsampleOffsetUs, List initializationData, DrmInitData drmInitData, + Metadata metadata) { this.id = id; this.containerMimeType = containerMimeType; this.sampleMimeType = sampleMimeType; @@ -332,6 +340,7 @@ public final class Format implements Parcelable { this.initializationData = initializationData == null ? Collections.emptyList() : initializationData; this.drmInitData = drmInitData; + this.metadata = metadata; } @SuppressWarnings("ResourceType") @@ -364,20 +373,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, String codecs, int bitrate, int width, int height, @@ -385,7 +395,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, @@ -401,21 +411,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); } /** @@ -475,6 +496,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; @@ -502,6 +524,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; @@ -574,6 +597,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 a8f04e5113..5f43971de8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -35,9 +35,9 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; 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; @@ -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 id3Output; private VideoListener videoListener; private AudioRendererEventListener audioDebugListener; private VideoRendererEventListener videoDebugListener; @@ -393,7 +393,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; } @@ -539,7 +539,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); } @@ -636,7 +636,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 +768,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 ab501af1cb..00f8e27ad2 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; } @@ -141,10 +141,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); } @@ -199,7 +210,7 @@ public final class Mp3Extractor implements Extractor { int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES; input.resetPeekPosition(); if (input.getPosition() == 0) { - Id3Util.parseId3(input, gaplessInfoHolder); + metadata = Id3Util.parseId3(input); peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); @@ -285,13 +296,16 @@ public final class Mp3Extractor implements Extractor { } 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 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 0ca0f216c6..358c815098 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,7 +22,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; @@ -278,7 +286,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); @@ -307,10 +315,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); } @@ -395,17 +402,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); @@ -415,14 +422,14 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - parseMetaAtom(udtaData, out); - break; + return parseMetaAtom(udtaData); } 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) { @@ -431,47 +438,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 f806f47410..241296742a 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,22 +142,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) { @@ -166,6 +157,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 b306fbf76e..7691899ade 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 @@ -423,6 +423,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}. *