Commit ba1da140 by Oliver Woodman

Further modifications to ID3 support

- Lots of misc cleanup
- Remove GaplessInfo from Metadata. IMO it doesn't quite belong there,
  and means it ends up being represented twice inside Format.
- Note: Changes untested, but will be tested in due course!
parent 3b34f850
...@@ -27,7 +27,6 @@ import com.google.android.exoplayer2.Timeline; ...@@ -27,7 +27,6 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; 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.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame;
...@@ -45,12 +44,10 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector; ...@@ -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.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelections; 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.upstream.DataSpec;
import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.io.IOException; import java.io.IOException;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /**
...@@ -59,7 +56,7 @@ import java.util.Locale; ...@@ -59,7 +56,7 @@ import java.util.Locale;
/* package */ final class EventLogger implements ExoPlayer.EventListener, /* package */ final class EventLogger implements ExoPlayer.EventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener,
MappingTrackSelector.EventListener<MappedTrackInfo>, MetadataRenderer.Output<Metadata> { MappingTrackSelector.EventListener<MappedTrackInfo>, MetadataRenderer.Output {
private static final String TAG = "EventLogger"; private static final String TAG = "EventLogger";
private static final int MAX_TIMELINE_ITEM_LINES = 3; private static final int MAX_TIMELINE_ITEM_LINES = 3;
...@@ -179,44 +176,40 @@ import java.util.Locale; ...@@ -179,44 +176,40 @@ import java.util.Locale;
Log.d(TAG, "]"); Log.d(TAG, "]");
} }
// MetadataRenderer.Output<Metadata> // MetadataRenderer.Output
@Override @Override
public void onMetadata(Metadata metadata) { public void onMetadata(Metadata metadata) {
List<Id3Frame> id3Frames = metadata.getFrames(); for (int i = 0; i < metadata.length(); i++) {
for (Id3Frame id3Frame : id3Frames) { Metadata.Entry entry = metadata.get(i);
if (id3Frame instanceof TxxxFrame) { if (entry instanceof TxxxFrame) {
TxxxFrame txxxFrame = (TxxxFrame) id3Frame; TxxxFrame txxxFrame = (TxxxFrame) entry;
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", txxxFrame.id, Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", txxxFrame.id,
txxxFrame.description, txxxFrame.value)); txxxFrame.description, txxxFrame.value));
} else if (id3Frame instanceof PrivFrame) { } else if (entry instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) id3Frame; PrivFrame privFrame = (PrivFrame) entry;
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner)); Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner));
} else if (id3Frame instanceof GeobFrame) { } else if (entry instanceof GeobFrame) {
GeobFrame geobFrame = (GeobFrame) id3Frame; GeobFrame geobFrame = (GeobFrame) entry;
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s", Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description));
} else if (id3Frame instanceof ApicFrame) { } else if (entry instanceof ApicFrame) {
ApicFrame apicFrame = (ApicFrame) id3Frame; ApicFrame apicFrame = (ApicFrame) entry;
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s", Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description)); apicFrame.id, apicFrame.mimeType, apicFrame.description));
} else if (id3Frame instanceof TextInformationFrame) { } else if (entry instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame; TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id, Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id,
textInformationFrame.description)); textInformationFrame.description));
} else if (id3Frame instanceof CommentFrame) { } else if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) id3Frame; CommentFrame commentFrame = (CommentFrame) entry;
Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id, Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id,
commentFrame.language, commentFrame.text)); commentFrame.language, commentFrame.text));
} else { } else if (entry instanceof Id3Frame) {
Id3Frame id3Frame = (Id3Frame) entry;
Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id)); 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 // AudioRendererEventListener
......
...@@ -24,6 +24,8 @@ import android.annotation.TargetApi; ...@@ -24,6 +24,8 @@ import android.annotation.TargetApi;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Parcel; import android.os.Parcel;
import com.google.android.exoplayer2.drm.DrmInitData; 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.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -56,11 +58,14 @@ public final class FormatTest extends TestCase { ...@@ -56,11 +58,14 @@ public final class FormatTest extends TestCase {
TestUtil.buildTestData(128, 1 /* data seed */)); TestUtil.buildTestData(128, 1 /* data seed */));
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
byte[] projectionData = new byte[] {1, 2, 3}; 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, 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, 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, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA,
drmInitData); drmInitData, metadata);
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
formatToParcel.writeToParcel(parcel, 0); formatToParcel.writeToParcel(parcel, 0);
......
...@@ -32,9 +32,8 @@ public class Id3DecoderTest extends TestCase { ...@@ -32,9 +32,8 @@ public class Id3DecoderTest extends TestCase {
54, 52, 95, 115, 116, 97, 114, 116, 0}; 54, 52, 95, 115, 116, 97, 114, 116, 0};
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
Metadata metadata = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
List<Id3Frame> id3Frames = metadata.getFrames(); assertEquals(1, metadata.length());
assertEquals(1, id3Frames.size()); TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0);
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0);
assertEquals("", txxxFrame.description); assertEquals("", txxxFrame.description);
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value); assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
} }
...@@ -45,9 +44,8 @@ public class Id3DecoderTest extends TestCase { ...@@ -45,9 +44,8 @@ public class Id3DecoderTest extends TestCase {
111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
Metadata metadata = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
List<Id3Frame> id3Frames = metadata.getFrames(); assertEquals(1, metadata.length());
assertEquals(1, id3Frames.size()); ApicFrame apicFrame = (ApicFrame) metadata.get(0);
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0);
assertEquals("image/jpeg", apicFrame.mimeType); assertEquals("image/jpeg", apicFrame.mimeType);
assertEquals(16, apicFrame.pictureType); assertEquals(16, apicFrame.pictureType);
assertEquals("Hello World", apicFrame.description); assertEquals("Hello World", apicFrame.description);
...@@ -60,9 +58,8 @@ public class Id3DecoderTest extends TestCase { ...@@ -60,9 +58,8 @@ public class Id3DecoderTest extends TestCase {
3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}; 3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0};
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
Metadata metadata = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
List<Id3Frame> id3Frames = metadata.getFrames(); assertEquals(1, metadata.length());
assertEquals(1, id3Frames.size()); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0);
assertEquals("TIT2", textInformationFrame.id); assertEquals("TIT2", textInformationFrame.id);
assertEquals("Hello World", textInformationFrame.description); assertEquals("Hello World", textInformationFrame.description);
} }
......
...@@ -21,7 +21,6 @@ import android.media.MediaFormat; ...@@ -21,7 +21,6 @@ import android.media.MediaFormat;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import com.google.android.exoplayer2.drm.DrmInitData; 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.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -411,7 +410,7 @@ public final class Format implements Parcelable { ...@@ -411,7 +410,7 @@ public final class Format implements Parcelable {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags,
language, subsampleOffsetUs, initializationData, drmInitData, null); language, subsampleOffsetUs, initializationData, drmInitData, metadata);
} }
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
...@@ -429,13 +428,9 @@ public final class Format implements Parcelable { ...@@ -429,13 +428,9 @@ public final class Format implements Parcelable {
} }
public Format copyWithMetadata(Metadata 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, return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
stereoMode, channelCount, sampleRate, pcmEncoding, ed, ep, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
} }
......
...@@ -111,7 +111,7 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -111,7 +111,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
private SurfaceHolder surfaceHolder; private SurfaceHolder surfaceHolder;
private TextureView textureView; private TextureView textureView;
private TextRenderer.Output textOutput; private TextRenderer.Output textOutput;
private MetadataRenderer.Output<Metadata> id3Output; private MetadataRenderer.Output metadataOutput;
private VideoListener videoListener; private VideoListener videoListener;
private AudioRendererEventListener audioDebugListener; private AudioRendererEventListener audioDebugListener;
private VideoRendererEventListener videoDebugListener; private VideoRendererEventListener videoDebugListener;
...@@ -389,12 +389,21 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -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. * @param output The output.
*/ */
public void setId3Output(MetadataRenderer.Output<Metadata> output) { public void setMetadataOutput(MetadataRenderer.Output output) {
id3Output = output; metadataOutput = output;
} }
// ExoPlayer implementation // ExoPlayer implementation
...@@ -539,9 +548,9 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -539,9 +548,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper()); Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper());
renderersList.add(textRenderer); renderersList.add(textRenderer);
MetadataRenderer<Metadata> id3Renderer = new MetadataRenderer<>(componentListener, MetadataRenderer metadataRenderer = new MetadataRenderer(componentListener,
mainHandler.getLooper(), new Id3Decoder()); mainHandler.getLooper(), new Id3Decoder());
renderersList.add(id3Renderer); renderersList.add(metadataRenderer);
} }
private void buildExtensionRenderers(ArrayList<Renderer> renderersList, private void buildExtensionRenderers(ArrayList<Renderer> renderersList,
...@@ -636,7 +645,7 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -636,7 +645,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
} }
private final class ComponentListener implements VideoRendererEventListener, private final class ComponentListener implements VideoRendererEventListener,
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<Metadata>, AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output,
SurfaceHolder.Callback, TextureView.SurfaceTextureListener, SurfaceHolder.Callback, TextureView.SurfaceTextureListener,
TrackSelector.EventListener<Object> { TrackSelector.EventListener<Object> {
...@@ -768,12 +777,12 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -768,12 +777,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
} }
} }
// MetadataRenderer.Output<Metadata> implementation // MetadataRenderer.Output implementation
@Override @Override
public void onMetadata(Metadata metadata) { public void onMetadata(Metadata metadata) {
if (id3Output != null) { if (metadataOutput != null) {
id3Output.onMetadata(metadata); metadataOutput.onMetadata(metadata);
} }
} }
......
/*
* 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;
}
}
...@@ -15,11 +15,91 @@ ...@@ -15,11 +15,91 @@
*/ */
package com.google.android.exoplayer2.extractor; 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. * Holder for gapless playback information.
*/ */
public final class GaplessInfoHolder { 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;
}
} }
...@@ -22,14 +22,18 @@ import com.google.android.exoplayer2.extractor.Extractor; ...@@ -22,14 +22,18 @@ import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; 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.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata; 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.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import org.w3c.dom.Comment;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
...@@ -70,7 +74,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -70,7 +74,7 @@ public final class Mp3Extractor implements Extractor {
private final long forcedFirstSampleTimestampUs; private final long forcedFirstSampleTimestampUs;
private final ParsableByteArray scratch; private final ParsableByteArray scratch;
private final MpegAudioHeader synchronizedHeader; private final MpegAudioHeader synchronizedHeader;
private Metadata metadata; private final GaplessInfoHolder gaplessInfoHolder;
// Extractor outputs. // Extractor outputs.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
...@@ -78,6 +82,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -78,6 +82,7 @@ public final class Mp3Extractor implements Extractor {
private int synchronizedHeaderData; private int synchronizedHeaderData;
private Metadata metadata;
private Seeker seeker; private Seeker seeker;
private long basisTimeUs; private long basisTimeUs;
private long samplesRead; private long samplesRead;
...@@ -100,6 +105,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -100,6 +105,7 @@ public final class Mp3Extractor implements Extractor {
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
scratch = new ParsableByteArray(4); scratch = new ParsableByteArray(4);
synchronizedHeader = new MpegAudioHeader(); synchronizedHeader = new MpegAudioHeader();
gaplessInfoHolder = new GaplessInfoHolder();
basisTimeUs = C.TIME_UNSET; basisTimeUs = C.TIME_UNSET;
} }
...@@ -141,20 +147,13 @@ public final class Mp3Extractor implements Extractor { ...@@ -141,20 +147,13 @@ public final class Mp3Extractor implements Extractor {
if (seeker == null) { if (seeker == null) {
seeker = setupSeeker(input); seeker = setupSeeker(input);
extractorOutput.seekMap(seeker); extractorOutput.seekMap(seeker);
GaplessInfo gaplessInfo = metadata != null ? metadata.getGaplessInfo() : null;
Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
synchronizedHeader.sampleRate, Format.NO_VALUE, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE, gaplessInfoHolder.encoderPadding, null, null, 0, null);
gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE,
null, null, 0, null);
if (metadata != null) { if (metadata != null) {
format = format.copyWithMetadata(metadata); format = format.copyWithMetadata(metadata);
} }
trackOutput.format(format); trackOutput.format(format);
} }
return readSample(input); return readSample(input);
...@@ -211,6 +210,17 @@ public final class Mp3Extractor implements Extractor { ...@@ -211,6 +210,17 @@ public final class Mp3Extractor implements Extractor {
input.resetPeekPosition(); input.resetPeekPosition();
if (input.getPosition() == 0) { if (input.getPosition() == 0) {
metadata = Id3Util.parseId3(input); 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(); peekedId3Bytes = (int) input.getPeekPosition();
if (!sniffing) { if (!sniffing) {
input.skipFully(peekedId3Bytes); input.skipFully(peekedId3Bytes);
...@@ -296,16 +306,13 @@ public final class Mp3Extractor implements Extractor { ...@@ -296,16 +306,13 @@ public final class Mp3Extractor implements Extractor {
} }
if (headerData == XING_HEADER || headerData == INFO_HEADER) { if (headerData == XING_HEADER || headerData == INFO_HEADER) {
seeker = XingSeeker.create(synchronizedHeader, frame, position, length); 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. // If there is a Xing header, read gapless playback metadata at a fixed offset.
input.resetPeekPosition(); input.resetPeekPosition();
input.advancePeekPosition(xingBase + 141); input.advancePeekPosition(xingBase + 141);
input.peekFully(scratch.data, 0, 3); input.peekFully(scratch.data, 0, 3);
scratch.setPosition(0); scratch.setPosition(0);
GaplessInfo gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24()); gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
metadata = metadata != null ?
metadata.withGaplessInfo(gaplessInfo) : new Metadata(null, gaplessInfo);
} }
input.skipFully(synchronizedHeader.frameSize); input.skipFully(synchronizedHeader.frameSize);
} else if (frame.limit() >= 40) { } else if (frame.limit() >= 40) {
......
...@@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.Extractor; ...@@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; 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.GaplessInfoHolder;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
...@@ -311,16 +310,12 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -311,16 +310,12 @@ public final class Mp4Extractor implements Extractor, SeekMap {
long durationUs = C.TIME_UNSET; long durationUs = C.TIME_UNSET;
List<Mp4Track> tracks = new ArrayList<>(); List<Mp4Track> tracks = new ArrayList<>();
long earliestSampleOffset = Long.MAX_VALUE; 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); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
if (udta != null) { if (udta != null) {
Metadata info = AtomParsers.parseUdta(udta, isQuickTime); metadata = AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder);
if (info != null) {
gaplessInfo = info.getGaplessInfo();
metadata = info;
}
} }
for (int i = 0; i < moov.containerChildren.size(); i++) { for (int i = 0; i < moov.containerChildren.size(); i++) {
...@@ -337,10 +332,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -337,10 +332,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
gaplessInfoHolder.gaplessInfo = gaplessInfo;
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
gaplessInfo = gaplessInfoHolder.gaplessInfo;
if (trackSampleTable.sampleCount == 0) { if (trackSampleTable.sampleCount == 0) {
continue; continue;
} }
...@@ -350,8 +342,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -350,8 +342,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
// Allow ten source samples per output sample, like the platform extractor. // Allow ten source samples per output sample, like the platform extractor.
int maxInputSize = trackSampleTable.maximumSize + 3 * 10; int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
Format format = track.format.copyWithMaxInputSize(maxInputSize); Format format = track.format.copyWithMaxInputSize(maxInputSize);
if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfo != null) { if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) {
format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding);
} }
if (metadata != null) { if (metadata != null) {
format = format.copyWithMetadata(metadata); format = format.copyWithMetadata(metadata);
......
...@@ -17,65 +17,79 @@ package com.google.android.exoplayer2.metadata; ...@@ -17,65 +17,79 @@ package com.google.android.exoplayer2.metadata;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; 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.Arrays;
import java.util.Collections;
import java.util.List; 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<Id3Frame> frames; /**
private final GaplessInfo gaplessInfo; * A metadata entry.
*/
public interface Entry extends Parcelable {}
public Metadata(List<Id3Frame> frames, GaplessInfo gaplessInfo) { private final Entry[] entries;
List<Id3Frame> theFrames = frames != null ? new ArrayList<>(frames) : new ArrayList<Id3Frame>();
this.frames = Collections.unmodifiableList(theFrames); /**
this.gaplessInfo = gaplessInfo; * @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(); * @param entries The metadata entries.
int encoderPadding = in.readInt(); */
gaplessInfo = encoderDelay > 0 || encoderPadding > 0 ? public Metadata(List<? extends Entry> entries) {
new GaplessInfo(encoderDelay, encoderPadding) : null; if (entries != null) {
frames = Arrays.asList((Id3Frame[]) in.readArray(Id3Frame.class.getClassLoader())); this.entries = new Entry[entries.size()];
entries.toArray(this.entries);
} else {
this.entries = new Entry[0];
}
} }
public Metadata withGaplessInfo(GaplessInfo info) { /* package */ Metadata(Parcel in) {
return new Metadata(frames, info); entries = new Metadata.Entry[in.readInt()];
for (int i = 0; i < entries.length; i++) {
entries[i] = in.readParcelable(Entry.class.getClassLoader());
}
} }
public List<Id3Frame> 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 @Override
public boolean equals(Object o) { public boolean equals(Object obj) {
if (this == o) return true; if (this == obj) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
Metadata that = (Metadata) o; if (obj == null || getClass() != obj.getClass()) {
return false;
if (!frames.equals(that.frames)) return false; }
return gaplessInfo != null ? gaplessInfo.equals(that.gaplessInfo) : that.gaplessInfo == null; Metadata other = (Metadata) obj;
return Arrays.equals(entries, other.entries);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = frames.hashCode(); return Arrays.hashCode(entries);
result = 31 * result + (gaplessInfo != null ? gaplessInfo.hashCode() : 0);
return result;
} }
@Override @Override
...@@ -85,13 +99,13 @@ public class Metadata implements Parcelable { ...@@ -85,13 +99,13 @@ public class Metadata implements Parcelable {
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderDelay : -1); dest.writeInt(entries.length);
dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderPadding : -1); for (Entry entry : entries) {
dest.writeArray(frames.toArray(new Id3Frame[frames.size()])); dest.writeParcelable(entry, 0);
}
} }
public static final Parcelable.Creator<Metadata> CREATOR = public static final Parcelable.Creator<Metadata> CREATOR = new Parcelable.Creator<Metadata>() {
new Parcelable.Creator<Metadata>() {
@Override @Override
public Metadata createFromParcel(Parcel in) { public Metadata createFromParcel(Parcel in) {
return new Metadata(in); return new Metadata(in);
...@@ -102,4 +116,5 @@ public class Metadata implements Parcelable { ...@@ -102,4 +116,5 @@ public class Metadata implements Parcelable {
return new Metadata[0]; return new Metadata[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<Id3Frame> 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;
}
}
...@@ -17,10 +17,8 @@ package com.google.android.exoplayer2.metadata; ...@@ -17,10 +17,8 @@ package com.google.android.exoplayer2.metadata;
/** /**
* Decodes metadata from binary data. * Decodes metadata from binary data.
*
* @param <T> The type of the metadata.
*/ */
public interface MetadataDecoder<T> { public interface MetadataDecoder {
/** /**
* Checks whether the decoder supports a given mime type. * Checks whether the decoder supports a given mime type.
...@@ -38,6 +36,6 @@ public interface MetadataDecoder<T> { ...@@ -38,6 +36,6 @@ public interface MetadataDecoder<T> {
* @return The decoded metadata object. * @return The decoded metadata object.
* @throws MetadataDecoderException If a problem occurred decoding the data. * @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;
} }
...@@ -30,38 +30,34 @@ import java.nio.ByteBuffer; ...@@ -30,38 +30,34 @@ import java.nio.ByteBuffer;
/** /**
* A renderer for metadata. * A renderer for metadata.
*
* @param <T> The type of the metadata.
*/ */
public final class MetadataRenderer<T> extends BaseRenderer implements Callback { public final class MetadataRenderer extends BaseRenderer implements Callback {
/** /**
* Receives output from a {@link MetadataRenderer}. * Receives output from a {@link MetadataRenderer}.
*
* @param <T> The type of the metadata.
*/ */
public interface Output<T> { public interface Output {
/** /**
* Called each time there is a metadata associated with current playback time. * Called each time there is a metadata associated with current playback time.
* *
* @param metadata The metadata. * @param metadata The metadata.
*/ */
void onMetadata(T metadata); void onMetadata(Metadata metadata);
} }
private static final int MSG_INVOKE_RENDERER = 0; private static final int MSG_INVOKE_RENDERER = 0;
private final MetadataDecoder<T> metadataDecoder; private final MetadataDecoder metadataDecoder;
private final Output<T> output; private final Output output;
private final Handler outputHandler; private final Handler outputHandler;
private final FormatHolder formatHolder; private final FormatHolder formatHolder;
private final DecoderInputBuffer buffer; private final DecoderInputBuffer buffer;
private boolean inputStreamEnded; private boolean inputStreamEnded;
private long pendingMetadataTimestamp; private long pendingMetadataTimestamp;
private T pendingMetadata; private Metadata pendingMetadata;
/** /**
* @param output The output. * @param output The output.
...@@ -72,8 +68,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback ...@@ -72,8 +68,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback
* called directly on the player's internal rendering thread. * called directly on the player's internal rendering thread.
* @param metadataDecoder A decoder for the metadata. * @param metadataDecoder A decoder for the metadata.
*/ */
public MetadataRenderer(Output<T> output, Looper outputLooper, public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) {
MetadataDecoder<T> metadataDecoder) {
super(C.TRACK_TYPE_METADATA); super(C.TRACK_TYPE_METADATA);
this.output = Assertions.checkNotNull(output); this.output = Assertions.checkNotNull(output);
this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
...@@ -137,7 +132,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback ...@@ -137,7 +132,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback
return true; return true;
} }
private void invokeRenderer(T metadata) { private void invokeRenderer(Metadata metadata) {
if (outputHandler != null) { if (outputHandler != null) {
outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
} else { } else {
...@@ -150,13 +145,13 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback ...@@ -150,13 +145,13 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback
public boolean handleMessage(Message msg) { public boolean handleMessage(Message msg) {
switch (msg.what) { switch (msg.what) {
case MSG_INVOKE_RENDERER: case MSG_INVOKE_RENDERER:
invokeRendererInternal((T) msg.obj); invokeRendererInternal((Metadata) msg.obj);
return true; return true;
} }
return false; return false;
} }
private void invokeRendererInternal(T metadata) { private void invokeRendererInternal(Metadata metadata) {
output.onMetadata(metadata); output.onMetadata(metadata);
} }
......
...@@ -15,8 +15,6 @@ ...@@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; 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.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataDecoderException;
...@@ -31,7 +29,7 @@ import java.util.Locale; ...@@ -31,7 +29,7 @@ import java.util.Locale;
/** /**
* Decodes individual TXXX text frames from raw ID3 data. * Decodes individual TXXX text frames from raw ID3 data.
*/ */
public final class Id3Decoder implements MetadataDecoder<Metadata> { public final class Id3Decoder implements MetadataDecoder {
private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; 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_16 = 1;
...@@ -41,7 +39,6 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> { ...@@ -41,7 +39,6 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> {
private int majorVersion; private int majorVersion;
private int minorVersion; private int minorVersion;
private boolean isUnsynchronized; private boolean isUnsynchronized;
private GaplessInfo gaplessInfo;
@Override @Override
public boolean canDecode(String mimeType) { public boolean canDecode(String mimeType) {
...@@ -141,11 +138,7 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> { ...@@ -141,11 +138,7 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> {
frame = decodeTextInformationFrame(frameData, frameSize, id); frame = decodeTextInformationFrame(frameData, frameSize, id);
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' &&
(frameId3 == 'M' || frameId3 == 0)) { (frameId3 == 'M' || frameId3 == 0)) {
CommentFrame commentFrame = decodeCommentFrame(frameData, frameSize); frame = decodeCommentFrame(frameData, frameSize);
frame = commentFrame;
if (gaplessInfo == null) {
gaplessInfo = GaplessInfo.createFromComment(commentFrame.id, commentFrame.text);
}
} else { } else {
String id = frameId3 != 0 ? String id = frameId3 != 0 ?
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
...@@ -159,7 +152,7 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> { ...@@ -159,7 +152,7 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> {
} }
} }
return new Metadata(id3Frames, null); return new Metadata(id3Frames);
} }
private static int indexOfEos(byte[] data, int fromIndex, int encoding) { private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
...@@ -198,7 +191,7 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> { ...@@ -198,7 +191,7 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> {
/** /**
* @param id3Buffer A {@link ParsableByteArray} from which data should be read. * @param id3Buffer A {@link ParsableByteArray} from which data should be read.
* @return The size of ID3 frames in bytes, excluding the header and footer. * @return The 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 { private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException {
int id1 = id3Buffer.readUnsignedByte(); int id1 = id3Buffer.readUnsignedByte();
......
...@@ -16,12 +16,14 @@ ...@@ -16,12 +16,14 @@
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcelable; import android.os.Parcelable;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
/** /**
* Base class for ID3 frames. * Base class for ID3 frames.
*/ */
public abstract class Id3Frame implements Parcelable { public abstract class Id3Frame implements Metadata.Entry {
/** /**
* The frame ID. * The frame ID.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment