Merge branch 'dev-v2-id3' into dev-v2

This commit is contained in:
Oliver Woodman 2016-10-27 12:37:22 +01:00
commit 8a89abcbf1
25 changed files with 1445 additions and 517 deletions

View File

@ -27,8 +27,10 @@ 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.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;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.GeobFrame; import com.google.android.exoplayer2.metadata.id3.GeobFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.PrivFrame;
@ -46,7 +48,6 @@ 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;
/** /**
@ -55,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,
MetadataRenderer.Output<List<Id3Frame>> { 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;
@ -157,6 +158,18 @@ import java.util.Locale;
} }
Log.d(TAG, " ]"); Log.d(TAG, " ]");
} }
// Log metadata for at most one of the tracks selected for the renderer.
if (trackSelection != null) {
for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) {
Metadata metadata = trackSelection.getFormat(selectionIndex).metadata;
if (metadata != null) {
Log.d(TAG, " Metadata [");
printMetadata(metadata, " ");
Log.d(TAG, " ]");
break;
}
}
}
Log.d(TAG, " ]"); Log.d(TAG, " ]");
} }
} }
@ -182,34 +195,13 @@ import java.util.Locale;
Log.d(TAG, "]"); Log.d(TAG, "]");
} }
// MetadataRenderer.Output<List<Id3Frame>> // MetadataRenderer.Output
@Override @Override
public void onMetadata(List<Id3Frame> id3Frames) { public void onMetadata(Metadata metadata) {
for (Id3Frame id3Frame : id3Frames) { Log.d(TAG, "onMetadata [");
if (id3Frame instanceof TxxxFrame) { printMetadata(metadata, " ");
TxxxFrame txxxFrame = (TxxxFrame) id3Frame; Log.d(TAG, "]");
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;
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner));
} else if (id3Frame instanceof GeobFrame) {
GeobFrame geobFrame = (GeobFrame) id3Frame;
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;
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;
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id,
textInformationFrame.description));
} else {
Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id));
}
}
} }
// AudioRendererEventListener // AudioRendererEventListener
@ -354,6 +346,39 @@ import java.util.Locale;
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
} }
private void printMetadata(Metadata metadata, String prefix) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof TxxxFrame) {
TxxxFrame txxxFrame = (TxxxFrame) entry;
Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id,
txxxFrame.description, txxxFrame.value));
} else if (entry instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) entry;
Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner));
} else if (entry instanceof GeobFrame) {
GeobFrame geobFrame = (GeobFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s",
geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description));
} else if (entry instanceof ApicFrame) {
ApicFrame apicFrame = (ApicFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description));
} else if (entry instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
Log.d(TAG, prefix + String.format("%s: description=%s", textInformationFrame.id,
textInformationFrame.description));
} else if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id,
commentFrame.language, commentFrame.description, commentFrame.text));
} else if (entry instanceof Id3Frame) {
Id3Frame id3Frame = (Id3Frame) entry;
Log.d(TAG, prefix + String.format("%s", id3Frame.id));
}
}
}
private String getSessionTimeString() { private String getSessionTimeString() {
return getTimeString(SystemClock.elapsedRealtime() - startTimeMs); return getTimeString(SystemClock.elapsedRealtime() - startTimeMs);
} }

View File

@ -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 {
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);

View File

@ -16,8 +16,8 @@
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.test.MoreAsserts; import android.test.MoreAsserts;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import java.util.List;
import junit.framework.TestCase; import junit.framework.TestCase;
/** /**
@ -30,9 +30,9 @@ public class Id3DecoderTest extends TestCase {
3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54, 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54,
54, 52, 95, 115, 116, 97, 114, 116, 0}; 54, 52, 95, 115, 116, 97, 114, 116, 0};
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size()); assertEquals(1, metadata.length());
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0); TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0);
assertEquals("", txxxFrame.description); assertEquals("", txxxFrame.description);
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value); assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
} }
@ -42,9 +42,9 @@ public class Id3DecoderTest extends TestCase {
3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87, 3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87,
111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size()); assertEquals(1, metadata.length());
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0); ApicFrame apicFrame = (ApicFrame) metadata.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);
@ -56,9 +56,9 @@ public class Id3DecoderTest extends TestCase {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0, byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0,
3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}; 3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0};
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size()); assertEquals(1, metadata.length());
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("TIT2", textInformationFrame.id); assertEquals("TIT2", textInformationFrame.id);
assertEquals("Hello World", textInformationFrame.description); assertEquals("Hello World", textInformationFrame.description);
} }

View File

@ -21,6 +21,7 @@ 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.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;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -57,6 +58,10 @@ public final class Format implements Parcelable {
* Codecs of the format as described in RFC 6381, or null if unknown or not applicable. * Codecs of the format as described in RFC 6381, or null if unknown or not applicable.
*/ */
public final String codecs; public final String codecs;
/**
* Metadata, or null if unknown or not applicable.
*/
public final Metadata metadata;
// Container specific. // Container specific.
@ -185,7 +190,7 @@ public final class Format implements Parcelable {
float frameRate, List<byte[]> initializationData) { float frameRate, List<byte[]> initializationData) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, null); NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, null, null);
} }
public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
@ -211,7 +216,7 @@ public final class Format implements Parcelable {
return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height, return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height,
frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData,
drmInitData); drmInitData, null);
} }
// Audio. // Audio.
@ -222,7 +227,7 @@ public final class Format implements Parcelable {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, NO_VALUE,
NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, initializationData, NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, initializationData,
null); null, null);
} }
public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
@ -239,18 +244,18 @@ public final class Format implements Parcelable {
@C.SelectionFlags int selectionFlags, String language) { @C.SelectionFlags int selectionFlags, String language) {
return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData, sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData,
selectionFlags, language); selectionFlags, language, null);
} }
public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, int maxInputSize, int channelCount, int sampleRate, int bitrate, int maxInputSize, int channelCount, int sampleRate,
@C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding, @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding,
List<byte[]> initializationData, DrmInitData drmInitData, List<byte[]> initializationData, DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags, String language) { @C.SelectionFlags int selectionFlags, String language, Metadata metadata) {
return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding,
encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE,
initializationData, drmInitData); initializationData, drmInitData, metadata);
} }
// Text. // Text.
@ -260,7 +265,7 @@ public final class Format implements Parcelable {
String language) { String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, null, null); NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, null, null, null);
} }
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
@ -274,7 +279,7 @@ public final class Format implements Parcelable {
long subsampleOffsetUs) { long subsampleOffsetUs) {
return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, selectionFlags, language, subsampleOffsetUs, null, drmInitData); NO_VALUE, selectionFlags, language, subsampleOffsetUs, null, drmInitData, null);
} }
// Image. // Image.
@ -283,7 +288,7 @@ public final class Format implements Parcelable {
int bitrate, List<byte[]> initializationData, String language, DrmInitData drmInitData) { int bitrate, List<byte[]> initializationData, String language, DrmInitData drmInitData) {
return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, 0, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData); NO_VALUE, 0, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null);
} }
// Generic. // Generic.
@ -292,14 +297,14 @@ public final class Format implements Parcelable {
String sampleMimeType, int bitrate) { String sampleMimeType, int bitrate) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, null); NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, null, null);
} }
public static Format createSampleFormat(String id, String sampleMimeType, String codecs, public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, DrmInitData drmInitData) { int bitrate, DrmInitData drmInitData) {
return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, drmInitData); NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null);
} }
/* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs, /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs,
@ -307,7 +312,8 @@ public final class Format implements Parcelable {
float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode, float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode,
int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay,
int encoderPadding, @C.SelectionFlags int selectionFlags, String language, int encoderPadding, @C.SelectionFlags int selectionFlags, String language,
long subsampleOffsetUs, List<byte[]> initializationData, DrmInitData drmInitData) { long subsampleOffsetUs, List<byte[]> initializationData, DrmInitData drmInitData,
Metadata metadata) {
this.id = id; this.id = id;
this.containerMimeType = containerMimeType; this.containerMimeType = containerMimeType;
this.sampleMimeType = sampleMimeType; this.sampleMimeType = sampleMimeType;
@ -332,6 +338,7 @@ public final class Format implements Parcelable {
this.initializationData = initializationData == null ? Collections.<byte[]>emptyList() this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
: initializationData; : initializationData;
this.drmInitData = drmInitData; this.drmInitData = drmInitData;
this.metadata = metadata;
} }
@SuppressWarnings("ResourceType") @SuppressWarnings("ResourceType")
@ -364,20 +371,21 @@ public final class Format implements Parcelable {
initializationData.add(in.createByteArray()); initializationData.add(in.createByteArray());
} }
drmInitData = in.readParcelable(DrmInitData.class.getClassLoader()); drmInitData = in.readParcelable(DrmInitData.class.getClassLoader());
metadata = in.readParcelable(Metadata.class.getClassLoader());
} }
public Format copyWithMaxInputSize(int maxInputSize) { public Format copyWithMaxInputSize(int maxInputSize) {
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, encoderDelay, encoderPadding, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
} }
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
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, encoderDelay, encoderPadding, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
} }
public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height, public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height,
@ -385,7 +393,7 @@ public final class Format implements Parcelable {
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, encoderDelay, encoderPadding, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
} }
public Format copyWithManifestFormatInfo(Format manifestFormat, public Format copyWithManifestFormatInfo(Format manifestFormat,
@ -401,21 +409,28 @@ 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); language, subsampleOffsetUs, initializationData, drmInitData, metadata);
} }
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { public Format copyWithGaplessInfo(int encoderDelay, int 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, encoderDelay, encoderPadding, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
} }
public Format copyWithDrmInitData(DrmInitData drmInitData) { public Format copyWithDrmInitData(DrmInitData drmInitData) {
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, encoderDelay, encoderPadding, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
}
public Format copyWithMetadata(Metadata metadata) {
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, metadata);
} }
/** /**
@ -475,6 +490,7 @@ public final class Format implements Parcelable {
result = 31 * result + sampleRate; result = 31 * result + sampleRate;
result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + (language == null ? 0 : language.hashCode());
result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode()); result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode());
result = 31 * result + (metadata == null ? 0 : metadata.hashCode());
hashCode = result; hashCode = result;
} }
return hashCode; return hashCode;
@ -502,6 +518,7 @@ public final class Format implements Parcelable {
|| !Util.areEqual(sampleMimeType, other.sampleMimeType) || !Util.areEqual(sampleMimeType, other.sampleMimeType)
|| !Util.areEqual(codecs, other.codecs) || !Util.areEqual(codecs, other.codecs)
|| !Util.areEqual(drmInitData, other.drmInitData) || !Util.areEqual(drmInitData, other.drmInitData)
|| !Util.areEqual(metadata, other.metadata)
|| !Arrays.equals(projectionData, other.projectionData) || !Arrays.equals(projectionData, other.projectionData)
|| initializationData.size() != other.initializationData.size()) { || initializationData.size() != other.initializationData.size()) {
return false; return false;
@ -574,6 +591,7 @@ public final class Format implements Parcelable {
dest.writeByteArray(initializationData.get(i)); dest.writeByteArray(initializationData.get(i));
} }
dest.writeParcelable(drmInitData, 0); dest.writeParcelable(drmInitData, 0);
dest.writeParcelable(metadata, 0);
} }
/** /**

View File

@ -35,9 +35,9 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
@ -107,7 +107,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<List<Id3Frame>> id3Output; private MetadataRenderer.Output metadataOutput;
private VideoListener videoListener; private VideoListener videoListener;
private AudioRendererEventListener audioDebugListener; private AudioRendererEventListener audioDebugListener;
private VideoRendererEventListener videoDebugListener; private VideoRendererEventListener videoDebugListener;
@ -364,12 +364,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<List<Id3Frame>> output) { public void setMetadataOutput(MetadataRenderer.Output output) {
id3Output = output; metadataOutput = output;
} }
// ExoPlayer implementation // ExoPlayer implementation
@ -540,9 +549,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<List<Id3Frame>> 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,
@ -644,7 +653,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
} }
private final class ComponentListener implements VideoRendererEventListener, private final class ComponentListener implements VideoRendererEventListener,
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<List<Id3Frame>>, AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output,
SurfaceHolder.Callback, TextureView.SurfaceTextureListener { SurfaceHolder.Callback, TextureView.SurfaceTextureListener {
// VideoRendererEventListener implementation // VideoRendererEventListener implementation
@ -775,12 +784,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
} }
} }
// MetadataRenderer.Output<List<Id3Frame>> implementation // MetadataRenderer.Output implementation
@Override @Override
public void onMetadata(List<Id3Frame> id3Frames) { public void onMetadata(Metadata metadata) {
if (id3Output != null) { if (metadataOutput != null) {
id3Output.onMetadata(id3Frames); metadataOutput.onMetadata(metadata);
} }
} }

View File

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -65,6 +67,25 @@ public final class GaplessInfoHolder {
return false; return false;
} }
/**
* Populates the holder with data parsed from ID3 {@link Metadata}.
*
* @param metadata The metadata from which to parse the gapless information.
* @return Whether the holder was populated.
*/
public boolean setFromMetadata(Metadata metadata) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
if (setFromComment(commentFrame.description, commentFrame.text)) {
return true;
}
}
}
return false;
}
/** /**
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header * 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. * or MPEG 4 user data), if valid and non-zero.
@ -73,7 +94,7 @@ public final class GaplessInfoHolder {
* @param data The comment's payload data. * @param data The comment's payload data.
* @return Whether the holder was populated. * @return Whether the holder was populated.
*/ */
public boolean setFromComment(String name, String data) { private boolean setFromComment(String name, String data) {
if (!GAPLESS_COMMENT_ID.equals(name)) { if (!GAPLESS_COMMENT_ID.equals(name)) {
return false; return false;
} }

View File

@ -1,293 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.mp3;
import android.util.Pair;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.charset.Charset;
/**
* Utility for parsing ID3 version 2 metadata in MP3 files.
*/
/* package */ final class Id3Util {
/**
* The maximum valid length for metadata in bytes.
*/
private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024;
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
private static final Charset[] CHARSET_BY_ENCODING = new Charset[] {Charset.forName("ISO-8859-1"),
Charset.forName("UTF-16LE"), Charset.forName("UTF-16BE"), Charset.forName("UTF-8")};
/**
* Peeks data from the input and parses ID3 metadata.
*
* @param input The {@link ExtractorInput} from which data should be peeked.
* @param out The {@link GaplessInfoHolder} to populate.
* @throws IOException If an error occurred peeking from the input.
* @throws InterruptedException If the thread was interrupted.
*/
public static void parseId3(ExtractorInput input, GaplessInfoHolder out)
throws IOException, InterruptedException {
ParsableByteArray scratch = new ParsableByteArray(10);
int peekedId3Bytes = 0;
while (true) {
input.peekFully(scratch.data, 0, 10);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != ID3_TAG) {
break;
}
int majorVersion = scratch.readUnsignedByte();
int minorVersion = scratch.readUnsignedByte();
int flags = scratch.readUnsignedByte();
int length = scratch.readSynchSafeInt();
if (!out.hasGaplessInfo() && canParseMetadata(majorVersion, minorVersion, flags, length)) {
byte[] frame = new byte[length];
input.peekFully(frame, 0, length);
parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags, out);
} else {
input.advancePeekPosition(length);
}
peekedId3Bytes += 10 + length;
}
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes);
}
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
int length) {
return minorVersion != 0xFF && majorVersion >= 2 && majorVersion <= 4
&& length <= MAXIMUM_METADATA_SIZE
&& !(majorVersion == 2 && ((flags & 0x3F) != 0 || (flags & 0x40) != 0))
&& !(majorVersion == 3 && (flags & 0x1F) != 0)
&& !(majorVersion == 4 && (flags & 0x0F) != 0);
}
private static void parseGaplessInfo(ParsableByteArray frame, int version, int flags,
GaplessInfoHolder out) {
unescape(frame, version, flags);
// Skip any extended header.
frame.setPosition(0);
if (version == 3 && (flags & 0x40) != 0) {
if (frame.bytesLeft() < 4) {
return;
}
int extendedHeaderSize = frame.readUnsignedIntToInt();
if (extendedHeaderSize > frame.bytesLeft()) {
return;
}
int paddingSize;
if (extendedHeaderSize >= 6) {
frame.skipBytes(2); // extended flags
paddingSize = frame.readUnsignedIntToInt();
frame.setPosition(4);
frame.setLimit(frame.limit() - paddingSize);
if (frame.bytesLeft() < extendedHeaderSize) {
return;
}
}
frame.skipBytes(extendedHeaderSize);
} else if (version == 4 && (flags & 0x40) != 0) {
if (frame.bytesLeft() < 4) {
return;
}
int extendedHeaderSize = frame.readSynchSafeInt();
if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) {
return;
}
frame.setPosition(extendedHeaderSize);
}
// Extract gapless playback metadata stored in comments.
Pair<String, String> comment;
while ((comment = findNextComment(version, frame)) != null) {
if (comment.first.length() > 3) {
if (out.setFromComment(comment.first.substring(3), comment.second)) {
break;
}
}
}
}
private static Pair<String, String> findNextComment(int majorVersion, ParsableByteArray data) {
int frameSize;
while (true) {
if (majorVersion == 2) {
if (data.bytesLeft() < 6) {
return null;
}
String id = data.readString(3, Charset.forName("US-ASCII"));
if (id.equals("\0\0\0")) {
return null;
}
frameSize = data.readUnsignedInt24();
if (frameSize == 0 || frameSize > data.bytesLeft()) {
return null;
}
if (id.equals("COM")) {
break;
}
} else /* major == 3 || major == 4 */ {
if (data.bytesLeft() < 10) {
return null;
}
String id = data.readString(4, Charset.forName("US-ASCII"));
if (id.equals("\0\0\0\0")) {
return null;
}
frameSize = majorVersion == 4 ? data.readSynchSafeInt() : data.readUnsignedIntToInt();
if (frameSize == 0 || frameSize > data.bytesLeft() - 2) {
return null;
}
int flags = data.readUnsignedShort();
boolean compressedOrEncrypted = (majorVersion == 4 && (flags & 0x0C) != 0)
|| (majorVersion == 3 && (flags & 0xC0) != 0);
if (!compressedOrEncrypted && id.equals("COMM")) {
break;
}
}
data.skipBytes(frameSize);
}
// The comment tag is at the reading position in data.
int encoding = data.readUnsignedByte();
if (encoding < 0 || encoding >= CHARSET_BY_ENCODING.length) {
return null;
}
Charset charset = CHARSET_BY_ENCODING[encoding];
String[] commentFields = data.readString(frameSize - 1, charset).split("\0");
return commentFields.length == 2 ? Pair.create(commentFields[0], commentFields[1]) : null;
}
private static boolean unescape(ParsableByteArray frame, int version, int flags) {
if (version != 4) {
if ((flags & 0x80) != 0) {
// Remove unsynchronization on ID3 version < 2.4.0.
byte[] bytes = frame.data;
int newLength = bytes.length;
for (int i = 0; i + 1 < newLength; i++) {
if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {
System.arraycopy(bytes, i + 2, bytes, i + 1, newLength - i - 2);
newLength--;
}
}
frame.setLimit(newLength);
}
} else {
// Remove unsynchronization on ID3 version 2.4.0.
if (canUnescapeVersion4(frame, false)) {
unescapeVersion4(frame, false);
} else if (canUnescapeVersion4(frame, true)) {
unescapeVersion4(frame, true);
} else {
return false;
}
}
return true;
}
private static boolean canUnescapeVersion4(ParsableByteArray frame,
boolean unsignedIntDataSizeHack) {
frame.setPosition(0);
while (frame.bytesLeft() >= 10) {
if (frame.readInt() == 0) {
return true;
}
long dataSize = frame.readUnsignedInt();
if (!unsignedIntDataSizeHack) {
// Parse the data size as a syncsafe integer.
if ((dataSize & 0x808080L) != 0) {
return false;
}
dataSize = (dataSize & 0x7F) | (((dataSize >> 8) & 0x7F) << 7)
| (((dataSize >> 16) & 0x7F) << 14) | (((dataSize >> 24) & 0x7F) << 21);
}
if (dataSize > frame.bytesLeft() - 2) {
return false;
}
int flags = frame.readUnsignedShort();
if ((flags & 1) != 0) {
if (frame.bytesLeft() < 4) {
return false;
}
}
frame.skipBytes((int) dataSize);
}
return true;
}
private static void unescapeVersion4(ParsableByteArray frame, boolean unsignedIntDataSizeHack) {
frame.setPosition(0);
byte[] bytes = frame.data;
while (frame.bytesLeft() >= 10) {
if (frame.readInt() == 0) {
return;
}
int dataSize =
unsignedIntDataSizeHack ? frame.readUnsignedIntToInt() : frame.readSynchSafeInt();
int flags = frame.readUnsignedShort();
int previousFlags = flags;
if ((flags & 1) != 0) {
// Strip data length indicator.
int offset = frame.getPosition();
System.arraycopy(bytes, offset + 4, bytes, offset, frame.bytesLeft() - 4);
dataSize -= 4;
flags &= ~1;
frame.setLimit(frame.limit() - 4);
}
if ((flags & 2) != 0) {
// Unescape 0xFF00 to 0xFF in the next dataSize bytes.
int readOffset = frame.getPosition() + 1;
int writeOffset = readOffset;
for (int i = 0; i + 1 < dataSize; i++) {
if ((bytes[readOffset - 1] & 0xFF) == 0xFF && bytes[readOffset] == 0) {
readOffset++;
dataSize--;
}
bytes[writeOffset++] = bytes[readOffset++];
}
frame.setLimit(frame.limit() - (readOffset - writeOffset));
System.arraycopy(bytes, readOffset, bytes, writeOffset, frame.bytesLeft() - readOffset);
flags &= ~2;
}
if (flags != previousFlags || unsignedIntDataSizeHack) {
int dataSizeOffset = frame.getPosition() - 6;
writeSyncSafeInteger(bytes, dataSizeOffset, dataSize);
bytes[dataSizeOffset + 4] = (byte) (flags >> 8);
bytes[dataSizeOffset + 5] = (byte) (flags & 0xFF);
}
frame.skipBytes(dataSize);
}
}
private static void writeSyncSafeInteger(byte[] bytes, int offset, int value) {
bytes[offset] = (byte) ((value >> 21) & 0x7F);
bytes[offset + 1] = (byte) ((value >> 14) & 0x7F);
bytes[offset + 2] = (byte) ((value >> 7) & 0x7F);
bytes[offset + 3] = (byte) (value & 0x7F);
}
private Id3Util() {}
}

View File

@ -27,6 +27,8 @@ 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.id3.Id3Decoder;
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 java.io.EOFException; import java.io.EOFException;
@ -57,6 +59,10 @@ public final class Mp3Extractor implements Extractor {
* The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
*/ */
private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
/**
* Maximum length of data read into {@link #scratch}.
*/
private static final int SCRATCH_LENGTH = 10;
/** /**
* Mask that includes the audio header values that must match between frames. * Mask that includes the audio header values that must match between frames.
@ -77,6 +83,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;
@ -97,7 +104,7 @@ public final class Mp3Extractor implements Extractor {
*/ */
public Mp3Extractor(long forcedFirstSampleTimestampUs) { public Mp3Extractor(long forcedFirstSampleTimestampUs) {
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
scratch = new ParsableByteArray(4); scratch = new ParsableByteArray(SCRATCH_LENGTH);
synchronizedHeader = new MpegAudioHeader(); synchronizedHeader = new MpegAudioHeader();
gaplessInfoHolder = new GaplessInfoHolder(); gaplessInfoHolder = new GaplessInfoHolder();
basisTimeUs = C.TIME_UNSET; basisTimeUs = C.TIME_UNSET;
@ -144,7 +151,7 @@ public final class Mp3Extractor implements Extractor {
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding, null, null, 0, null)); gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata));
} }
return readSample(input); return readSample(input);
} }
@ -199,7 +206,7 @@ public final class Mp3Extractor implements Extractor {
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES; int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
input.resetPeekPosition(); input.resetPeekPosition();
if (input.getPosition() == 0) { if (input.getPosition() == 0) {
Id3Util.parseId3(input, gaplessInfoHolder); peekId3Data(input);
peekedId3Bytes = (int) input.getPeekPosition(); peekedId3Bytes = (int) input.getPeekPosition();
if (!sniffing) { if (!sniffing) {
input.skipFully(peekedId3Bytes); input.skipFully(peekedId3Bytes);
@ -253,6 +260,45 @@ public final class Mp3Extractor implements Extractor {
return true; return true;
} }
/**
* Peeks ID3 data from the input, including gapless playback information.
*
* @param input The {@link ExtractorInput} from which data should be peeked.
* @throws IOException If an error occurred peeking from the input.
* @throws InterruptedException If the thread was interrupted.
*/
private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
int peekedId3Bytes = 0;
while (true) {
input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {
// Not an ID3 tag.
break;
}
scratch.skipBytes(3); // Skip major version, minor version and flags.
int framesLength = scratch.readSynchSafeInt();
int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength;
if (metadata == null) {
byte[] id3Data = new byte[tagLength];
System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
metadata = new Id3Decoder().decode(id3Data, tagLength);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
}
} else {
input.advancePeekPosition(framesLength);
}
peekedId3Bytes += tagLength;
}
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes);
}
/** /**
* Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide
* data from the start of the first frame in the stream. On returning, the input's position will * data from the start of the first frame in the stream. On returning, the input's position will

View File

@ -132,7 +132,6 @@ import java.util.List;
public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08"); public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08");
public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09");
public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC");
public static final int TYPE_DASHES = Util.getIntegerCodeForString("----");
public final int type; public final int type;
@ -299,7 +298,7 @@ import java.util.List;
* @return The corresponding four character string. * @return The corresponding four character string.
*/ */
public static String getAtomTypeString(int type) { public static String getAtomTypeString(int type) {
return "" + (char) (type >> 24) return "" + (char) ((type >> 24) & 0xFF)
+ (char) ((type >> 16) & 0xFF) + (char) ((type >> 16) & 0xFF)
+ (char) ((type >> 8) & 0xFF) + (char) ((type >> 8) & 0xFF)
+ (char) (type & 0xFF); + (char) (type & 0xFF);

View File

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
@ -30,6 +31,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.AvcConfig; import com.google.android.exoplayer2.video.AvcConfig;
import com.google.android.exoplayer2.video.HevcConfig; import com.google.android.exoplayer2.video.HevcConfig;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -400,80 +402,54 @@ import java.util.List;
* *
* @param udtaAtom The udta (user data) atom to decode. * @param udtaAtom The udta (user data) atom to decode.
* @param isQuickTime True for QuickTime media. False otherwise. * @param isQuickTime True for QuickTime media. False otherwise.
* @param out {@link GaplessInfoHolder} to populate with gapless playback information. * @return Parsed metadata, or null.
*/ */
public static void parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, GaplessInfoHolder out) { public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
if (isQuickTime) { if (isQuickTime) {
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
// decode one. // decode one.
return; return null;
} }
ParsableByteArray udtaData = udtaAtom.data; ParsableByteArray udtaData = udtaAtom.data;
udtaData.setPosition(Atom.HEADER_SIZE); udtaData.setPosition(Atom.HEADER_SIZE);
while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) { while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) {
int atomPosition = udtaData.getPosition();
int atomSize = udtaData.readInt(); int atomSize = udtaData.readInt();
int atomType = udtaData.readInt(); int atomType = udtaData.readInt();
if (atomType == Atom.TYPE_meta) { if (atomType == Atom.TYPE_meta) {
udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setPosition(atomPosition);
udtaData.setLimit(udtaData.getPosition() + atomSize); return parseMetaAtom(udtaData, atomPosition + atomSize);
parseMetaAtom(udtaData, out);
break;
} }
udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
} }
return null;
} }
private static void parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) { private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) {
data.skipBytes(Atom.FULL_HEADER_SIZE); meta.skipBytes(Atom.FULL_HEADER_SIZE);
ParsableByteArray ilst = new ParsableByteArray(); while (meta.getPosition() < limit) {
while (data.bytesLeft() >= Atom.HEADER_SIZE) { int atomPosition = meta.getPosition();
int payloadSize = data.readInt() - Atom.HEADER_SIZE; int atomSize = meta.readInt();
int atomType = data.readInt(); int atomType = meta.readInt();
if (atomType == Atom.TYPE_ilst) { if (atomType == Atom.TYPE_ilst) {
ilst.reset(data.data, data.getPosition() + payloadSize); meta.setPosition(atomPosition);
ilst.setPosition(data.getPosition()); return parseIlst(meta, atomPosition + atomSize);
parseIlst(ilst, out);
if (out.hasGaplessInfo()) {
return;
}
} }
data.skipBytes(payloadSize); meta.skipBytes(atomSize - Atom.HEADER_SIZE);
} }
return null;
} }
private static void parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) { private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
while (ilst.bytesLeft() > 0) { ilst.skipBytes(Atom.HEADER_SIZE);
int position = ilst.getPosition(); ArrayList<Metadata.Entry> entries = new ArrayList<>();
int endPosition = position + ilst.readInt(); while (ilst.getPosition() < limit) {
int type = ilst.readInt(); Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst);
if (type == Atom.TYPE_DASHES) { if (entry != null) {
String lastCommentMean = null; entries.add(entry);
String lastCommentName = null;
String lastCommentData = null;
while (ilst.getPosition() < endPosition) {
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
int key = ilst.readInt();
ilst.skipBytes(4);
if (key == Atom.TYPE_mean) {
lastCommentMean = ilst.readString(length);
} else if (key == Atom.TYPE_name) {
lastCommentName = ilst.readString(length);
} else if (key == Atom.TYPE_data) {
ilst.skipBytes(4);
lastCommentData = ilst.readString(length - 4);
} else {
ilst.skipBytes(length);
}
}
if (lastCommentName != null && lastCommentData != null
&& "com.apple.iTunes".equals(lastCommentMean)) {
out.setFromComment(lastCommentName, lastCommentData);
break;
}
} else {
ilst.setPosition(endPosition);
} }
} }
return entries.isEmpty() ? null : new Metadata(entries);
} }
/** /**
@ -484,12 +460,9 @@ import java.util.List;
*/ */
private static long parseMvhd(ParsableByteArray mvhd) { private static long parseMvhd(ParsableByteArray mvhd) {
mvhd.setPosition(Atom.HEADER_SIZE); mvhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = mvhd.readInt(); int fullAtom = mvhd.readInt();
int version = Atom.parseFullAtomVersion(fullAtom); int version = Atom.parseFullAtomVersion(fullAtom);
mvhd.skipBytes(version == 0 ? 8 : 16); mvhd.skipBytes(version == 0 ? 8 : 16);
return mvhd.readUnsignedInt(); return mvhd.readUnsignedInt();
} }

View File

@ -0,0 +1,323 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.mp4;
import android.util.Log;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
/**
* Parses metadata items stored in ilst atoms.
*/
/* package */ final class MetadataUtil {
private static final String TAG = "MetadataUtil";
// Codes that start with the copyright character (omitted) and have equivalent ID3 frames.
private static final int SHORT_TYPE_NAME_1 = Util.getIntegerCodeForString("nam");
private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk");
private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt");
private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day");
private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART");
private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too");
private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb");
private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com");
private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt");
private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr");
private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen");
// Codes that have equivalent ID3 frames.
private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr");
private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre");
private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp");
private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk");
private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn");
private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo");
private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil");
private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART");
private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm");
private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal");
private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar");
private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa");
private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco");
// Types that do not have equivalent ID3 frames.
private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng");
private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap");
private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn");
private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh");
// Type for items that are intended for internal use by the player.
private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----");
// Standard genres.
private static final String[] STANDARD_GENRES = new String[] {
// These are the official ID3v1 genres.
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap",
"Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
"Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient",
"Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical",
"Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise",
"AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative",
"Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave",
"Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream",
"Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap",
"Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave",
"Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal",
"Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll",
"Hard Rock",
// These were made up by the authors of Winamp but backported into the ID3 spec.
"Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion",
"Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
"Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock",
"Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour",
"Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony",
"Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club",
"Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul",
"Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House",
"Dance Hall",
// These were also invented by the Winamp folks but ignored by the ID3 authors.
"Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie",
"BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap",
"Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian",
"Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop",
"Synthpop"
};
private static final String LANGUAGE_UNDEFINED = "und";
private MetadataUtil() {}
/**
* Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting
* from the current position of the {@link ParsableByteArray}, and the position is advanced by
* the size of the element. The position is advanced even if the element's type is unrecognized.
*
* @param ilst Holds the data to be parsed.
* @return The parsed element, or null if the element's type was not recognized.
*/
public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
int position = ilst.getPosition();
int endPosition = position + ilst.readInt();
int type = ilst.readInt();
int typeTopByte = (type >> 24) & 0xFF;
try {
if (typeTopByte == '\u00A9' /* Copyright char */
|| typeTopByte == '\uFFFD' /* Replacement char */) {
int shortType = type & 0x00FFFFFF;
if (shortType == SHORT_TYPE_COMMENT) {
return parseCommentAttribute(type, ilst);
} else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) {
return parseTextAttribute(type, "TIT2", ilst);
} else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) {
return parseTextAttribute(type, "TCOM", ilst);
} else if (shortType == SHORT_TYPE_YEAR) {
return parseTextAttribute(type, "TDRC", ilst);
} else if (shortType == SHORT_TYPE_ARTIST) {
return parseTextAttribute(type, "TPE1", ilst);
} else if (shortType == SHORT_TYPE_ENCODER) {
return parseTextAttribute(type, "TSSE", ilst);
} else if (shortType == SHORT_TYPE_ALBUM) {
return parseTextAttribute(type, "TALB", ilst);
} else if (shortType == SHORT_TYPE_LYRICS) {
return parseTextAttribute(type, "USLT", ilst);
} else if (shortType == SHORT_TYPE_GENRE) {
return parseTextAttribute(type, "TCON", ilst);
} else if (shortType == TYPE_GROUPING) {
return parseTextAttribute(type, "TIT1", ilst);
}
} else if (type == TYPE_GENRE) {
return parseStandardGenreAttribute(ilst);
} else if (type == TYPE_DISK_NUMBER) {
return parseIndexAndCountAttribute(type, "TPOS", ilst);
} else if (type == TYPE_TRACK_NUMBER) {
return parseIndexAndCountAttribute(type, "TRCK", ilst);
} else if (type == TYPE_TEMPO) {
return parseUint8Attribute(type, "TBPM", ilst, true, false);
} else if (type == TYPE_COMPILATION) {
return parseUint8Attribute(type, "TCMP", ilst, true, true);
} else if (type == TYPE_COVER_ART) {
return parseCoverArt(ilst);
} else if (type == TYPE_ALBUM_ARTIST) {
return parseTextAttribute(type, "TPE2", ilst);
} else if (type == TYPE_SORT_TRACK_NAME) {
return parseTextAttribute(type, "TSOT", ilst);
} else if (type == TYPE_SORT_ALBUM) {
return parseTextAttribute(type, "TSO2", ilst);
} else if (type == TYPE_SORT_ARTIST) {
return parseTextAttribute(type, "TSOA", ilst);
} else if (type == TYPE_SORT_ALBUM_ARTIST) {
return parseTextAttribute(type, "TSOP", ilst);
} else if (type == TYPE_SORT_COMPOSER) {
return parseTextAttribute(type, "TSOC", ilst);
} else if (type == TYPE_RATING) {
return parseUint8Attribute(type, "ITUNESADVISORY", ilst, false, false);
} else if (type == TYPE_GAPLESS_ALBUM) {
return parseUint8Attribute(type, "ITUNESGAPLESS", ilst, false, true);
} else if (type == TYPE_TV_SORT_SHOW) {
return parseTextAttribute(type, "TVSHOWSORT", ilst);
} else if (type == TYPE_TV_SHOW) {
return parseTextAttribute(type, "TVSHOW", ilst);
} else if (type == TYPE_INTERNAL) {
return parseInternalAttribute(ilst, endPosition);
}
Log.d(TAG, "Skipped unknown metadata entry: " + Atom.getAtomTypeString(type));
return null;
} finally {
ilst.setPosition(endPosition);
}
}
private static TextInformationFrame parseTextAttribute(int type, String id,
ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
data.skipBytes(8); // version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(atomSize - 16);
return new TextInformationFrame(id, value);
}
Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
return null;
}
private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
data.skipBytes(8); // version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(atomSize - 16);
return new CommentFrame(LANGUAGE_UNDEFINED, value, value);
}
Log.w(TAG, "Failed to parse comment attribute: " + Atom.getAtomTypeString(type));
return null;
}
private static Id3Frame parseUint8Attribute(int type, String id, ParsableByteArray data,
boolean isTextInformationFrame, boolean isBoolean) {
int value = parseUint8AttributeValue(data);
if (isBoolean) {
value = Math.min(1, value);
}
if (value >= 0) {
return isTextInformationFrame ? new TextInformationFrame(id, Integer.toString(value))
: new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
}
Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
return null;
}
private static TextInformationFrame parseIndexAndCountAttribute(int type, String attributeName,
ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data && atomSize >= 22) {
data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
int index = data.readUnsignedShort();
if (index > 0) {
String description = "" + index;
int count = data.readUnsignedShort();
if (count > 0) {
description += "/" + count;
}
return new TextInformationFrame(attributeName, description);
}
}
Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
return null;
}
private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
int genreCode = parseUint8AttributeValue(data);
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
? STANDARD_GENRES[genreCode - 1] : null;
if (genreString != null) {
return new TextInformationFrame("TCON", genreString);
}
Log.w(TAG, "Failed to parse standard genre code");
return null;
}
private static ApicFrame parseCoverArt(ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
int fullVersionInt = data.readInt();
int flags = Atom.parseFullAtomFlags(fullVersionInt);
String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null;
if (mimeType == null) {
Log.w(TAG, "Unrecognized cover art flags: " + flags);
return null;
}
data.skipBytes(4); // empty (4)
byte[] pictureData = new byte[atomSize - 16];
data.readBytes(pictureData, 0, pictureData.length);
return new ApicFrame(mimeType, null, 3 /* Cover (front) */, pictureData);
}
Log.w(TAG, "Failed to parse cover art attribute");
return null;
}
private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
String domain = null;
String name = null;
int dataAtomPosition = -1;
int dataAtomSize = -1;
while (data.getPosition() < endPosition) {
int atomPosition = data.getPosition();
int atomSize = data.readInt();
int atomType = data.readInt();
data.skipBytes(4); // version (1), flags (3)
if (atomType == Atom.TYPE_mean) {
domain = data.readNullTerminatedString(atomSize - 12);
} else if (atomType == Atom.TYPE_name) {
name = data.readNullTerminatedString(atomSize - 12);
} else {
if (atomType == Atom.TYPE_data) {
dataAtomPosition = atomPosition;
dataAtomSize = atomSize;
}
data.skipBytes(atomSize - 12);
}
}
if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) {
// We're only interested in iTunSMPB.
return null;
}
data.setPosition(dataAtomPosition);
data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(dataAtomSize - 16);
return new CommentFrame(LANGUAGE_UNDEFINED, name, value);
}
private static int parseUint8AttributeValue(ParsableByteArray data) {
data.skipBytes(4); // atomSize
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
data.skipBytes(8); // version (1), flags (3), empty (4)
return data.readUnsignedByte();
}
Log.w(TAG, "Failed to parse uint8 attribute value");
return -1;
}
}

View File

@ -27,6 +27,7 @@ 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.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
@ -310,10 +311,14 @@ public final class Mp4Extractor implements Extractor, SeekMap {
List<Mp4Track> tracks = new ArrayList<>(); List<Mp4Track> tracks = new ArrayList<>();
long earliestSampleOffset = Long.MAX_VALUE; long earliestSampleOffset = Long.MAX_VALUE;
Metadata metadata = null;
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); 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) {
AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder); metadata = AtomParsers.parseUdta(udta, isQuickTime);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
}
} }
for (int i = 0; i < moov.containerChildren.size(); i++) { for (int i = 0; i < moov.containerChildren.size(); i++) {
@ -340,9 +345,14 @@ 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 && gaplessInfoHolder.hasGaplessInfo()) { if (track.type == C.TRACK_TYPE_AUDIO) {
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, if (gaplessInfoHolder.hasGaplessInfo()) {
gaplessInfoHolder.encoderPadding); format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding);
}
if (metadata != null) {
format = format.copyWithMetadata(metadata);
}
} }
mp4Track.trackOutput.format(format); mp4Track.trackOutput.format(format);

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Arrays;
import java.util.List;
/**
* A collection of metadata entries.
*/
public final class Metadata implements Parcelable {
/**
* A metadata entry.
*/
public interface Entry extends Parcelable {}
private final Entry[] entries;
/**
* @param entries The metadata entries.
*/
public Metadata(Entry... entries) {
this.entries = entries == null ? new Entry[0] : entries;
}
/**
* @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];
}
}
/* 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());
}
}
/**
* Returns the number of metadata entries.
*/
public int length() {
return entries.length;
}
/**
* 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 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() {
return Arrays.hashCode(entries);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(entries.length);
for (Entry entry : entries) {
dest.writeParcelable(entry, 0);
}
}
public static final Parcelable.Creator<Metadata> CREATOR = new Parcelable.Creator<Metadata>() {
@Override
public Metadata createFromParcel(Parcel in) {
return new Metadata(in);
}
@Override
public Metadata[] newArray(int size) {
return new Metadata[0];
}
};
}

View File

@ -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> {
* @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;
} }

View File

@ -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
* 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
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
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);
} }

View File

@ -15,6 +15,11 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/** /**
* APIC (Attached Picture) ID3 frame. * APIC (Attached Picture) ID3 frame.
*/ */
@ -35,4 +40,58 @@ public final class ApicFrame extends Id3Frame {
this.pictureData = pictureData; this.pictureData = pictureData;
} }
/* package */ ApicFrame(Parcel in) {
super(ID);
mimeType = in.readString();
description = in.readString();
pictureType = in.readInt();
pictureData = in.createByteArray();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ApicFrame other = (ApicFrame) obj;
return pictureType == other.pictureType && Util.areEqual(mimeType, other.mimeType)
&& Util.areEqual(description, other.description)
&& Arrays.equals(pictureData, other.pictureData);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + pictureType;
result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + Arrays.hashCode(pictureData);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mimeType);
dest.writeString(description);
dest.writeInt(pictureType);
dest.writeByteArray(pictureData);
}
public static final Parcelable.Creator<ApicFrame> CREATOR = new Parcelable.Creator<ApicFrame>() {
@Override
public ApicFrame createFromParcel(Parcel in) {
return new ApicFrame(in);
}
@Override
public ApicFrame[] newArray(int size) {
return new ApicFrame[size];
}
};
} }

View File

@ -15,6 +15,10 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Arrays;
/** /**
* Binary ID3 frame. * Binary ID3 frame.
*/ */
@ -22,9 +26,55 @@ public final class BinaryFrame extends Id3Frame {
public final byte[] data; public final byte[] data;
public BinaryFrame(String type, byte[] data) { public BinaryFrame(String id, byte[] data) {
super(type); super(id);
this.data = data; this.data = data;
} }
/* package */ BinaryFrame(Parcel in) {
super(in.readString());
data = in.createByteArray();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
BinaryFrame other = (BinaryFrame) obj;
return id.equals(other.id) && Arrays.equals(data, other.data);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + id.hashCode();
result = 31 * result + Arrays.hashCode(data);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeByteArray(data);
}
public static final Parcelable.Creator<BinaryFrame> CREATOR =
new Parcelable.Creator<BinaryFrame>() {
@Override
public BinaryFrame createFromParcel(Parcel in) {
return new BinaryFrame(in);
}
@Override
public BinaryFrame[] newArray(int size) {
return new BinaryFrame[size];
}
};
} }

View File

@ -0,0 +1,91 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/**
* Comment ID3 frame.
*/
public final class CommentFrame extends Id3Frame {
public static final String ID = "COMM";
public final String language;
public final String description;
public final String text;
public CommentFrame(String language, String description, String text) {
super(ID);
this.language = language;
this.description = description;
this.text = text;
}
/* package */ CommentFrame(Parcel in) {
super(ID);
language = in.readString();
description = in.readString();
text = in.readString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
CommentFrame other = (CommentFrame) obj;
return Util.areEqual(description, other.description) && Util.areEqual(language, other.language)
&& Util.areEqual(text, other.text);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (language != null ? language.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (text != null ? text.hashCode() : 0);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(language);
dest.writeString(text);
}
public static final Parcelable.Creator<CommentFrame> CREATOR =
new Parcelable.Creator<CommentFrame>() {
@Override
public CommentFrame createFromParcel(Parcel in) {
return new CommentFrame(in);
}
@Override
public CommentFrame[] newArray(int size) {
return new CommentFrame[size];
}
};
}

View File

@ -15,6 +15,11 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/** /**
* GEOB (General Encapsulated Object) ID3 frame. * GEOB (General Encapsulated Object) ID3 frame.
*/ */
@ -35,4 +40,57 @@ public final class GeobFrame extends Id3Frame {
this.data = data; this.data = data;
} }
/* package */ GeobFrame(Parcel in) {
super(ID);
mimeType = in.readString();
filename = in.readString();
description = in.readString();
data = in.createByteArray();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
GeobFrame other = (GeobFrame) obj;
return Util.areEqual(mimeType, other.mimeType) && Util.areEqual(filename, other.filename)
&& Util.areEqual(description, other.description) && Arrays.equals(data, other.data);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
result = 31 * result + (filename != null ? filename.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + Arrays.hashCode(data);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mimeType);
dest.writeString(filename);
dest.writeString(description);
dest.writeByteArray(data);
}
public static final Parcelable.Creator<GeobFrame> CREATOR = new Parcelable.Creator<GeobFrame>() {
@Override
public GeobFrame createFromParcel(Parcel in) {
return new GeobFrame(in);
}
@Override
public GeobFrame[] newArray(int size) {
return new GeobFrame[size];
}
};
} }

View File

@ -15,21 +15,33 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.util.Log;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /**
* Decodes individual TXXX text frames from raw ID3 data. * Decodes ID3 tags.
*/ */
public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> { public final class Id3Decoder implements MetadataDecoder {
private static final String TAG = "Id3Decoder";
/**
* The first three bytes of a well formed ID3 tag header.
*/
public static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
/**
* Length of an ID3 tag header.
*/
public static final int ID3_HEADER_LENGTH = 10;
private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
@ -42,51 +54,46 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
} }
@Override @Override
public List<Id3Frame> decode(byte[] data, int size) throws MetadataDecoderException { public Metadata decode(byte[] data, int size) {
List<Id3Frame> id3Frames = new ArrayList<>(); List<Id3Frame> id3Frames = new ArrayList<>();
ParsableByteArray id3Data = new ParsableByteArray(data, size); ParsableByteArray id3Data = new ParsableByteArray(data, size);
int id3Size = decodeId3Header(id3Data);
while (id3Size > 0) { Id3Header id3Header = decodeHeader(id3Data);
int frameId0 = id3Data.readUnsignedByte(); if (id3Header == null) {
int frameId1 = id3Data.readUnsignedByte(); return null;
int frameId2 = id3Data.readUnsignedByte(); }
int frameId3 = id3Data.readUnsignedByte();
int frameSize = id3Data.readSynchSafeInt();
if (frameSize <= 1) {
break;
}
// Skip frame flags. int startPosition = id3Data.getPosition();
id3Data.skipBytes(2); int framesSize = id3Header.framesSize;
if (id3Header.isUnsynchronized) {
framesSize = removeUnsynchronization(id3Data, id3Header.framesSize);
}
id3Data.setLimit(startPosition + framesSize);
try { boolean unsignedIntFrameSizeHack = false;
Id3Frame frame; if (id3Header.majorVersion == 4) {
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { if (!validateV4Frames(id3Data, false)) {
frame = decodeTxxxFrame(id3Data, frameSize); if (validateV4Frames(id3Data, true)) {
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { unsignedIntFrameSizeHack = true;
frame = decodePrivFrame(id3Data, frameSize);
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
frame = decodeGeobFrame(id3Data, frameSize);
} else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') {
frame = decodeApicFrame(id3Data, frameSize);
} else if (frameId0 == 'T') {
String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = decodeTextInformationFrame(id3Data, frameSize, id);
} else { } else {
String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); Log.w(TAG, "Failed to validate V4 ID3 tag");
frame = decodeBinaryFrame(id3Data, frameSize, id); return null;
} }
id3Frames.add(frame);
id3Size -= frameSize + 10 /* header size */;
} catch (UnsupportedEncodingException e) {
throw new MetadataDecoderException("Unsupported encoding", e);
} }
} }
return Collections.unmodifiableList(id3Frames); int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
while (id3Data.bytesLeft() >= frameHeaderSize) {
Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack);
if (frame != null) {
id3Frames.add(frame);
}
}
return new Metadata(id3Frames);
} }
// TODO: Move the following three methods nearer to the bottom of the file.
private static int indexOfEos(byte[] data, int fromIndex, int encoding) { private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
int terminationPos = indexOfZeroByte(data, fromIndex); int terminationPos = indexOfZeroByte(data, fromIndex);
@ -95,7 +102,7 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
return terminationPos; return terminationPos;
} }
// Otherwise look for a second zero byte. // Otherwise ensure an even index and look for a second zero byte.
while (terminationPos < data.length - 1) { while (terminationPos < data.length - 1) {
if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) {
return terminationPos; return terminationPos;
@ -121,38 +128,207 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
} }
/** /**
* @param id3Buffer A {@link ParsableByteArray} from which data should be read. * @param data A {@link ParsableByteArray} from which the header should be read.
* @return The size of ID3 frames in bytes, excluding the header and footer. * @return The parsed header, or null if the ID3 tag is unsupported.
* @throws MetadataDecoderException If ID3 file identifier != "ID3".
*/ */
private static int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { private static Id3Header decodeHeader(ParsableByteArray data) {
int id1 = id3Buffer.readUnsignedByte(); if (data.bytesLeft() < ID3_HEADER_LENGTH) {
int id2 = id3Buffer.readUnsignedByte(); Log.w(TAG, "Data too short to be an ID3 tag");
int id3 = id3Buffer.readUnsignedByte(); return null;
if (id1 != 'I' || id2 != 'D' || id3 != '3') {
throw new MetadataDecoderException(String.format(Locale.US,
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
} }
id3Buffer.skipBytes(2); // Skip version.
int flags = id3Buffer.readUnsignedByte(); int id = data.readUnsignedInt24();
int id3Size = id3Buffer.readSynchSafeInt(); if (id != ID3_TAG) {
Log.w(TAG, "Unexpected first three bytes of ID3 tag header: " + id);
return null;
}
// Check if extended header presents. int majorVersion = data.readUnsignedByte();
if ((flags & 0x2) != 0) { data.skipBytes(1); // Skip minor version.
int extendedHeaderSize = id3Buffer.readSynchSafeInt(); int flags = data.readUnsignedByte();
if (extendedHeaderSize > 4) { int framesSize = data.readSynchSafeInt();
id3Buffer.skipBytes(extendedHeaderSize - 4);
if (majorVersion == 2) {
boolean isCompressed = (flags & 0x40) != 0;
if (isCompressed) {
Log.w(TAG, "Skipped ID3 tag with majorVersion=2 and undefined compression scheme");
return null;
} }
id3Size -= extendedHeaderSize; } else if (majorVersion == 3) {
boolean hasExtendedHeader = (flags & 0x40) != 0;
if (hasExtendedHeader) {
int extendedHeaderSize = data.readInt(); // Size excluding size field.
data.skipBytes(extendedHeaderSize);
framesSize -= (extendedHeaderSize + 4);
}
} else if (majorVersion == 4) {
boolean hasExtendedHeader = (flags & 0x40) != 0;
if (hasExtendedHeader) {
int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field.
data.skipBytes(extendedHeaderSize - 4);
framesSize -= extendedHeaderSize;
}
boolean hasFooter = (flags & 0x10) != 0;
if (hasFooter) {
framesSize -= 10;
}
} else {
Log.w(TAG, "Skipped ID3 tag with unsupported majorVersion=" + majorVersion);
return null;
} }
// Check if footer presents. // isUnsynchronized is advisory only in version 4. Frame level flags are used instead.
if ((flags & 0x8) != 0) { boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0;
id3Size -= 10; return new Id3Header(majorVersion, isUnsynchronized, framesSize);
}
private static boolean validateV4Frames(ParsableByteArray id3Data,
boolean unsignedIntFrameSizeHack) {
int startPosition = id3Data.getPosition();
try {
while (id3Data.bytesLeft() >= 10) {
int id = id3Data.readInt();
int frameSize = id3Data.readUnsignedIntToInt();
int flags = id3Data.readUnsignedShort();
if (id == 0 && frameSize == 0 && flags == 0) {
return true;
} else {
if (!unsignedIntFrameSizeHack) {
// Parse the data size as a synchsafe integer, as per the spec.
if ((frameSize & 0x808080L) != 0) {
return false;
}
frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
| (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
}
int minimumFrameSize = 0;
if ((flags & 0x0040) != 0 /* hasGroupIdentifier */) {
minimumFrameSize++;
}
if ((flags & 0x0001) != 0 /* hasDataLength */) {
minimumFrameSize += 4;
}
if (frameSize < minimumFrameSize) {
return false;
}
if (id3Data.bytesLeft() < frameSize) {
return false;
}
id3Data.skipBytes(frameSize); // flags
}
}
return true;
} finally {
id3Data.setPosition(startPosition);
}
}
private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data,
boolean unsignedIntFrameSizeHack) {
int frameId0 = id3Data.readUnsignedByte();
int frameId1 = id3Data.readUnsignedByte();
int frameId2 = id3Data.readUnsignedByte();
int frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0;
int frameSize;
if (majorVersion == 4) {
frameSize = id3Data.readUnsignedIntToInt();
if (!unsignedIntFrameSizeHack) {
frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
| (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
}
} else if (majorVersion == 3) {
frameSize = id3Data.readUnsignedIntToInt();
} else /* id3Header.majorVersion == 2 */ {
frameSize = id3Data.readUnsignedInt24();
} }
return id3Size; int flags = majorVersion >= 3 ? id3Data.readUnsignedShort() : 0;
if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0
&& flags == 0) {
// We must be reading zero padding at the end of the tag.
id3Data.setPosition(id3Data.limit());
return null;
}
int nextFramePosition = id3Data.getPosition() + frameSize;
if (nextFramePosition > id3Data.limit()) {
Log.w(TAG, "Frame size exceeds remaining tag data");
id3Data.setPosition(id3Data.limit());
return null;
}
// Frame flags.
boolean isCompressed = false;
boolean isEncrypted = false;
boolean isUnsynchronized = false;
boolean hasDataLength = false;
boolean hasGroupIdentifier = false;
if (majorVersion == 3) {
isCompressed = (flags & 0x0080) != 0;
isEncrypted = (flags & 0x0040) != 0;
hasGroupIdentifier = (flags & 0x0020) != 0;
hasDataLength = isCompressed;
} else if (majorVersion == 4) {
hasGroupIdentifier = (flags & 0x0040) != 0;
isCompressed = (flags & 0x0008) != 0;
isEncrypted = (flags & 0x0004) != 0;
isUnsynchronized = (flags & 0x0002) != 0;
hasDataLength = (flags & 0x0001) != 0;
}
if (isCompressed || isEncrypted) {
Log.w(TAG, "Skipping unsupported compressed or encrypted frame");
id3Data.setPosition(nextFramePosition);
return null;
}
if (hasGroupIdentifier) {
frameSize--;
id3Data.skipBytes(1);
}
if (hasDataLength) {
frameSize -= 4;
id3Data.skipBytes(4);
}
if (isUnsynchronized) {
frameSize = removeUnsynchronization(id3Data, frameSize);
}
try {
Id3Frame frame;
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'
&& (majorVersion == 2 || frameId3 == 'X')) {
frame = decodeTxxxFrame(id3Data, frameSize);
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
frame = decodePrivFrame(id3Data, frameSize);
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O'
&& (frameId3 == 'B' || majorVersion == 2)) {
frame = decodeGeobFrame(id3Data, frameSize);
} else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C')
: (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) {
frame = decodeApicFrame(id3Data, frameSize, majorVersion);
} else if (frameId0 == 'T') {
String id = majorVersion == 2
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
: String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = decodeTextInformationFrame(id3Data, frameSize, id);
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M'
&& (frameId3 == 'M' || majorVersion == 2)) {
frame = decodeCommentFrame(id3Data, frameSize);
} else {
String id = majorVersion == 2
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
: String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = decodeBinaryFrame(id3Data, frameSize, id);
}
return frame;
} catch (UnsupportedEncodingException e) {
Log.w(TAG, "Unsupported character encoding");
return null;
} finally {
id3Data.setPosition(nextFramePosition);
}
} }
private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
@ -214,16 +390,29 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
return new GeobFrame(mimeType, filename, description, objectData); return new GeobFrame(mimeType, filename, description, objectData);
} }
private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize) private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize,
throws UnsupportedEncodingException { int majorVersion) throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte(); int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding); String charset = getCharsetName(encoding);
byte[] data = new byte[frameSize - 1]; byte[] data = new byte[frameSize - 1];
id3Data.readBytes(data, 0, frameSize - 1); id3Data.readBytes(data, 0, frameSize - 1);
int mimeTypeEndIndex = indexOfZeroByte(data, 0); String mimeType;
String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); int mimeTypeEndIndex;
if (majorVersion == 2) {
mimeTypeEndIndex = 2;
mimeType = "image/" + new String(data, 0, 3, "ISO-8859-1").toLowerCase();
if (mimeType.equals("image/jpg")) {
mimeType = "image/jpeg";
}
} else {
mimeTypeEndIndex = indexOfZeroByte(data, 0);
mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1").toLowerCase();
if (mimeType.indexOf('/') == -1) {
mimeType = "image/" + mimeType;
}
}
int pictureType = data[mimeTypeEndIndex + 1] & 0xFF; int pictureType = data[mimeTypeEndIndex + 1] & 0xFF;
@ -238,6 +427,28 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
return new ApicFrame(mimeType, description, pictureType, pictureData); return new ApicFrame(mimeType, description, pictureType, pictureData);
} }
private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize)
throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] data = new byte[3];
id3Data.readBytes(data, 0, 3);
String language = new String(data, 0, 3);
data = new byte[frameSize - 4];
id3Data.readBytes(data, 0, frameSize - 4);
int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset);
int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
int textEndIndex = indexOfEos(data, textStartIndex, encoding);
String text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset);
return new CommentFrame(language, description, text);
}
private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
int frameSize, String id) throws UnsupportedEncodingException { int frameSize, String id) throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte(); int encoding = id3Data.readUnsignedByte();
@ -260,6 +471,25 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
return new BinaryFrame(id, frame); return new BinaryFrame(id, frame);
} }
/**
* Performs in-place removal of unsynchronization for {@code length} bytes starting from
* {@link ParsableByteArray#getPosition()}
*
* @param data Contains the data to be processed.
* @param length The length of the data to be processed.
* @return The length of the data after processing.
*/
private static int removeUnsynchronization(ParsableByteArray data, int length) {
byte[] bytes = data.data;
for (int i = data.getPosition(); i + 1 < length; i++) {
if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {
System.arraycopy(bytes, i + 2, bytes, i + 1, length - i - 2);
length--;
}
}
return length;
}
/** /**
* Maps encoding byte from ID3v2 frame to a Charset. * Maps encoding byte from ID3v2 frame to a Charset.
* @param encodingByte The value of encoding byte from ID3v2 frame. * @param encodingByte The value of encoding byte from ID3v2 frame.
@ -280,4 +510,18 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
} }
} }
private static final class Id3Header {
private final int majorVersion;
private final boolean isUnsynchronized;
private final int framesSize;
public Id3Header(int majorVersion, boolean isUnsynchronized, int framesSize) {
this.majorVersion = majorVersion;
this.isUnsynchronized = isUnsynchronized;
this.framesSize = framesSize;
}
}
} }

View File

@ -15,10 +15,13 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
/** /**
* Base class for ID3 frames. * Base class for ID3 frames.
*/ */
public abstract class Id3Frame { public abstract class Id3Frame implements Metadata.Entry {
/** /**
* The frame ID. * The frame ID.
@ -26,7 +29,12 @@ public abstract class Id3Frame {
public final String id; public final String id;
public Id3Frame(String id) { public Id3Frame(String id) {
this.id = id; this.id = Assertions.checkNotNull(id);
}
@Override
public int describeContents() {
return 0;
} }
} }

View File

@ -15,6 +15,11 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/** /**
* PRIV (Private) ID3 frame. * PRIV (Private) ID3 frame.
*/ */
@ -31,4 +36,50 @@ public final class PrivFrame extends Id3Frame {
this.privateData = privateData; this.privateData = privateData;
} }
/* package */ PrivFrame(Parcel in) {
super(ID);
owner = in.readString();
privateData = in.createByteArray();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PrivFrame other = (PrivFrame) obj;
return Util.areEqual(owner, other.owner) && Arrays.equals(privateData, other.privateData);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (owner != null ? owner.hashCode() : 0);
result = 31 * result + Arrays.hashCode(privateData);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(owner);
dest.writeByteArray(privateData);
}
public static final Parcelable.Creator<PrivFrame> CREATOR = new Parcelable.Creator<PrivFrame>() {
@Override
public PrivFrame createFromParcel(Parcel in) {
return new PrivFrame(in);
}
@Override
public PrivFrame[] newArray(int size) {
return new PrivFrame[size];
}
};
} }

