diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index f3c19da8d1..595f14e784 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.ApicFrame; @@ -45,12 +44,10 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelections; -import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; import java.text.NumberFormat; -import java.util.List; import java.util.Locale; /** @@ -59,7 +56,7 @@ import java.util.Locale; /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, - MappingTrackSelector.EventListener, MetadataRenderer.Output { + MappingTrackSelector.EventListener, MetadataRenderer.Output { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -179,44 +176,40 @@ import java.util.Locale; Log.d(TAG, "]"); } - // MetadataRenderer.Output + // MetadataRenderer.Output @Override public void onMetadata(Metadata metadata) { - List id3Frames = metadata.getFrames(); - for (Id3Frame id3Frame : id3Frames) { - if (id3Frame instanceof TxxxFrame) { - TxxxFrame txxxFrame = (TxxxFrame) id3Frame; + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof TxxxFrame) { + TxxxFrame txxxFrame = (TxxxFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", txxxFrame.id, txxxFrame.description, txxxFrame.value)); - } else if (id3Frame instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) id3Frame; + } else if (entry instanceof PrivFrame) { + PrivFrame privFrame = (PrivFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner)); - } else if (id3Frame instanceof GeobFrame) { - GeobFrame geobFrame = (GeobFrame) id3Frame; + } else if (entry instanceof GeobFrame) { + GeobFrame geobFrame = (GeobFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s", geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); - } else if (id3Frame instanceof ApicFrame) { - ApicFrame apicFrame = (ApicFrame) id3Frame; + } else if (entry instanceof ApicFrame) { + ApicFrame apicFrame = (ApicFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s", apicFrame.id, apicFrame.mimeType, apicFrame.description)); - } else if (id3Frame instanceof TextInformationFrame) { - TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame; + } else if (entry instanceof TextInformationFrame) { + TextInformationFrame textInformationFrame = (TextInformationFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id, textInformationFrame.description)); - } else if (id3Frame instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) id3Frame; + } else if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id, commentFrame.language, commentFrame.text)); - } else { + } else if (entry instanceof Id3Frame) { + Id3Frame id3Frame = (Id3Frame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id)); } } - GaplessInfo gaplessInfo = metadata.getGaplessInfo(); - if (gaplessInfo != null) { - Log.i(TAG, String.format("ID3 TimedMetadata encoder delay=%d padding=%d", - gaplessInfo.encoderDelay, gaplessInfo.encoderPadding)); - } } // AudioRendererEventListener diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java index 9bdf330b02..1eb38de40f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java @@ -24,6 +24,8 @@ import android.annotation.TargetApi; import android.media.MediaFormat; import android.os.Parcel; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -56,11 +58,14 @@ public final class FormatTest extends TestCase { TestUtil.buildTestData(128, 1 /* data seed */)); DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); byte[] projectionData = new byte[] {1, 2, 3}; + Metadata metadata = new Metadata( + new TextInformationFrame("id1", "description1"), + new TextInformationFrame("id2", "description2")); Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, - drmInitData); + drmInitData, metadata); Parcel parcel = Parcel.obtain(); formatToParcel.writeToParcel(parcel, 0); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 97ebc6dbbc..8ec966967e 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -32,9 +32,8 @@ public class Id3DecoderTest extends TestCase { 54, 52, 95, 115, 116, 97, 114, 116, 0}; Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - List id3Frames = metadata.getFrames(); - assertEquals(1, id3Frames.size()); - TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0); + assertEquals(1, metadata.length()); + TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0); assertEquals("", txxxFrame.description); assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value); } @@ -45,9 +44,8 @@ public class Id3DecoderTest extends TestCase { 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - List id3Frames = metadata.getFrames(); - assertEquals(1, id3Frames.size()); - ApicFrame apicFrame = (ApicFrame) id3Frames.get(0); + assertEquals(1, metadata.length()); + ApicFrame apicFrame = (ApicFrame) metadata.get(0); assertEquals("image/jpeg", apicFrame.mimeType); assertEquals(16, apicFrame.pictureType); assertEquals("Hello World", apicFrame.description); @@ -60,9 +58,8 @@ public class Id3DecoderTest extends TestCase { 3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}; Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - List id3Frames = metadata.getFrames(); - assertEquals(1, id3Frames.size()); - TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0); + assertEquals(1, metadata.length()); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertEquals("TIT2", textInformationFrame.id); assertEquals("Hello World", textInformationFrame.description); } diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 078fbf98bd..65e797c8fe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -21,7 +21,6 @@ import android.media.MediaFormat; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -411,7 +410,7 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, - language, subsampleOffsetUs, initializationData, drmInitData, null); + language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { @@ -429,14 +428,10 @@ public final class Format implements Parcelable { } public Format copyWithMetadata(Metadata metadata) { - GaplessInfo gaplessInfo = metadata.getGaplessInfo(); - int ed = gaplessInfo != null ? gaplessInfo.encoderDelay : encoderDelay; - int ep = gaplessInfo != null ? gaplessInfo.encoderPadding : encoderPadding; - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, ed, ep, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, + stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 5f43971de8..4829b44d25 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -111,7 +111,7 @@ public final class SimpleExoPlayer implements ExoPlayer { private SurfaceHolder surfaceHolder; private TextureView textureView; private TextRenderer.Output textOutput; - private MetadataRenderer.Output id3Output; + private MetadataRenderer.Output metadataOutput; private VideoListener videoListener; private AudioRendererEventListener audioDebugListener; private VideoRendererEventListener videoDebugListener; @@ -389,12 +389,21 @@ public final class SimpleExoPlayer implements ExoPlayer { } /** - * Sets a listener to receive ID3 metadata events. + * @deprecated Use {@link #setMetadataOutput(MetadataRenderer.Output)} instead. + * @param output The output. + */ + @Deprecated + public void setId3Output(MetadataRenderer.Output output) { + setMetadataOutput(output); + } + + /** + * Sets a listener to receive metadata events. * * @param output The output. */ - public void setId3Output(MetadataRenderer.Output output) { - id3Output = output; + public void setMetadataOutput(MetadataRenderer.Output output) { + metadataOutput = output; } // ExoPlayer implementation @@ -539,9 +548,9 @@ public final class SimpleExoPlayer implements ExoPlayer { Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper()); renderersList.add(textRenderer); - MetadataRenderer id3Renderer = new MetadataRenderer<>(componentListener, + MetadataRenderer metadataRenderer = new MetadataRenderer(componentListener, mainHandler.getLooper(), new Id3Decoder()); - renderersList.add(id3Renderer); + renderersList.add(metadataRenderer); } private void buildExtensionRenderers(ArrayList renderersList, @@ -636,7 +645,7 @@ public final class SimpleExoPlayer implements ExoPlayer { } private final class ComponentListener implements VideoRendererEventListener, - AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, + AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, SurfaceHolder.Callback, TextureView.SurfaceTextureListener, TrackSelector.EventListener { @@ -768,12 +777,12 @@ public final class SimpleExoPlayer implements ExoPlayer { } } - // MetadataRenderer.Output implementation + // MetadataRenderer.Output implementation @Override public void onMetadata(Metadata metadata) { - if (id3Output != null) { - id3Output.onMetadata(metadata); + if (metadataOutput != null) { + metadataOutput.onMetadata(metadata); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java deleted file mode 100644 index 7335d9103f..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor; - -import android.util.Log; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Gapless playback information. - */ -public final class GaplessInfo { - - private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; - private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); - - /** - * The number of samples to trim from the start of the decoded audio stream. - */ - public final int encoderDelay; - - /** - * The number of samples to trim from the end of the decoded audio stream. - */ - public final int encoderPadding; - - /** - * Parses gapless playback information from a gapless playback comment (stored in an ID3 header - * or MPEG 4 user data), if valid and non-zero. - * @param name The comment's identifier. - * @param data The comment's payload data. - * @return the gapless playback info, or null if the provided data is not valid. - */ - public static GaplessInfo createFromComment(String name, String data) { - if(!GAPLESS_COMMENT_ID.equals(name)) { - return null; - } else { - Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); - if(matcher.find()) { - try { - int encoderDelay = Integer.parseInt(matcher.group(1), 16); - int encoderPadding = Integer.parseInt(matcher.group(2), 16); - if(encoderDelay > 0 || encoderPadding > 0) { - Log.d("ExoplayerImpl", "Parsed gapless info: " + encoderDelay + " " + encoderPadding); - return new GaplessInfo(encoderDelay, encoderPadding); - } - } catch (NumberFormatException var5) { - ; - } - } - - // Ignore incorrectly formatted comments. - Log.d("ExoplayerImpl", "Unable to parse gapless info: " + data); - return null; - } - } - - /** - * Parses gapless playback information from an MP3 Xing header, if valid and non-zero. - * - * @param value The 24-bit value to decode. - * @return the gapless playback info, or null if the provided data is not valid. - */ - public static GaplessInfo createFromXingHeaderValue(int value) { - int encoderDelay = value >> 12; - int encoderPadding = value & 0x0FFF; - return encoderDelay > 0 || encoderPadding > 0 ? - new GaplessInfo(encoderDelay, encoderPadding) : - null; - } - - public GaplessInfo(int encoderDelay, int encoderPadding) { - this.encoderDelay = encoderDelay; - this.encoderPadding = encoderPadding; - } -} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 4f98ce4f7e..72d2e1abdf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -15,11 +15,91 @@ */ package com.google.android.exoplayer2.extractor; +import com.google.android.exoplayer2.Format; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Holder for gapless playback information. */ public final class GaplessInfoHolder { - public GaplessInfo gaplessInfo; + private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; + private static final Pattern GAPLESS_COMMENT_PATTERN = + Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); + + /** + * The number of samples to trim from the start of the decoded audio stream, or + * {@link Format#NO_VALUE} if not set. + */ + public int encoderDelay; + + /** + * The number of samples to trim from the end of the decoded audio stream, or + * {@link Format#NO_VALUE} if not set. + */ + public int encoderPadding; + + /** + * Creates a new holder for gapless playback information. + */ + public GaplessInfoHolder() { + encoderDelay = Format.NO_VALUE; + encoderPadding = Format.NO_VALUE; + } + + /** + * Populates the holder with data from an MP3 Xing header, if valid and non-zero. + * + * @param value The 24-bit value to decode. + * @return Whether the holder was populated. + */ + public boolean setFromXingHeaderValue(int value) { + int encoderDelay = value >> 12; + int encoderPadding = value & 0x0FFF; + if (encoderDelay > 0 || encoderPadding > 0) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + return true; + } + return false; + } + + /** + * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header + * or MPEG 4 user data), if valid and non-zero. + * + * @param name The comment's identifier. + * @param data The comment's payload data. + * @return Whether the holder was populated. + */ + public boolean setFromComment(String name, String data) { + if (!GAPLESS_COMMENT_ID.equals(name)) { + return false; + } + Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); + if (matcher.find()) { + try { + int encoderDelay = Integer.parseInt(matcher.group(1), 16); + int encoderPadding = Integer.parseInt(matcher.group(2), 16); + if (encoderDelay > 0 || encoderPadding > 0) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + return true; + } + } catch (NumberFormatException e) { + // Ignore incorrectly formatted comments. + } + } + return false; + } + + /** + * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set. + */ + public boolean hasGaplessInfo() { + return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE; + } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 00f8e27ad2..a107b11b2e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -22,14 +22,18 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; + +import org.w3c.dom.Comment; + import java.io.EOFException; import java.io.IOException; @@ -70,7 +74,7 @@ public final class Mp3Extractor implements Extractor { private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; - private Metadata metadata; + private final GaplessInfoHolder gaplessInfoHolder; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -78,6 +82,7 @@ public final class Mp3Extractor implements Extractor { private int synchronizedHeaderData; + private Metadata metadata; private Seeker seeker; private long basisTimeUs; private long samplesRead; @@ -100,6 +105,7 @@ public final class Mp3Extractor implements Extractor { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(4); synchronizedHeader = new MpegAudioHeader(); + gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; } @@ -141,20 +147,13 @@ public final class Mp3Extractor implements Extractor { if (seeker == null) { seeker = setupSeeker(input); extractorOutput.seekMap(seeker); - - GaplessInfo gaplessInfo = metadata != null ? metadata.getGaplessInfo() : null; - Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, - Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, - synchronizedHeader.sampleRate, Format.NO_VALUE, - gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE, - gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE, - null, null, 0, null); - + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, + synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding, null, null, 0, null); if (metadata != null) { format = format.copyWithMetadata(metadata); } - trackOutput.format(format); } return readSample(input); @@ -211,6 +210,17 @@ public final class Mp3Extractor implements Extractor { input.resetPeekPosition(); if (input.getPosition() == 0) { metadata = Id3Util.parseId3(input); + if (!gaplessInfoHolder.hasGaplessInfo()) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; + if (gaplessInfoHolder.setFromComment(commentFrame.description, commentFrame.text)) { + break; + } + } + } + } peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); @@ -296,16 +306,13 @@ public final class Mp3Extractor implements Extractor { } if (headerData == XING_HEADER || headerData == INFO_HEADER) { seeker = XingSeeker.create(synchronizedHeader, frame, position, length); - if (seeker != null && metadata == null || metadata.getGaplessInfo() == null) { + if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); input.advancePeekPosition(xingBase + 141); input.peekFully(scratch.data, 0, 3); scratch.setPosition(0); - GaplessInfo gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24()); - metadata = metadata != null ? - metadata.withGaplessInfo(gaplessInfo) : new Metadata(null, gaplessInfo); - + gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); } input.skipFully(synchronizedHeader.frameSize); } else if (frame.limit() >= 40) { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 358c815098..05e20102fc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -22,10 +22,8 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataBuilder; import com.google.android.exoplayer2.metadata.id3.BinaryFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; @@ -38,6 +36,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.AvcConfig; import com.google.android.exoplayer2.video.HevcConfig; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -95,8 +95,8 @@ import java.util.List; Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, - stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, - stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); + stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, + stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); } /** @@ -286,7 +286,7 @@ import java.util.List; flags = rechunkedResults.flags; } - if (track.editListDurations == null || gaplessInfoHolder.gaplessInfo != null) { + if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) { // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // This implementation does not support applying both gapless metadata and an edit list. Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); @@ -315,9 +315,10 @@ import java.util.List; track.format.sampleRate, track.timescale); long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, track.format.sampleRate, track.timescale); - if ((encoderDelay > 0 || encoderPadding > 0) && encoderDelay <= Integer.MAX_VALUE + if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE && encoderPadding <= Integer.MAX_VALUE) { - gaplessInfoHolder.gaplessInfo = new GaplessInfo((int) encoderDelay, (int) encoderPadding); + gaplessInfoHolder.encoderDelay = (int) encoderDelay; + gaplessInfoHolder.encoderPadding = (int) encoderPadding; Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } @@ -402,13 +403,14 @@ import java.util.List; } /** - * Parses a udta atom for metadata, including gapless playback information. + * Parses a udta atom. * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. - * @return metadata stored in the user data, or {@code null} if not present. + * @param out {@link GaplessInfoHolder} to populate with gapless playback information. */ - public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { + public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, + GaplessInfoHolder out) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // decode one. @@ -422,14 +424,14 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - return parseMetaAtom(udtaData); + return parseMetaAtom(udtaData, out); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray data) { + private static Metadata parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) { data.skipBytes(Atom.FULL_HEADER_SIZE); ParsableByteArray ilst = new ParsableByteArray(); while (data.bytesLeft() >= Atom.HEADER_SIZE) { @@ -438,9 +440,9 @@ import java.util.List; if (atomType == Atom.TYPE_ilst) { ilst.reset(data.data, data.getPosition() + payloadSize); ilst.setPosition(data.getPosition()); - Metadata result = parseIlst(ilst); - if (result != null) { - return result; + Metadata metadata = parseIlst(ilst, out); + if (metadata != null) { + return metadata; } } data.skipBytes(payloadSize); @@ -448,19 +450,16 @@ import java.util.List; return null; } - private static Metadata parseIlst(ParsableByteArray ilst) { - - MetadataBuilder builder = new MetadataBuilder(); - + private static Metadata parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) { + ArrayList entries = new ArrayList<>(); while (ilst.bytesLeft() > 0) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); - parseIlstElement(ilst, type, endPosition, builder); + parseIlstElement(ilst, type, endPosition, entries, out); ilst.setPosition(endPosition); } - - return builder.build(); + return entries.isEmpty() ? null : new Metadata(entries); } private static final String P1 = "\u00a9"; @@ -506,66 +505,64 @@ import java.util.List; // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes - private static void parseIlstElement( - ParsableByteArray ilst, int type, int endPosition, MetadataBuilder builder) { + private static void parseIlstElement(ParsableByteArray ilst, int type, int endPosition, + List builder, GaplessInfoHolder out) { if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { - parseTextAttribute(builder, "TIT2", ilst, endPosition); + parseTextAttribute(builder, "TIT2", ilst); } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { - parseCommentAttribute(builder, "COMM", ilst, endPosition); + parseCommentAttribute(builder, "COMM", ilst); } else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) { - parseTextAttribute(builder, "TDRC", ilst, endPosition); + parseTextAttribute(builder, "TDRC", ilst); } else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) { - parseTextAttribute(builder, "TPE1", ilst, endPosition); + parseTextAttribute(builder, "TPE1", ilst); } else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) { - parseTextAttribute(builder, "TSSE", ilst, endPosition); + parseTextAttribute(builder, "TSSE", ilst); } else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) { - parseTextAttribute(builder, "TALB", ilst, endPosition); + parseTextAttribute(builder, "TALB", ilst); } else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 || - type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { - parseTextAttribute(builder, "TCOM", ilst, endPosition); + type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { + parseTextAttribute(builder, "TCOM", ilst); } else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) { - parseTextAttribute(builder, "lyrics", ilst, endPosition); + parseTextAttribute(builder, "lyrics", ilst); } else if (type == TYPE_STANDARD_GENRE) { - parseStandardGenreAttribute(builder, "TCON", ilst, endPosition); + parseStandardGenreAttribute(builder, "TCON", ilst); } else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) { - parseTextAttribute(builder, "TCON", ilst, endPosition); + parseTextAttribute(builder, "TCON", ilst); } else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) { - parseTextAttribute(builder, "TIT1", ilst, endPosition); + parseTextAttribute(builder, "TIT1", ilst); } else if (type == TYPE_DISK_NUMBER) { parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition); } else if (type == TYPE_TRACK_NUMBER) { parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition); } else if (type == TYPE_TEMPO) { - parseIntegerAttribute(builder, "TBPM", ilst, endPosition); + parseIntegerAttribute(builder, "TBPM", ilst); } else if (type == TYPE_COMPILATION) { - parseBooleanAttribute(builder, "TCMP", ilst, endPosition); + parseBooleanAttribute(builder, "TCMP", ilst); } else if (type == TYPE_ALBUM_ARTIST) { - parseTextAttribute(builder, "TPE2", ilst, endPosition); + parseTextAttribute(builder, "TPE2", ilst); } else if (type == TYPE_SORT_TRACK_NAME) { - parseTextAttribute(builder, "TSOT", ilst, endPosition); + parseTextAttribute(builder, "TSOT", ilst); } else if (type == TYPE_SORT_ALBUM) { - parseTextAttribute(builder, "TSO2", ilst, endPosition); + parseTextAttribute(builder, "TSO2", ilst); } else if (type == TYPE_SORT_ARTIST) { - parseTextAttribute(builder, "TSOA", ilst, endPosition); + parseTextAttribute(builder, "TSOA", ilst); } else if (type == TYPE_SORT_ALBUM_ARTIST) { - parseTextAttribute(builder, "TSOP", ilst, endPosition); + parseTextAttribute(builder, "TSOP", ilst); } else if (type == TYPE_SORT_COMPOSER) { - parseTextAttribute(builder, "TSOC", ilst, endPosition); + parseTextAttribute(builder, "TSOC", ilst); } else if (type == TYPE_SORT_SHOW) { - parseTextAttribute(builder, "sortShow", ilst, endPosition); + parseTextAttribute(builder, "sortShow", ilst); } else if (type == TYPE_GAPLESS_ALBUM) { - parseBooleanAttribute(builder, "gaplessAlbum", ilst, endPosition); + parseBooleanAttribute(builder, "gaplessAlbum", ilst); } else if (type == TYPE_SHOW) { - parseTextAttribute(builder, "show", ilst, endPosition); + parseTextAttribute(builder, "show", ilst); } else if (type == Atom.TYPE_DASHES) { - parseExtendedAttribute(builder, ilst, endPosition); + parseExtendedAttribute(builder, ilst, endPosition, out); } } - private static void parseTextAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseTextAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -579,10 +576,8 @@ import java.util.List; } } - private static void parseCommentAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseCommentAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -596,10 +591,8 @@ import java.util.List; } } - private static void parseBooleanAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseBooleanAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -616,10 +609,8 @@ import java.util.List; } } - private static void parseIntegerAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseIntegerAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -636,10 +627,8 @@ import java.util.List; } } - private static void parseIndexAndCountAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseIndexAndCountAttribute(List builder, + String attributeName, ParsableByteArray ilst, int endPosition) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -654,7 +643,7 @@ import java.util.List; String s = "" + index; if (count > 0) { s = s + "/" + count; - } + } Id3Frame frame = new TextInformationFrame(attributeName, s); builder.add(frame); } @@ -665,10 +654,8 @@ import java.util.List; } } - private static void parseStandardGenreAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseStandardGenreAttribute(List builder, + String attributeName, ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -690,9 +677,8 @@ import java.util.List; } } - private static void parseExtendedAttribute(MetadataBuilder builder, - ParsableByteArray ilst, - int endPosition) { + private static void parseExtendedAttribute(List builder, ParsableByteArray ilst, + int endPosition, GaplessInfoHolder out) { String domain = null; String name = null; Object value = null; @@ -713,9 +699,9 @@ import java.util.List; } if (value != null) { - if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunSMPB")) { + if (!out.hasGaplessInfo() && Util.areEqual(domain, "com.apple.iTunes")) { String s = value instanceof byte[] ? new String((byte[]) value) : value.toString(); - builder.setGaplessInfo(GaplessInfo.createFromComment("iTunSMPB", s)); + out.setFromComment(name, s); } if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunNORM") && (value instanceof byte[])) { @@ -889,12 +875,12 @@ import java.util.List; /** * Parses a stsd atom (defined in 14496-12). * - * @param stsd The stsd atom to decode. - * @param trackId The track's identifier in its container. + * @param stsd The stsd atom to decode. + * @param trackId The track's identifier in its container. * @param rotationDegrees The rotation of the track in degrees. - * @param language The language of the track. - * @param drmInitData {@link DrmInitData} to be included in the format. - * @param isQuickTime True for QuickTime media. False otherwise. + * @param language The language of the track. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @param isQuickTime True for QuickTime media. False otherwise. * @return An object containing the parsed data. */ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees, diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 1f4461f21e..303b4671cf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; @@ -311,16 +310,12 @@ public final class Mp4Extractor implements Extractor, SeekMap { long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; - GaplessInfo gaplessInfo = null; - Metadata metadata = null; + Metadata metadata = null; + GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { - Metadata info = AtomParsers.parseUdta(udta, isQuickTime); - if (info != null) { - gaplessInfo = info.getGaplessInfo(); - metadata = info; - } + metadata = AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder); } for (int i = 0; i < moov.containerChildren.size(); i++) { @@ -337,10 +332,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); - GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); - gaplessInfoHolder.gaplessInfo = gaplessInfo; TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); - gaplessInfo = gaplessInfoHolder.gaplessInfo; if (trackSampleTable.sampleCount == 0) { continue; } @@ -350,8 +342,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); - if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfo != null) { - format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); + if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) { + format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding); } if (metadata != null) { format = format.copyWithMetadata(metadata); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index c30e7ddb57..40c05a5602 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -17,65 +17,79 @@ package com.google.android.exoplayer2.metadata; import android.os.Parcel; import android.os.Parcelable; - -import com.google.android.exoplayer2.extractor.GaplessInfo; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; - -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; /** - * ID3 style metadata, with convenient access to gapless playback information. + * A collection of metadata entries. */ -public class Metadata implements Parcelable { +public final class Metadata implements Parcelable { - private final List frames; - private final GaplessInfo gaplessInfo; + /** + * A metadata entry. + */ + public interface Entry extends Parcelable {} - public Metadata(List frames, GaplessInfo gaplessInfo) { - List theFrames = frames != null ? new ArrayList<>(frames) : new ArrayList(); - this.frames = Collections.unmodifiableList(theFrames); - this.gaplessInfo = gaplessInfo; + private final Entry[] entries; + + /** + * @param entries The metadata entries. + */ + public Metadata(Entry... entries) { + this.entries = entries == null ? new Entry[0] : entries; } - public Metadata(Parcel in) { - int encoderDelay = in.readInt(); - int encoderPadding = in.readInt(); - gaplessInfo = encoderDelay > 0 || encoderPadding > 0 ? - new GaplessInfo(encoderDelay, encoderPadding) : null; - frames = Arrays.asList((Id3Frame[]) in.readArray(Id3Frame.class.getClassLoader())); + /** + * @param entries The metadata entries. + */ + public Metadata(List entries) { + if (entries != null) { + this.entries = new Entry[entries.size()]; + entries.toArray(this.entries); + } else { + this.entries = new Entry[0]; + } } - public Metadata withGaplessInfo(GaplessInfo info) { - return new Metadata(frames, info); + /* package */ Metadata(Parcel in) { + entries = new Metadata.Entry[in.readInt()]; + for (int i = 0; i < entries.length; i++) { + entries[i] = in.readParcelable(Entry.class.getClassLoader()); + } } - public List getFrames() { - return frames; + /** + * Returns the number of metadata entries. + */ + public int length() { + return entries.length; } - public GaplessInfo getGaplessInfo() { - return gaplessInfo; + /** + * Returns the entry at the specified index. + * + * @param index The index of the entry. + * @return The entry at the specified index. + */ + public Metadata.Entry get(int index) { + return entries[index]; } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Metadata that = (Metadata) o; - - if (!frames.equals(that.frames)) return false; - return gaplessInfo != null ? gaplessInfo.equals(that.gaplessInfo) : that.gaplessInfo == null; + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Metadata other = (Metadata) obj; + return Arrays.equals(entries, other.entries); } @Override public int hashCode() { - int result = frames.hashCode(); - result = 31 * result + (gaplessInfo != null ? gaplessInfo.hashCode() : 0); - return result; + return Arrays.hashCode(entries); } @Override @@ -85,21 +99,22 @@ public class Metadata implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderDelay : -1); - dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderPadding : -1); - dest.writeArray(frames.toArray(new Id3Frame[frames.size()])); + dest.writeInt(entries.length); + for (Entry entry : entries) { + dest.writeParcelable(entry, 0); + } } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - @Override - public Metadata createFromParcel(Parcel in) { - return new Metadata(in); - } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public Metadata createFromParcel(Parcel in) { + return new Metadata(in); + } + + @Override + public Metadata[] newArray(int size) { + return new Metadata[0]; + } + }; - @Override - public Metadata[] newArray(int size) { - return new Metadata[0]; - } - }; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java deleted file mode 100644 index 57f49e5b20..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.metadata; - -import com.google.android.exoplayer2.extractor.GaplessInfo; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; - -import java.util.ArrayList; -import java.util.List; - -/** - * Builder for ID3 style metadata. - */ -public class MetadataBuilder { - private List frames = new ArrayList<>(); - private GaplessInfo gaplessInfo; - - public void add(Id3Frame frame) { - frames.add(frame); - } - - public void setGaplessInfo(GaplessInfo info) { - this.gaplessInfo = info; - } - - public Metadata build() { - return !frames.isEmpty() || gaplessInfo != null ? new Metadata(frames, gaplessInfo): null; - } -} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java index 7cde1f243d..a73311f16b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java @@ -17,10 +17,8 @@ package com.google.android.exoplayer2.metadata; /** * Decodes metadata from binary data. - * - * @param The type of the metadata. */ -public interface MetadataDecoder { +public interface MetadataDecoder { /** * Checks whether the decoder supports a given mime type. @@ -38,6 +36,6 @@ public interface MetadataDecoder { * @return The decoded metadata object. * @throws MetadataDecoderException If a problem occurred decoding the data. */ - T decode(byte[] data, int size) throws MetadataDecoderException; + Metadata decode(byte[] data, int size) throws MetadataDecoderException; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index aca38a1258..ff1364610b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -30,38 +30,34 @@ import java.nio.ByteBuffer; /** * A renderer for metadata. - * - * @param The type of the metadata. */ -public final class MetadataRenderer extends BaseRenderer implements Callback { +public final class MetadataRenderer extends BaseRenderer implements Callback { /** * Receives output from a {@link MetadataRenderer}. - * - * @param The type of the metadata. */ - public interface Output { + public interface Output { /** * Called each time there is a metadata associated with current playback time. * * @param metadata The metadata. */ - void onMetadata(T metadata); + void onMetadata(Metadata metadata); } private static final int MSG_INVOKE_RENDERER = 0; - private final MetadataDecoder metadataDecoder; - private final Output output; + private final MetadataDecoder metadataDecoder; + private final Output output; private final Handler outputHandler; private final FormatHolder formatHolder; private final DecoderInputBuffer buffer; private boolean inputStreamEnded; private long pendingMetadataTimestamp; - private T pendingMetadata; + private Metadata pendingMetadata; /** * @param output The output. @@ -72,8 +68,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback * called directly on the player's internal rendering thread. * @param metadataDecoder A decoder for the metadata. */ - public MetadataRenderer(Output output, Looper outputLooper, - MetadataDecoder metadataDecoder) { + public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) { super(C.TRACK_TYPE_METADATA); this.output = Assertions.checkNotNull(output); this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); @@ -137,7 +132,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback return true; } - private void invokeRenderer(T metadata) { + private void invokeRenderer(Metadata metadata) { if (outputHandler != null) { outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); } else { @@ -150,13 +145,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_INVOKE_RENDERER: - invokeRendererInternal((T) msg.obj); + invokeRendererInternal((Metadata) msg.obj); return true; } return false; } - private void invokeRendererInternal(T metadata) { + private void invokeRendererInternal(Metadata metadata) { output.onMetadata(metadata); } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 2c234a6042..8887af8675 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.metadata.id3; -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; @@ -31,7 +29,7 @@ import java.util.Locale; /** * Decodes individual TXXX text frames from raw ID3 data. */ -public final class Id3Decoder implements MetadataDecoder { +public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; @@ -41,7 +39,6 @@ public final class Id3Decoder implements MetadataDecoder { private int majorVersion; private int minorVersion; private boolean isUnsynchronized; - private GaplessInfo gaplessInfo; @Override public boolean canDecode(String mimeType) { @@ -141,11 +138,7 @@ public final class Id3Decoder implements MetadataDecoder { frame = decodeTextInformationFrame(frameData, frameSize, id); } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && (frameId3 == 'M' || frameId3 == 0)) { - CommentFrame commentFrame = decodeCommentFrame(frameData, frameSize); - frame = commentFrame; - if (gaplessInfo == null) { - gaplessInfo = GaplessInfo.createFromComment(commentFrame.id, commentFrame.text); - } + frame = decodeCommentFrame(frameData, frameSize); } else { String id = frameId3 != 0 ? String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : @@ -159,7 +152,7 @@ public final class Id3Decoder implements MetadataDecoder { } } - return new Metadata(id3Frames, null); + return new Metadata(id3Frames); } private static int indexOfEos(byte[] data, int fromIndex, int encoding) { @@ -198,7 +191,7 @@ public final class Id3Decoder implements MetadataDecoder { /** * @param id3Buffer A {@link ParsableByteArray} from which data should be read. * @return The size of ID3 frames in bytes, excluding the header and footer. - * @throws ParserException If ID3 file identifier != "ID3". + * @throws MetadataDecoderException If ID3 file identifier != "ID3". */ private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { int id1 = id3Buffer.readUnsignedByte(); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index 41c4ae4e03..dc41d2250c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -16,12 +16,14 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcelable; + +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; /** * Base class for ID3 frames. */ -public abstract class Id3Frame implements Parcelable { +public abstract class Id3Frame implements Metadata.Entry { /** * The frame ID.