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;
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<MappedTrackInfo>, MetadataRenderer.Output<Metadata> {
MappingTrackSelector.EventListener<MappedTrackInfo>, 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<Metadata>
// MetadataRenderer.Output
@Override
public void onMetadata(Metadata metadata) {
List<Id3Frame> 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
......
......@@ -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);
......
......@@ -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<Id3Frame> 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<Id3Frame> 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<Id3Frame> 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);
}
......
......@@ -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,13 +428,9 @@ 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,
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
}
......
......@@ -111,7 +111,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
private SurfaceHolder surfaceHolder;
private TextureView textureView;
private TextRenderer.Output textOutput;
private MetadataRenderer.Output<Metadata> 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<Metadata> 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<Metadata> id3Renderer = new MetadataRenderer<>(componentListener,
MetadataRenderer metadataRenderer = new MetadataRenderer(componentListener,
mainHandler.getLooper(), new Id3Decoder());
renderersList.add(id3Renderer);
renderersList.add(metadataRenderer);
}
private void buildExtensionRenderers(ArrayList<Renderer> renderersList,
......@@ -636,7 +645,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
private final class ComponentListener implements VideoRendererEventListener,
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<Metadata>,
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output,
SurfaceHolder.Callback, TextureView.SurfaceTextureListener,
TrackSelector.EventListener<Object> {
......@@ -768,12 +777,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
}
// MetadataRenderer.Output<Metadata> implementation
// MetadataRenderer.Output implementation
@Override
public void onMetadata(Metadata metadata) {
if (id3Output != null) {
id3Output.onMetadata(metadata);
if (metadataOutput != null) {
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 @@
*/
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;
}
}
......@@ -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);
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) {
......
......@@ -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;
......@@ -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<Metadata.Entry> 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<Metadata.Entry> 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);
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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> 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<Metadata.Entry> builder,
String attributeName, ParsableByteArray ilst, int endPosition) {
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
int key = ilst.readInt();
ilst.skipBytes(4);
......@@ -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<Metadata.Entry> 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<Metadata.Entry> 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[])) {
......
......@@ -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<Mp4Track> 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);
......
......@@ -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<Id3Frame> frames;
private final GaplessInfo gaplessInfo;
/**
* A metadata entry.
*/
public interface Entry extends Parcelable {}
public Metadata(List<Id3Frame> frames, GaplessInfo gaplessInfo) {
List<Id3Frame> theFrames = frames != null ? new ArrayList<>(frames) : new ArrayList<Id3Frame>();
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<? extends Entry> 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<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
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,13 +99,13 @@ 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<Metadata> CREATOR =
new Parcelable.Creator<Metadata>() {
public static final Parcelable.Creator<Metadata> CREATOR = new Parcelable.Creator<Metadata>() {
@Override
public Metadata createFromParcel(Parcel in) {
return new Metadata(in);
......@@ -102,4 +116,5 @@ public class Metadata implements Parcelable {
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;
/**
* 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.
......@@ -38,6 +36,6 @@ public interface MetadataDecoder<T> {
* @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;
}
......@@ -30,38 +30,34 @@ import java.nio.ByteBuffer;
/**
* 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}.
*
* @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.
*
* @param metadata The metadata.
*/
void onMetadata(T metadata);
void onMetadata(Metadata metadata);
}
private static final int MSG_INVOKE_RENDERER = 0;
private final MetadataDecoder<T> metadataDecoder;
private final Output<T> 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<T> extends BaseRenderer implements Callback
* called directly on the player's internal rendering thread.
* @param metadataDecoder A decoder for the metadata.
*/
public MetadataRenderer(Output<T> output, Looper outputLooper,
MetadataDecoder<T> 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<T> 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<T> 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);
}
......
......@@ -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<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_UTF_16 = 1;
......@@ -41,7 +39,6 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> {
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<Metadata> {
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<Metadata> {
}
}
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<Metadata> {
/**
* @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();
......
......@@ -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.
......
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