View File

@ -15,6 +15,10 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/** /**
* Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame. * Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame.
*/ */
@ -27,4 +31,50 @@ public final class TextInformationFrame extends Id3Frame {
this.description = description; this.description = description;
} }
/* package */ TextInformationFrame(Parcel in) {
super(in.readString());
description = in.readString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
TextInformationFrame other = (TextInformationFrame) obj;
return id.equals(other.id) && Util.areEqual(description, other.description);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + id.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(description);
}
public static final Parcelable.Creator<TextInformationFrame> CREATOR =
new Parcelable.Creator<TextInformationFrame>() {
@Override
public TextInformationFrame createFromParcel(Parcel in) {
return new TextInformationFrame(in);
}
@Override
public TextInformationFrame[] newArray(int size) {
return new TextInformationFrame[size];
}
};
} }

View File

@ -15,6 +15,10 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/** /**
* TXXX (User defined text information) ID3 frame. * TXXX (User defined text information) ID3 frame.
*/ */
@ -31,4 +35,50 @@ public final class TxxxFrame extends Id3Frame {
this.value = value; this.value = value;
} }
/* package */ TxxxFrame(Parcel in) {
super(ID);
description = in.readString();
value = in.readString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
TxxxFrame other = (TxxxFrame) obj;
return Util.areEqual(description, other.description) && Util.areEqual(value, other.value);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (value != null ? value.hashCode() : 0);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(description);
dest.writeString(value);
}
public static final Parcelable.Creator<TxxxFrame> CREATOR = new Parcelable.Creator<TxxxFrame>() {
@Override
public TxxxFrame createFromParcel(Parcel in) {
return new TxxxFrame(in);
}
@Override
public TxxxFrame[] newArray(int size) {
return new TxxxFrame[size];
}
};
} }

View File

@ -300,9 +300,9 @@ public final class ParsableByteArray {
*/ */
public int readLittleEndianInt() { public int readLittleEndianInt() {
return (data[position++] & 0xFF) return (data[position++] & 0xFF)
| (data[position++] & 0xFF) << 8 | (data[position++] & 0xFF) << 8
| (data[position++] & 0xFF) << 16 | (data[position++] & 0xFF) << 16
| (data[position++] & 0xFF) << 24; | (data[position++] & 0xFF) << 24;
} }
/** /**
@ -423,6 +423,24 @@ public final class ParsableByteArray {
return readString(length, Charset.defaultCharset()); return readString(length, Charset.defaultCharset());
} }
/**
* Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is ignored,
* if present.
*
* @param length The number of bytes to read.
* @return The string encoded by the bytes.
*/
public String readNullTerminatedString(int length) {
int stringLength = length;
int lastIndex = position + length - 1;
if (lastIndex < limit && data[lastIndex] == 0) {
stringLength--;
}
String result = new String(data, position, stringLength, Charset.defaultCharset());
position += length;
return result;
}
/** /**
* Reads the next {@code length} bytes as characters in the specified {@link Charset}. * Reads the next {@code length} bytes as characters in the specified {@link Charset}.
* *