mirror of
https://github.com/androidx/media.git
synced 2025-05-03 21:57:46 +08:00
Merge branch 'dev-v2-id3' into dev-v2
This commit is contained in:
commit
8a89abcbf1
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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() {}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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}.
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user