Commit 8a89abcb by Oliver Woodman

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

parents 2c543632 8caaf0b5
Showing with 777 additions and 141 deletions
...@@ -27,8 +27,10 @@ import com.google.android.exoplayer2.Timeline; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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);
} }
......
...@@ -24,6 +24,8 @@ import android.annotation.TargetApi; ...@@ -24,6 +24,8 @@ import android.annotation.TargetApi;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Parcel; import android.os.Parcel;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -56,11 +58,14 @@ public final class FormatTest extends TestCase { ...@@ -56,11 +58,14 @@ public final class FormatTest extends TestCase {
TestUtil.buildTestData(128, 1 /* data seed */)); TestUtil.buildTestData(128, 1 /* data seed */));
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
byte[] projectionData = new byte[] {1, 2, 3}; byte[] projectionData = new byte[] {1, 2, 3};
Metadata metadata = new Metadata(
new TextInformationFrame("id1", "description1"),
new TextInformationFrame("id2", "description2"));
Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100, 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100,
C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA,
drmInitData); drmInitData, metadata);
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
formatToParcel.writeToParcel(parcel, 0); formatToParcel.writeToParcel(parcel, 0);
......
...@@ -16,8 +16,8 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
} }
......
...@@ -35,9 +35,9 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
} }
} }
......
...@@ -16,6 +16,8 @@ ...@@ -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;
...@@ -66,6 +68,25 @@ public final class GaplessInfoHolder { ...@@ -66,6 +68,25 @@ public final class GaplessInfoHolder {
} }
/** /**
* 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 { ...@@ -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;
} }
......
...@@ -27,6 +27,8 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
...@@ -254,6 +261,45 @@ public final class Mp3Extractor implements Extractor { ...@@ -254,6 +261,45 @@ public final class Mp3Extractor implements Extractor {
} }
/** /**
* 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
* be set to the start of the first frame of audio. * be set to the start of the first frame of audio.
......
...@@ -132,7 +132,6 @@ import java.util.List; ...@@ -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; ...@@ -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);
......
...@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ParserException; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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();
} }
......
...@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder; ...@@ -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 { ...@@ -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 { ...@@ -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);
......
/*
* 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];
}
};
}
...@@ -17,10 +17,8 @@ package com.google.android.exoplayer2.metadata; ...@@ -17,10 +17,8 @@ package com.google.android.exoplayer2.metadata;
/** /**
* Decodes metadata from binary data. * Decodes metadata from binary data.
*
* @param <T> The type of the metadata.
*/ */
public interface MetadataDecoder<T> { public interface MetadataDecoder {
/** /**
* Checks whether the decoder supports a given mime type. * Checks whether the decoder supports a given mime type.
...@@ -38,6 +36,6 @@ public interface MetadataDecoder<T> { ...@@ -38,6 +36,6 @@ public interface MetadataDecoder<T> {
* @return The decoded metadata object. * @return The decoded metadata object.
* @throws MetadataDecoderException If a problem occurred decoding the data. * @throws MetadataDecoderException If a problem occurred decoding the data.
*/ */
T decode(byte[] data, int size) throws MetadataDecoderException; Metadata decode(byte[] data, int size) throws MetadataDecoderException;
} }
...@@ -30,38 +30,34 @@ import java.nio.ByteBuffer; ...@@ -30,38 +30,34 @@ import java.nio.ByteBuffer;
/** /**
* A renderer for metadata. * A renderer for metadata.
*
* @param <T> The type of the metadata.
*/ */
public final class MetadataRenderer<T> extends BaseRenderer implements Callback { public final class MetadataRenderer extends BaseRenderer implements Callback {
/** /**
* Receives output from a {@link MetadataRenderer}. * Receives output from a {@link MetadataRenderer}.
*
* @param <T> The type of the metadata.
*/ */
public interface Output<T> { public interface Output {
/** /**
* Called each time there is a metadata associated with current playback time. * Called each time there is a metadata associated with current playback time.
* *
* @param metadata The metadata. * @param metadata The metadata.
*/ */
void onMetadata(T metadata); void onMetadata(Metadata metadata);
} }
private static final int MSG_INVOKE_RENDERER = 0; private static final int MSG_INVOKE_RENDERER = 0;
private final MetadataDecoder<T> metadataDecoder; private final MetadataDecoder metadataDecoder;
private final Output<T> output; private final Output output;
private final Handler outputHandler; private final Handler outputHandler;
private final FormatHolder formatHolder; private final FormatHolder formatHolder;
private final DecoderInputBuffer buffer; private final DecoderInputBuffer buffer;
private boolean inputStreamEnded; private boolean inputStreamEnded;
private long pendingMetadataTimestamp; private long pendingMetadataTimestamp;
private T pendingMetadata; private Metadata pendingMetadata;
/** /**
* @param output The output. * @param output The output.
...@@ -72,8 +68,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback ...@@ -72,8 +68,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback
* called directly on the player's internal rendering thread. * called directly on the player's internal rendering thread.
* @param metadataDecoder A decoder for the metadata. * @param metadataDecoder A decoder for the metadata.
*/ */
public MetadataRenderer(Output<T> output, Looper outputLooper, public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) {
MetadataDecoder<T> metadataDecoder) {
super(C.TRACK_TYPE_METADATA); super(C.TRACK_TYPE_METADATA);
this.output = Assertions.checkNotNull(output); this.output = Assertions.checkNotNull(output);
this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
...@@ -137,7 +132,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback ...@@ -137,7 +132,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback
return true; return true;
} }
private void invokeRenderer(T metadata) { private void invokeRenderer(Metadata metadata) {
if (outputHandler != null) { if (outputHandler != null) {
outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
} else { } else {
...@@ -150,13 +145,13 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback ...@@ -150,13 +145,13 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback
public boolean handleMessage(Message msg) { public boolean handleMessage(Message msg) {
switch (msg.what) { switch (msg.what) {
case MSG_INVOKE_RENDERER: case MSG_INVOKE_RENDERER:
invokeRendererInternal((T) msg.obj); invokeRendererInternal((Metadata) msg.obj);
return true; return true;
} }
return false; return false;
} }
private void invokeRendererInternal(T metadata) { private void invokeRendererInternal(Metadata metadata) {
output.onMetadata(metadata); output.onMetadata(metadata);
} }
......
...@@ -15,6 +15,11 @@ ...@@ -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 { ...@@ -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];
}
};
} }
...@@ -15,6 +15,10 @@ ...@@ -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 { ...@@ -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];
}
};
} }
/*
* 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];
}
};
}
...@@ -15,6 +15,11 @@ ...@@ -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 { ...@@ -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];
}
};
} }
...@@ -15,10 +15,13 @@ ...@@ -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 { ...@@ -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;
} }
} }
...@@ -15,6 +15,11 @@ ...@@ -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 { ...@@ -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];
}
};
} }
...@@ -15,6 +15,10 @@ ...@@ -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 { ...@@ -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];
}
};
} }
...@@ -15,6 +15,10 @@ ...@@ -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 { ...@@ -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];
}
};
} }
...@@ -300,9 +300,9 @@ public final class ParsableByteArray { ...@@ -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;
} }
/** /**
...@@ -424,6 +424,24 @@ public final class ParsableByteArray { ...@@ -424,6 +424,24 @@ public final class ParsableByteArray {
} }
/** /**
* 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}.
* *
* @param length The number of bytes to read. * @param length The number of bytes to read.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment