mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Merge branch 'albumMetadataV2.1' of git://github.com/cbfiddle/ExoPlayer into cbfiddle-albumMetadataV2.1
This commit is contained in:
commit
5a097a4c9d
23 changed files with 1388 additions and 438 deletions
|
|
@ -27,8 +27,11 @@ import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||||
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
|
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.metadata.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;
|
||||||
|
|
@ -38,6 +41,7 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelections;
|
import com.google.android.exoplayer2.trackselection.TrackSelections;
|
||||||
|
|
@ -55,7 +59,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,
|
||||||
TrackSelector.EventListener<MappedTrackInfo>, MetadataRenderer.Output<List<Id3Frame>> {
|
MappingTrackSelector.EventListener<MappedTrackInfo>, MetadataRenderer.Output<Metadata> {
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -175,10 +179,11 @@ import java.util.Locale;
|
||||||
Log.d(TAG, "]");
|
Log.d(TAG, "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetadataRenderer.Output<List<Id3Frame>>
|
// MetadataRenderer.Output<Metadata>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMetadata(List<Id3Frame> id3Frames) {
|
public void onMetadata(Metadata metadata) {
|
||||||
|
List<Id3Frame> id3Frames = metadata.getFrames();
|
||||||
for (Id3Frame id3Frame : id3Frames) {
|
for (Id3Frame id3Frame : id3Frames) {
|
||||||
if (id3Frame instanceof TxxxFrame) {
|
if (id3Frame instanceof TxxxFrame) {
|
||||||
TxxxFrame txxxFrame = (TxxxFrame) id3Frame;
|
TxxxFrame txxxFrame = (TxxxFrame) id3Frame;
|
||||||
|
|
@ -199,10 +204,19 @@ import java.util.Locale;
|
||||||
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame;
|
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame;
|
||||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id,
|
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id,
|
||||||
textInformationFrame.description));
|
textInformationFrame.description));
|
||||||
|
} else if (id3Frame instanceof CommentFrame) {
|
||||||
|
CommentFrame commentFrame = (CommentFrame) id3Frame;
|
||||||
|
Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id,
|
||||||
|
commentFrame.language, commentFrame.text));
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id));
|
Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GaplessInfo gaplessInfo = metadata.getGaplessInfo();
|
||||||
|
if (gaplessInfo != null) {
|
||||||
|
Log.i(TAG, String.format("ID3 TimedMetadata encoder delay=%d padding=%d",
|
||||||
|
gaplessInfo.encoderDelay, gaplessInfo.encoderPadding));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioRendererEventListener
|
// AudioRendererEventListener
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
|
||||||
|
|
||||||
import android.test.MoreAsserts;
|
import android.test.MoreAsserts;
|
||||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
|
@ -30,7 +31,8 @@ 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);
|
||||||
|
List<Id3Frame> id3Frames = metadata.getFrames();
|
||||||
assertEquals(1, id3Frames.size());
|
assertEquals(1, id3Frames.size());
|
||||||
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0);
|
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0);
|
||||||
assertEquals("", txxxFrame.description);
|
assertEquals("", txxxFrame.description);
|
||||||
|
|
@ -42,7 +44,8 @@ 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);
|
||||||
|
List<Id3Frame> id3Frames = metadata.getFrames();
|
||||||
assertEquals(1, id3Frames.size());
|
assertEquals(1, id3Frames.size());
|
||||||
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0);
|
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0);
|
||||||
assertEquals("image/jpeg", apicFrame.mimeType);
|
assertEquals("image/jpeg", apicFrame.mimeType);
|
||||||
|
|
@ -56,7 +59,8 @@ 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);
|
||||||
|
List<Id3Frame> id3Frames = metadata.getFrames();
|
||||||
assertEquals(1, id3Frames.size());
|
assertEquals(1, id3Frames.size());
|
||||||
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0);
|
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0);
|
||||||
assertEquals("TIT2", textInformationFrame.id);
|
assertEquals("TIT2", textInformationFrame.id);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import android.media.MediaFormat;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||||
|
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.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;
|
||||||
|
|
@ -86,6 +88,11 @@ public final class Format implements Parcelable {
|
||||||
* DRM initialization data if the stream is protected, or null otherwise.
|
* DRM initialization data if the stream is protected, or null otherwise.
|
||||||
*/
|
*/
|
||||||
public final DrmInitData drmInitData;
|
public final DrmInitData drmInitData;
|
||||||
|
/**
|
||||||
|
* Static metadata
|
||||||
|
*/
|
||||||
|
public final Metadata metadata;
|
||||||
|
|
||||||
|
|
||||||
// Video specific.
|
// Video specific.
|
||||||
|
|
||||||
|
|
@ -185,7 +192,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 +218,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 +229,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,
|
||||||
|
|
@ -250,7 +257,7 @@ public final class Format implements Parcelable {
|
||||||
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, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text.
|
// Text.
|
||||||
|
|
@ -260,7 +267,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 +281,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 +290,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 +299,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 +314,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 +340,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 +373,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 +395,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 +411,32 @@ 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, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
GaplessInfo gaplessInfo = metadata.getGaplessInfo();
|
||||||
|
int ed = gaplessInfo != null ? gaplessInfo.encoderDelay : encoderDelay;
|
||||||
|
int ep = gaplessInfo != null ? gaplessInfo.encoderPadding : encoderPadding;
|
||||||
|
|
||||||
|
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
|
||||||
|
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
|
||||||
|
stereoMode, channelCount, sampleRate, pcmEncoding, ed, ep,
|
||||||
|
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -475,6 +496,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 +524,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 +597,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.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
import com.google.android.exoplayer2.text.TextRenderer;
|
import com.google.android.exoplayer2.text.TextRenderer;
|
||||||
|
|
@ -111,7 +111,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
private SurfaceHolder surfaceHolder;
|
private SurfaceHolder surfaceHolder;
|
||||||
private TextureView textureView;
|
private TextureView textureView;
|
||||||
private TextRenderer.Output textOutput;
|
private TextRenderer.Output textOutput;
|
||||||
private MetadataRenderer.Output<List<Id3Frame>> id3Output;
|
private MetadataRenderer.Output<Metadata> id3Output;
|
||||||
private VideoListener videoListener;
|
private VideoListener videoListener;
|
||||||
private AudioRendererEventListener audioDebugListener;
|
private AudioRendererEventListener audioDebugListener;
|
||||||
private VideoRendererEventListener videoDebugListener;
|
private VideoRendererEventListener videoDebugListener;
|
||||||
|
|
@ -393,7 +393,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
*
|
*
|
||||||
* @param output The output.
|
* @param output The output.
|
||||||
*/
|
*/
|
||||||
public void setId3Output(MetadataRenderer.Output<List<Id3Frame>> output) {
|
public void setId3Output(MetadataRenderer.Output<Metadata> output) {
|
||||||
id3Output = output;
|
id3Output = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -539,7 +539,7 @@ 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<Metadata> id3Renderer = new MetadataRenderer<>(componentListener,
|
||||||
mainHandler.getLooper(), new Id3Decoder());
|
mainHandler.getLooper(), new Id3Decoder());
|
||||||
renderersList.add(id3Renderer);
|
renderersList.add(id3Renderer);
|
||||||
}
|
}
|
||||||
|
|
@ -636,7 +636,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<Metadata>,
|
||||||
SurfaceHolder.Callback, TextureView.SurfaceTextureListener,
|
SurfaceHolder.Callback, TextureView.SurfaceTextureListener,
|
||||||
TrackSelector.EventListener<Object> {
|
TrackSelector.EventListener<Object> {
|
||||||
|
|
||||||
|
|
@ -768,12 +768,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetadataRenderer.Output<List<Id3Frame>> implementation
|
// MetadataRenderer.Output<Metadata> implementation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMetadata(List<Id3Frame> id3Frames) {
|
public void onMetadata(Metadata metadata) {
|
||||||
if (id3Output != null) {
|
if (id3Output != null) {
|
||||||
id3Output.onMetadata(id3Frames);
|
id3Output.onMetadata(metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.extractor;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gapless playback information.
|
||||||
|
*/
|
||||||
|
public final class GaplessInfo {
|
||||||
|
|
||||||
|
private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
|
||||||
|
private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of samples to trim from the start of the decoded audio stream.
|
||||||
|
*/
|
||||||
|
public final int encoderDelay;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of samples to trim from the end of the decoded audio stream.
|
||||||
|
*/
|
||||||
|
public final int encoderPadding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses gapless playback information from a gapless playback comment (stored in an ID3 header
|
||||||
|
* or MPEG 4 user data), if valid and non-zero.
|
||||||
|
* @param name The comment's identifier.
|
||||||
|
* @param data The comment's payload data.
|
||||||
|
* @return the gapless playback info, or null if the provided data is not valid.
|
||||||
|
*/
|
||||||
|
public static GaplessInfo createFromComment(String name, String data) {
|
||||||
|
if(!GAPLESS_COMMENT_ID.equals(name)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
|
||||||
|
if(matcher.find()) {
|
||||||
|
try {
|
||||||
|
int encoderDelay = Integer.parseInt(matcher.group(1), 16);
|
||||||
|
int encoderPadding = Integer.parseInt(matcher.group(2), 16);
|
||||||
|
if(encoderDelay > 0 || encoderPadding > 0) {
|
||||||
|
Log.d("ExoplayerImpl", "Parsed gapless info: " + encoderDelay + " " + encoderPadding);
|
||||||
|
return new GaplessInfo(encoderDelay, encoderPadding);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException var5) {
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore incorrectly formatted comments.
|
||||||
|
Log.d("ExoplayerImpl", "Unable to parse gapless info: " + data);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses gapless playback information from an MP3 Xing header, if valid and non-zero.
|
||||||
|
*
|
||||||
|
* @param value The 24-bit value to decode.
|
||||||
|
* @return the gapless playback info, or null if the provided data is not valid.
|
||||||
|
*/
|
||||||
|
public static GaplessInfo createFromXingHeaderValue(int value) {
|
||||||
|
int encoderDelay = value >> 12;
|
||||||
|
int encoderPadding = value & 0x0FFF;
|
||||||
|
return encoderDelay > 0 || encoderPadding > 0 ?
|
||||||
|
new GaplessInfo(encoderDelay, encoderPadding) :
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GaplessInfo(int encoderDelay, int encoderPadding) {
|
||||||
|
this.encoderDelay = encoderDelay;
|
||||||
|
this.encoderPadding = encoderPadding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,90 +15,11 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor;
|
package com.google.android.exoplayer2.extractor;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.Format;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holder for gapless playback information.
|
* Holder for gapless playback information.
|
||||||
*/
|
*/
|
||||||
public final class GaplessInfoHolder {
|
public final class GaplessInfoHolder {
|
||||||
|
|
||||||
private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
|
public GaplessInfo gaplessInfo;
|
||||||
private static final Pattern GAPLESS_COMMENT_PATTERN =
|
|
||||||
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of samples to trim from the start of the decoded audio stream, or
|
|
||||||
* {@link Format#NO_VALUE} if not set.
|
|
||||||
*/
|
|
||||||
public int encoderDelay;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of samples to trim from the end of the decoded audio stream, or
|
|
||||||
* {@link Format#NO_VALUE} if not set.
|
|
||||||
*/
|
|
||||||
public int encoderPadding;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new holder for gapless playback information.
|
|
||||||
*/
|
|
||||||
public GaplessInfoHolder() {
|
|
||||||
encoderDelay = Format.NO_VALUE;
|
|
||||||
encoderPadding = Format.NO_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populates the holder with data from an MP3 Xing header, if valid and non-zero.
|
|
||||||
*
|
|
||||||
* @param value The 24-bit value to decode.
|
|
||||||
* @return Whether the holder was populated.
|
|
||||||
*/
|
|
||||||
public boolean setFromXingHeaderValue(int value) {
|
|
||||||
int encoderDelay = value >> 12;
|
|
||||||
int encoderPadding = value & 0x0FFF;
|
|
||||||
if (encoderDelay > 0 || encoderPadding > 0) {
|
|
||||||
this.encoderDelay = encoderDelay;
|
|
||||||
this.encoderPadding = encoderPadding;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
|
|
||||||
* or MPEG 4 user data), if valid and non-zero.
|
|
||||||
*
|
|
||||||
* @param name The comment's identifier.
|
|
||||||
* @param data The comment's payload data.
|
|
||||||
* @return Whether the holder was populated.
|
|
||||||
*/
|
|
||||||
public boolean setFromComment(String name, String data) {
|
|
||||||
if (!GAPLESS_COMMENT_ID.equals(name)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
|
|
||||||
if (matcher.find()) {
|
|
||||||
try {
|
|
||||||
int encoderDelay = Integer.parseInt(matcher.group(1), 16);
|
|
||||||
int encoderPadding = Integer.parseInt(matcher.group(2), 16);
|
|
||||||
if (encoderDelay > 0 || encoderPadding > 0) {
|
|
||||||
this.encoderDelay = encoderDelay;
|
|
||||||
this.encoderPadding = encoderPadding;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// Ignore incorrectly formatted comments.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set.
|
|
||||||
*/
|
|
||||||
public boolean hasGaplessInfo() {
|
|
||||||
return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.mp3;
|
package com.google.android.exoplayer2.extractor.mp3;
|
||||||
|
|
||||||
import android.util.Pair;
|
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
|
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||||
|
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.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for parsing ID3 version 2 metadata in MP3 files.
|
* Utility for parsing ID3 version 2 metadata in MP3 files.
|
||||||
|
|
@ -34,19 +34,18 @@ import java.nio.charset.Charset;
|
||||||
private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024;
|
private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024;
|
||||||
|
|
||||||
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
|
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.
|
* Peeks data from the input and parses ID3 metadata, including gapless playback information.
|
||||||
*
|
*
|
||||||
* @param input The {@link ExtractorInput} from which data should be peeked.
|
* @param input The {@link ExtractorInput} from which data should be peeked.
|
||||||
* @param out The {@link GaplessInfoHolder} to populate.
|
* @return The metadata, if present, {@code null} otherwise.
|
||||||
* @throws IOException If an error occurred peeking from the input.
|
* @throws IOException If an error occurred peeking from the input.
|
||||||
* @throws InterruptedException If the thread was interrupted.
|
* @throws InterruptedException If the thread was interrupted.
|
||||||
*/
|
*/
|
||||||
public static void parseId3(ExtractorInput input, GaplessInfoHolder out)
|
public static Metadata parseId3(ExtractorInput input)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
Metadata result = null;
|
||||||
ParsableByteArray scratch = new ParsableByteArray(10);
|
ParsableByteArray scratch = new ParsableByteArray(10);
|
||||||
int peekedId3Bytes = 0;
|
int peekedId3Bytes = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -60,18 +59,26 @@ import java.nio.charset.Charset;
|
||||||
int minorVersion = scratch.readUnsignedByte();
|
int minorVersion = scratch.readUnsignedByte();
|
||||||
int flags = scratch.readUnsignedByte();
|
int flags = scratch.readUnsignedByte();
|
||||||
int length = scratch.readSynchSafeInt();
|
int length = scratch.readSynchSafeInt();
|
||||||
if (!out.hasGaplessInfo() && canParseMetadata(majorVersion, minorVersion, flags, length)) {
|
int frameLength = length + 10;
|
||||||
byte[] frame = new byte[length];
|
|
||||||
input.peekFully(frame, 0, length);
|
try {
|
||||||
parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags, out);
|
if (canParseMetadata(majorVersion, minorVersion, flags, length)) {
|
||||||
} else {
|
input.resetPeekPosition();
|
||||||
input.advancePeekPosition(length);
|
byte[] frame = new byte[frameLength];
|
||||||
|
input.peekFully(frame, 0, frameLength);
|
||||||
|
return new Id3Decoder().decode(frame, frameLength);
|
||||||
|
} else {
|
||||||
|
input.advancePeekPosition(length);
|
||||||
|
}
|
||||||
|
} catch (MetadataDecoderException e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
peekedId3Bytes += 10 + length;
|
peekedId3Bytes += frameLength;
|
||||||
}
|
}
|
||||||
input.resetPeekPosition();
|
input.resetPeekPosition();
|
||||||
input.advancePeekPosition(peekedId3Bytes);
|
input.advancePeekPosition(peekedId3Bytes);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
|
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
|
||||||
|
|
@ -83,211 +90,6 @@ import java.nio.charset.Charset;
|
||||||
&& !(majorVersion == 4 && (flags & 0x0F) != 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() {}
|
private Id3Util() {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,12 @@ import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||||
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.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;
|
||||||
|
|
@ -69,7 +70,7 @@ public final class Mp3Extractor implements Extractor {
|
||||||
private final long forcedFirstSampleTimestampUs;
|
private final long forcedFirstSampleTimestampUs;
|
||||||
private final ParsableByteArray scratch;
|
private final ParsableByteArray scratch;
|
||||||
private final MpegAudioHeader synchronizedHeader;
|
private final MpegAudioHeader synchronizedHeader;
|
||||||
private final GaplessInfoHolder gaplessInfoHolder;
|
private Metadata metadata;
|
||||||
|
|
||||||
// Extractor outputs.
|
// Extractor outputs.
|
||||||
private ExtractorOutput extractorOutput;
|
private ExtractorOutput extractorOutput;
|
||||||
|
|
@ -99,7 +100,6 @@ public final class Mp3Extractor implements Extractor {
|
||||||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||||
scratch = new ParsableByteArray(4);
|
scratch = new ParsableByteArray(4);
|
||||||
synchronizedHeader = new MpegAudioHeader();
|
synchronizedHeader = new MpegAudioHeader();
|
||||||
gaplessInfoHolder = new GaplessInfoHolder();
|
|
||||||
basisTimeUs = C.TIME_UNSET;
|
basisTimeUs = C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,10 +141,21 @@ public final class Mp3Extractor implements Extractor {
|
||||||
if (seeker == null) {
|
if (seeker == null) {
|
||||||
seeker = setupSeeker(input);
|
seeker = setupSeeker(input);
|
||||||
extractorOutput.seekMap(seeker);
|
extractorOutput.seekMap(seeker);
|
||||||
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
|
|
||||||
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
|
GaplessInfo gaplessInfo = metadata != null ? metadata.getGaplessInfo() : null;
|
||||||
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
|
|
||||||
gaplessInfoHolder.encoderPadding, null, null, 0, null));
|
Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
|
||||||
|
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
|
||||||
|
synchronizedHeader.sampleRate, Format.NO_VALUE,
|
||||||
|
gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE,
|
||||||
|
gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE,
|
||||||
|
null, null, 0, null);
|
||||||
|
|
||||||
|
if (metadata != null) {
|
||||||
|
format = format.copyWithMetadata(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackOutput.format(format);
|
||||||
}
|
}
|
||||||
return readSample(input);
|
return readSample(input);
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +210,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);
|
metadata = Id3Util.parseId3(input);
|
||||||
peekedId3Bytes = (int) input.getPeekPosition();
|
peekedId3Bytes = (int) input.getPeekPosition();
|
||||||
if (!sniffing) {
|
if (!sniffing) {
|
||||||
input.skipFully(peekedId3Bytes);
|
input.skipFully(peekedId3Bytes);
|
||||||
|
|
@ -285,13 +296,16 @@ public final class Mp3Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
if (headerData == XING_HEADER || headerData == INFO_HEADER) {
|
if (headerData == XING_HEADER || headerData == INFO_HEADER) {
|
||||||
seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
|
seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
|
||||||
if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
|
if (seeker != null && metadata == null || metadata.getGaplessInfo() == null) {
|
||||||
// If there is a Xing header, read gapless playback metadata at a fixed offset.
|
// If there is a Xing header, read gapless playback metadata at a fixed offset.
|
||||||
input.resetPeekPosition();
|
input.resetPeekPosition();
|
||||||
input.advancePeekPosition(xingBase + 141);
|
input.advancePeekPosition(xingBase + 141);
|
||||||
input.peekFully(scratch.data, 0, 3);
|
input.peekFully(scratch.data, 0, 3);
|
||||||
scratch.setPosition(0);
|
scratch.setPosition(0);
|
||||||
gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
|
GaplessInfo gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24());
|
||||||
|
metadata = metadata != null ?
|
||||||
|
metadata.withGaplessInfo(gaplessInfo) : new Metadata(null, gaplessInfo);
|
||||||
|
|
||||||
}
|
}
|
||||||
input.skipFully(synchronizedHeader.frameSize);
|
input.skipFully(synchronizedHeader.frameSize);
|
||||||
} else if (frame.limit() >= 40) {
|
} else if (frame.limit() >= 40) {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,15 @@ import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
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.GaplessInfo;
|
||||||
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.metadata.MetadataBuilder;
|
||||||
|
import com.google.android.exoplayer2.metadata.id3.BinaryFrame;
|
||||||
|
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||||
|
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||||
|
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
||||||
|
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
|
||||||
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;
|
||||||
|
|
@ -278,7 +286,7 @@ import java.util.List;
|
||||||
flags = rechunkedResults.flags;
|
flags = rechunkedResults.flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) {
|
if (track.editListDurations == null || gaplessInfoHolder.gaplessInfo != null) {
|
||||||
// There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
|
// There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
|
||||||
// This implementation does not support applying both gapless metadata and an edit list.
|
// This implementation does not support applying both gapless metadata and an edit list.
|
||||||
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
|
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
|
||||||
|
|
@ -307,10 +315,9 @@ import java.util.List;
|
||||||
track.format.sampleRate, track.timescale);
|
track.format.sampleRate, track.timescale);
|
||||||
long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,
|
long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,
|
||||||
track.format.sampleRate, track.timescale);
|
track.format.sampleRate, track.timescale);
|
||||||
if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE
|
if ((encoderDelay > 0 || encoderPadding > 0) && encoderDelay <= Integer.MAX_VALUE
|
||||||
&& encoderPadding <= Integer.MAX_VALUE) {
|
&& encoderPadding <= Integer.MAX_VALUE) {
|
||||||
gaplessInfoHolder.encoderDelay = (int) encoderDelay;
|
gaplessInfoHolder.gaplessInfo = new GaplessInfo((int) encoderDelay, (int) encoderPadding);
|
||||||
gaplessInfoHolder.encoderPadding = (int) encoderPadding;
|
|
||||||
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
|
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
|
||||||
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
|
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
|
||||||
}
|
}
|
||||||
|
|
@ -395,17 +402,17 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a udta atom.
|
* Parses a udta atom for metadata, including gapless playback information.
|
||||||
*
|
*
|
||||||
* @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 metadata stored in the user data, or {@code null} if not present.
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
|
|
@ -415,14 +422,14 @@ import java.util.List;
|
||||||
if (atomType == Atom.TYPE_meta) {
|
if (atomType == Atom.TYPE_meta) {
|
||||||
udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE);
|
udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE);
|
||||||
udtaData.setLimit(udtaData.getPosition() + atomSize);
|
udtaData.setLimit(udtaData.getPosition() + atomSize);
|
||||||
parseMetaAtom(udtaData, out);
|
return parseMetaAtom(udtaData);
|
||||||
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 data) {
|
||||||
data.skipBytes(Atom.FULL_HEADER_SIZE);
|
data.skipBytes(Atom.FULL_HEADER_SIZE);
|
||||||
ParsableByteArray ilst = new ParsableByteArray();
|
ParsableByteArray ilst = new ParsableByteArray();
|
||||||
while (data.bytesLeft() >= Atom.HEADER_SIZE) {
|
while (data.bytesLeft() >= Atom.HEADER_SIZE) {
|
||||||
|
|
@ -431,47 +438,333 @@ import java.util.List;
|
||||||
if (atomType == Atom.TYPE_ilst) {
|
if (atomType == Atom.TYPE_ilst) {
|
||||||
ilst.reset(data.data, data.getPosition() + payloadSize);
|
ilst.reset(data.data, data.getPosition() + payloadSize);
|
||||||
ilst.setPosition(data.getPosition());
|
ilst.setPosition(data.getPosition());
|
||||||
parseIlst(ilst, out);
|
Metadata result = parseIlst(ilst);
|
||||||
if (out.hasGaplessInfo()) {
|
if (result != null) {
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data.skipBytes(payloadSize);
|
data.skipBytes(payloadSize);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) {
|
private static Metadata parseIlst(ParsableByteArray ilst) {
|
||||||
|
|
||||||
|
MetadataBuilder builder = new MetadataBuilder();
|
||||||
|
|
||||||
while (ilst.bytesLeft() > 0) {
|
while (ilst.bytesLeft() > 0) {
|
||||||
int position = ilst.getPosition();
|
int position = ilst.getPosition();
|
||||||
int endPosition = position + ilst.readInt();
|
int endPosition = position + ilst.readInt();
|
||||||
int type = ilst.readInt();
|
int type = ilst.readInt();
|
||||||
if (type == Atom.TYPE_DASHES) {
|
parseIlstElement(ilst, type, endPosition, builder);
|
||||||
String lastCommentMean = null;
|
ilst.setPosition(endPosition);
|
||||||
String lastCommentName = null;
|
}
|
||||||
String lastCommentData = null;
|
|
||||||
while (ilst.getPosition() < endPosition) {
|
return builder.build();
|
||||||
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
}
|
||||||
int key = ilst.readInt();
|
|
||||||
ilst.skipBytes(4);
|
private static final String P1 = "\u00a9";
|
||||||
if (key == Atom.TYPE_mean) {
|
private static final String P2 = "\ufffd";
|
||||||
lastCommentMean = ilst.readString(length);
|
private static final int TYPE_NAME_1 = Util.getIntegerCodeForString(P1 + "nam");
|
||||||
} else if (key == Atom.TYPE_name) {
|
private static final int TYPE_NAME_2 = Util.getIntegerCodeForString(P2 + "nam");
|
||||||
lastCommentName = ilst.readString(length);
|
private static final int TYPE_NAME_3 = Util.getIntegerCodeForString(P1 + "trk");
|
||||||
} else if (key == Atom.TYPE_data) {
|
private static final int TYPE_NAME_4 = Util.getIntegerCodeForString(P2 + "trk");
|
||||||
ilst.skipBytes(4);
|
private static final int TYPE_COMMENT_1 = Util.getIntegerCodeForString(P1 + "cmt");
|
||||||
lastCommentData = ilst.readString(length - 4);
|
private static final int TYPE_COMMENT_2 = Util.getIntegerCodeForString(P2 + "cmt");
|
||||||
} else {
|
private static final int TYPE_YEAR_1 = Util.getIntegerCodeForString(P1 + "day");
|
||||||
ilst.skipBytes(length);
|
private static final int TYPE_YEAR_2 = Util.getIntegerCodeForString(P2 + "day");
|
||||||
|
private static final int TYPE_ARTIST_1 = Util.getIntegerCodeForString(P1 + "ART");
|
||||||
|
private static final int TYPE_ARTIST_2 = Util.getIntegerCodeForString(P2 + "ART");
|
||||||
|
private static final int TYPE_ENCODER_1 = Util.getIntegerCodeForString(P1 + "too");
|
||||||
|
private static final int TYPE_ENCODER_2 = Util.getIntegerCodeForString(P2 + "too");
|
||||||
|
private static final int TYPE_ALBUM_1 = Util.getIntegerCodeForString(P1 + "alb");
|
||||||
|
private static final int TYPE_ALBUM_2 = Util.getIntegerCodeForString(P2 + "alb");
|
||||||
|
private static final int TYPE_COMPOSER_1 = Util.getIntegerCodeForString(P1 + "com");
|
||||||
|
private static final int TYPE_COMPOSER_2 = Util.getIntegerCodeForString(P2 + "com");
|
||||||
|
private static final int TYPE_COMPOSER_3 = Util.getIntegerCodeForString(P1 + "wrt");
|
||||||
|
private static final int TYPE_COMPOSER_4 = Util.getIntegerCodeForString(P2 + "wrt");
|
||||||
|
private static final int TYPE_LYRICS_1 = Util.getIntegerCodeForString(P1 + "lyr");
|
||||||
|
private static final int TYPE_LYRICS_2 = Util.getIntegerCodeForString(P2 + "lyr");
|
||||||
|
private static final int TYPE_GENRE_1 = Util.getIntegerCodeForString(P1 + "gen");
|
||||||
|
private static final int TYPE_GENRE_2 = Util.getIntegerCodeForString(P2 + "gen");
|
||||||
|
private static final int TYPE_STANDARD_GENRE = Util.getIntegerCodeForString("gnre");
|
||||||
|
private static final int TYPE_GROUPING_1 = Util.getIntegerCodeForString(P1 + "grp");
|
||||||
|
private static final int TYPE_GROUPING_2 = Util.getIntegerCodeForString(P2 + "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");
|
||||||
|
private static final int TYPE_SORT_SHOW = Util.getIntegerCodeForString("sosn");
|
||||||
|
private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap");
|
||||||
|
private static final int TYPE_SHOW = Util.getIntegerCodeForString("tvsh");
|
||||||
|
|
||||||
|
// TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes
|
||||||
|
|
||||||
|
private static void parseIlstElement(
|
||||||
|
ParsableByteArray ilst, int type, int endPosition, MetadataBuilder builder) {
|
||||||
|
if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) {
|
||||||
|
parseTextAttribute(builder, "TIT2", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) {
|
||||||
|
parseCommentAttribute(builder, "COMM", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) {
|
||||||
|
parseTextAttribute(builder, "TDRC", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) {
|
||||||
|
parseTextAttribute(builder, "TPE1", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) {
|
||||||
|
parseTextAttribute(builder, "TSSE", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) {
|
||||||
|
parseTextAttribute(builder, "TALB", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 ||
|
||||||
|
type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) {
|
||||||
|
parseTextAttribute(builder, "TCOM", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) {
|
||||||
|
parseTextAttribute(builder, "lyrics", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_STANDARD_GENRE) {
|
||||||
|
parseStandardGenreAttribute(builder, "TCON", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) {
|
||||||
|
parseTextAttribute(builder, "TCON", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) {
|
||||||
|
parseTextAttribute(builder, "TIT1", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_DISK_NUMBER) {
|
||||||
|
parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_TRACK_NUMBER) {
|
||||||
|
parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_TEMPO) {
|
||||||
|
parseIntegerAttribute(builder, "TBPM", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_COMPILATION) {
|
||||||
|
parseBooleanAttribute(builder, "TCMP", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_ALBUM_ARTIST) {
|
||||||
|
parseTextAttribute(builder, "TPE2", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_SORT_TRACK_NAME) {
|
||||||
|
parseTextAttribute(builder, "TSOT", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_SORT_ALBUM) {
|
||||||
|
parseTextAttribute(builder, "TSO2", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_SORT_ARTIST) {
|
||||||
|
parseTextAttribute(builder, "TSOA", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_SORT_ALBUM_ARTIST) {
|
||||||
|
parseTextAttribute(builder, "TSOP", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_SORT_COMPOSER) {
|
||||||
|
parseTextAttribute(builder, "TSOC", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_SORT_SHOW) {
|
||||||
|
parseTextAttribute(builder, "sortShow", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_GAPLESS_ALBUM) {
|
||||||
|
parseBooleanAttribute(builder, "gaplessAlbum", ilst, endPosition);
|
||||||
|
} else if (type == TYPE_SHOW) {
|
||||||
|
parseTextAttribute(builder, "show", ilst, endPosition);
|
||||||
|
} else if (type == Atom.TYPE_DASHES) {
|
||||||
|
parseExtendedAttribute(builder, ilst, endPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseTextAttribute(MetadataBuilder builder,
|
||||||
|
String attributeName,
|
||||||
|
ParsableByteArray ilst,
|
||||||
|
int endPosition) {
|
||||||
|
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||||
|
int key = ilst.readInt();
|
||||||
|
ilst.skipBytes(4);
|
||||||
|
if (key == Atom.TYPE_data) {
|
||||||
|
ilst.skipBytes(4);
|
||||||
|
String value = ilst.readNullTerminatedString(length - 4);
|
||||||
|
Id3Frame frame = new TextInformationFrame(attributeName, value);
|
||||||
|
builder.add(frame);
|
||||||
|
} else {
|
||||||
|
ilst.skipBytes(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseCommentAttribute(MetadataBuilder builder,
|
||||||
|
String attributeName,
|
||||||
|
ParsableByteArray ilst,
|
||||||
|
int endPosition) {
|
||||||
|
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||||
|
int key = ilst.readInt();
|
||||||
|
ilst.skipBytes(4);
|
||||||
|
if (key == Atom.TYPE_data) {
|
||||||
|
ilst.skipBytes(4);
|
||||||
|
String value = ilst.readNullTerminatedString(length - 4);
|
||||||
|
Id3Frame frame = new CommentFrame("eng", attributeName, value);
|
||||||
|
builder.add(frame);
|
||||||
|
} else {
|
||||||
|
ilst.skipBytes(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseBooleanAttribute(MetadataBuilder builder,
|
||||||
|
String attributeName,
|
||||||
|
ParsableByteArray ilst,
|
||||||
|
int endPosition) {
|
||||||
|
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||||
|
int key = ilst.readInt();
|
||||||
|
ilst.skipBytes(4);
|
||||||
|
if (key == Atom.TYPE_data) {
|
||||||
|
Object value = parseDataBox(ilst, length);
|
||||||
|
if (value instanceof Integer) {
|
||||||
|
int n = (Integer) value;
|
||||||
|
String s = n == 0 ? "0" : "1";
|
||||||
|
Id3Frame frame = new TextInformationFrame(attributeName, s);
|
||||||
|
builder.add(frame);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ilst.skipBytes(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseIntegerAttribute(MetadataBuilder builder,
|
||||||
|
String attributeName,
|
||||||
|
ParsableByteArray ilst,
|
||||||
|
int endPosition) {
|
||||||
|
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||||
|
int key = ilst.readInt();
|
||||||
|
ilst.skipBytes(4);
|
||||||
|
if (key == Atom.TYPE_data) {
|
||||||
|
Object value = parseDataBox(ilst, length);
|
||||||
|
if (value instanceof Integer) {
|
||||||
|
int n = (Integer) value;
|
||||||
|
String s = "" + n;
|
||||||
|
Id3Frame frame = new TextInformationFrame(attributeName, s);
|
||||||
|
builder.add(frame);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ilst.skipBytes(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseIndexAndCountAttribute(MetadataBuilder builder,
|
||||||
|
String attributeName,
|
||||||
|
ParsableByteArray ilst,
|
||||||
|
int endPosition) {
|
||||||
|
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||||
|
int key = ilst.readInt();
|
||||||
|
ilst.skipBytes(4);
|
||||||
|
if (key == Atom.TYPE_data) {
|
||||||
|
Object value = parseDataBox(ilst, length);
|
||||||
|
if (value instanceof byte[]) {
|
||||||
|
byte[] bytes = (byte[]) value;
|
||||||
|
if (bytes.length == 8) {
|
||||||
|
int index = (bytes[2] << 8) + (bytes[3] & 0xFF);
|
||||||
|
int count = (bytes[4] << 8) + (bytes[5] & 0xFF);
|
||||||
|
if (index > 0) {
|
||||||
|
String s = "" + index;
|
||||||
|
if (count > 0) {
|
||||||
|
s = s + "/" + count;
|
||||||
|
}
|
||||||
|
Id3Frame frame = new TextInformationFrame(attributeName, s);
|
||||||
|
builder.add(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lastCommentName != null && lastCommentData != null
|
|
||||||
&& "com.apple.iTunes".equals(lastCommentMean)) {
|
|
||||||
out.setFromComment(lastCommentName, lastCommentData);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ilst.setPosition(endPosition);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ilst.skipBytes(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseStandardGenreAttribute(MetadataBuilder builder,
|
||||||
|
String attributeName,
|
||||||
|
ParsableByteArray ilst,
|
||||||
|
int endPosition) {
|
||||||
|
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||||
|
int key = ilst.readInt();
|
||||||
|
ilst.skipBytes(4);
|
||||||
|
if (key == Atom.TYPE_data) {
|
||||||
|
Object value = parseDataBox(ilst, length);
|
||||||
|
if (value instanceof byte[]) {
|
||||||
|
byte[] bytes = (byte[]) value;
|
||||||
|
if (bytes.length == 2) {
|
||||||
|
int code = (bytes[0] << 8) + (bytes[1] & 0xFF);
|
||||||
|
String s = Id3Decoder.decodeGenre(code);
|
||||||
|
if (s != null) {
|
||||||
|
Id3Frame frame = new TextInformationFrame(attributeName, s);
|
||||||
|
builder.add(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ilst.skipBytes(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseExtendedAttribute(MetadataBuilder builder,
|
||||||
|
ParsableByteArray ilst,
|
||||||
|
int endPosition) {
|
||||||
|
String domain = null;
|
||||||
|
String name = null;
|
||||||
|
Object value = 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) {
|
||||||
|
domain = ilst.readNullTerminatedString(length);
|
||||||
|
} else if (key == Atom.TYPE_name) {
|
||||||
|
name = ilst.readNullTerminatedString(length);
|
||||||
|
} else if (key == Atom.TYPE_data) {
|
||||||
|
value = parseDataBox(ilst, length);
|
||||||
|
} else {
|
||||||
|
ilst.skipBytes(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunSMPB")) {
|
||||||
|
String s = value instanceof byte[] ? new String((byte[]) value) : value.toString();
|
||||||
|
builder.setGaplessInfo(GaplessInfo.createFromComment("iTunSMPB", s));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunNORM") && (value instanceof byte[])) {
|
||||||
|
String s = new String((byte[]) value);
|
||||||
|
Id3Frame frame = new CommentFrame("eng", "iTunNORM", s);
|
||||||
|
builder.add(frame);
|
||||||
|
} else if (domain != null && name != null) {
|
||||||
|
String extendedName = domain + "." + name;
|
||||||
|
if (value instanceof String) {
|
||||||
|
Id3Frame frame = new TextInformationFrame(extendedName, (String) value);
|
||||||
|
builder.add(frame);
|
||||||
|
} else if (value instanceof Integer) {
|
||||||
|
Id3Frame frame = new TextInformationFrame(extendedName, value.toString());
|
||||||
|
builder.add(frame);
|
||||||
|
} else if (value instanceof byte[]) {
|
||||||
|
byte[] bb = (byte[]) value;
|
||||||
|
Id3Frame frame = new BinaryFrame(extendedName, bb);
|
||||||
|
builder.add(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object parseDataBox(ParsableByteArray ilst, int length) {
|
||||||
|
int versionAndFlags = ilst.readInt();
|
||||||
|
int flags = versionAndFlags & 0xFFFFFF;
|
||||||
|
boolean isText = (flags == 1);
|
||||||
|
boolean isData = (flags == 0);
|
||||||
|
boolean isImageData = (flags == 0xD);
|
||||||
|
boolean isInteger = (flags == 21);
|
||||||
|
int dataLength = length - 4;
|
||||||
|
if (isText) {
|
||||||
|
return ilst.readNullTerminatedString(dataLength);
|
||||||
|
} else if (isInteger) {
|
||||||
|
if (dataLength == 1) {
|
||||||
|
return ilst.readUnsignedByte();
|
||||||
|
} else if (dataLength == 2) {
|
||||||
|
return ilst.readUnsignedShort();
|
||||||
|
} else {
|
||||||
|
ilst.skipBytes(dataLength);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (isData) {
|
||||||
|
byte[] bytes = new byte[dataLength];
|
||||||
|
ilst.readBytes(bytes, 0, dataLength);
|
||||||
|
return bytes;
|
||||||
|
} else {
|
||||||
|
ilst.skipBytes(dataLength);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,13 @@ import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
|
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
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;
|
||||||
|
|
@ -309,11 +311,16 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
||||||
long durationUs = C.TIME_UNSET;
|
long durationUs = C.TIME_UNSET;
|
||||||
List<Mp4Track> tracks = new ArrayList<>();
|
List<Mp4Track> tracks = new ArrayList<>();
|
||||||
long earliestSampleOffset = Long.MAX_VALUE;
|
long earliestSampleOffset = Long.MAX_VALUE;
|
||||||
|
GaplessInfo gaplessInfo = null;
|
||||||
|
Metadata metadata = null;
|
||||||
|
|
||||||
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 info = AtomParsers.parseUdta(udta, isQuickTime);
|
||||||
|
if (info != null) {
|
||||||
|
gaplessInfo = info.getGaplessInfo();
|
||||||
|
metadata = info;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < moov.containerChildren.size(); i++) {
|
for (int i = 0; i < moov.containerChildren.size(); i++) {
|
||||||
|
|
@ -330,7 +337,10 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
||||||
|
|
||||||
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
|
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
|
||||||
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
|
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
|
||||||
|
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
|
||||||
|
gaplessInfoHolder.gaplessInfo = gaplessInfo;
|
||||||
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
|
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
|
||||||
|
gaplessInfo = gaplessInfoHolder.gaplessInfo;
|
||||||
if (trackSampleTable.sampleCount == 0) {
|
if (trackSampleTable.sampleCount == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -340,9 +350,11 @@ 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 && gaplessInfo != null) {
|
||||||
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
|
format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding);
|
||||||
gaplessInfoHolder.encoderPadding);
|
}
|
||||||
|
if (metadata != null) {
|
||||||
|
format = format.copyWithMetadata(metadata);
|
||||||
}
|
}
|
||||||
mp4Track.trackOutput.format(format);
|
mp4Track.trackOutput.format(format);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* 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 com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||||
|
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID3 style metadata, with convenient access to gapless playback information.
|
||||||
|
*/
|
||||||
|
public class Metadata implements Parcelable {
|
||||||
|
|
||||||
|
private final List<Id3Frame> frames;
|
||||||
|
private final GaplessInfo gaplessInfo;
|
||||||
|
|
||||||
|
public Metadata(List<Id3Frame> frames, GaplessInfo gaplessInfo) {
|
||||||
|
List<Id3Frame> theFrames = frames != null ? new ArrayList<>(frames) : new ArrayList<Id3Frame>();
|
||||||
|
this.frames = Collections.unmodifiableList(theFrames);
|
||||||
|
this.gaplessInfo = gaplessInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metadata(Parcel in) {
|
||||||
|
int encoderDelay = in.readInt();
|
||||||
|
int encoderPadding = in.readInt();
|
||||||
|
gaplessInfo = encoderDelay > 0 || encoderPadding > 0 ?
|
||||||
|
new GaplessInfo(encoderDelay, encoderPadding) : null;
|
||||||
|
frames = Arrays.asList((Id3Frame[]) in.readArray(Id3Frame.class.getClassLoader()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metadata withGaplessInfo(GaplessInfo info) {
|
||||||
|
return new Metadata(frames, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Id3Frame> getFrames() {
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GaplessInfo getGaplessInfo() {
|
||||||
|
return gaplessInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
Metadata that = (Metadata) o;
|
||||||
|
|
||||||
|
if (!frames.equals(that.frames)) return false;
|
||||||
|
return gaplessInfo != null ? gaplessInfo.equals(that.gaplessInfo) : that.gaplessInfo == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = frames.hashCode();
|
||||||
|
result = 31 * result + (gaplessInfo != null ? gaplessInfo.hashCode() : 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderDelay : -1);
|
||||||
|
dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderPadding : -1);
|
||||||
|
dest.writeArray(frames.toArray(new Id3Frame[frames.size()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.metadata;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||||
|
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder for ID3 style metadata.
|
||||||
|
*/
|
||||||
|
public class MetadataBuilder {
|
||||||
|
private List<Id3Frame> frames = new ArrayList<>();
|
||||||
|
private GaplessInfo gaplessInfo;
|
||||||
|
|
||||||
|
public void add(Id3Frame frame) {
|
||||||
|
frames.add(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGaplessInfo(GaplessInfo info) {
|
||||||
|
this.gaplessInfo = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metadata build() {
|
||||||
|
return !frames.isEmpty() || gaplessInfo != null ? new Metadata(frames, gaplessInfo): null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* APIC (Attached Picture) ID3 frame.
|
* APIC (Attached Picture) ID3 frame.
|
||||||
*/
|
*/
|
||||||
|
|
@ -35,4 +39,62 @@ public final class ApicFrame extends Id3Frame {
|
||||||
this.pictureData = pictureData;
|
this.pictureData = pictureData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ApicFrame(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
mimeType = in.readString();
|
||||||
|
description = in.readString();
|
||||||
|
pictureType = in.readInt();
|
||||||
|
pictureData = in.createByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
ApicFrame that = (ApicFrame) o;
|
||||||
|
|
||||||
|
if (id != null ? !id.equals(that.id) : that.id != null) return false;
|
||||||
|
if (pictureType != that.pictureType) return false;
|
||||||
|
if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null)
|
||||||
|
return false;
|
||||||
|
if (description != null ? !description.equals(that.description) : that.description != null)
|
||||||
|
return false;
|
||||||
|
return Arrays.equals(pictureData, that.pictureData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = id != null ? id.hashCode() : 0;
|
||||||
|
result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
|
||||||
|
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||||
|
result = 31 * result + pictureType;
|
||||||
|
result = 31 * result + Arrays.hashCode(pictureData);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeString(id);
|
||||||
|
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.
|
||||||
*/
|
*/
|
||||||
|
|
@ -27,4 +31,49 @@ public final class BinaryFrame extends Id3Frame {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BinaryFrame(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
data = in.createByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
BinaryFrame that = (BinaryFrame) o;
|
||||||
|
|
||||||
|
if (id != null ? !id.equals(that.id) : that.id != null)
|
||||||
|
return false;
|
||||||
|
return Arrays.equals(data, that.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = id != null ? id.hashCode() : 0;
|
||||||
|
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,83 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comment ID3 frame.
|
||||||
|
*/
|
||||||
|
public final class CommentFrame extends Id3Frame {
|
||||||
|
|
||||||
|
public final String language;
|
||||||
|
public final String text;
|
||||||
|
|
||||||
|
public CommentFrame(String language, String description, String text) {
|
||||||
|
super(description);
|
||||||
|
this.language = language;
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommentFrame(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
language = in.readString();
|
||||||
|
text = in.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
CommentFrame that = (CommentFrame) o;
|
||||||
|
|
||||||
|
if (id != null ? !id.equals(that.id) : that.id != null) return false;
|
||||||
|
if (language != null ? !language.equals(that.language) : that.language != null) return false;
|
||||||
|
return text != null ? text.equals(that.text) : that.text == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = id != null ? id.hashCode() : 0;
|
||||||
|
result = 31 * result + (language != null ? language.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,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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GEOB (General Encapsulated Object) ID3 frame.
|
* GEOB (General Encapsulated Object) ID3 frame.
|
||||||
*/
|
*/
|
||||||
|
|
@ -35,4 +39,63 @@ public final class GeobFrame extends Id3Frame {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GeobFrame(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
mimeType = in.readString();
|
||||||
|
filename = in.readString();
|
||||||
|
description = in.readString();
|
||||||
|
data = in.createByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
GeobFrame that = (GeobFrame) o;
|
||||||
|
|
||||||
|
if (id != null ? !id.equals(that.id) : that.id != null) return false;
|
||||||
|
if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null)
|
||||||
|
return false;
|
||||||
|
if (filename != null ? !filename.equals(that.filename) : that.filename != null)
|
||||||
|
return false;
|
||||||
|
if (description != null ? !description.equals(that.description) : that.description != null)
|
||||||
|
return false;
|
||||||
|
return Arrays.equals(data, that.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = id != null ? id.hashCode() : 0;
|
||||||
|
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(id);
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
package com.google.android.exoplayer2.metadata.id3;
|
package com.google.android.exoplayer2.metadata.id3;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.metadata.MetadataDecoder;
|
import com.google.android.exoplayer2.metadata.MetadataDecoder;
|
||||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
@ -23,69 +25,141 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
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 individual TXXX text frames from raw ID3 data.
|
||||||
*/
|
*/
|
||||||
public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
|
public final class Id3Decoder implements MetadataDecoder<Metadata> {
|
||||||
|
|
||||||
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;
|
||||||
private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
|
private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
|
||||||
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
|
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
|
||||||
|
|
||||||
|
private int majorVersion;
|
||||||
|
private int minorVersion;
|
||||||
|
private boolean isUnsynchronized;
|
||||||
|
private GaplessInfo gaplessInfo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canDecode(String mimeType) {
|
public boolean canDecode(String mimeType) {
|
||||||
return mimeType.equals(MimeTypes.APPLICATION_ID3);
|
return mimeType.equals(MimeTypes.APPLICATION_ID3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Id3Frame> decode(byte[] data, int size) throws MetadataDecoderException {
|
public Metadata decode(byte[] data, int size) throws MetadataDecoderException {
|
||||||
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);
|
int id3Size = decodeId3Header(id3Data);
|
||||||
|
|
||||||
|
if (isUnsynchronized) {
|
||||||
|
id3Data = removeUnsynchronization(id3Data, id3Size);
|
||||||
|
id3Size = id3Data.bytesLeft();
|
||||||
|
}
|
||||||
|
|
||||||
while (id3Size > 0) {
|
while (id3Size > 0) {
|
||||||
int frameId0 = id3Data.readUnsignedByte();
|
int frameId0 = id3Data.readUnsignedByte();
|
||||||
int frameId1 = id3Data.readUnsignedByte();
|
int frameId1 = id3Data.readUnsignedByte();
|
||||||
int frameId2 = id3Data.readUnsignedByte();
|
int frameId2 = id3Data.readUnsignedByte();
|
||||||
int frameId3 = id3Data.readUnsignedByte();
|
int frameId3 = majorVersion > 2 ? id3Data.readUnsignedByte() : 0;
|
||||||
int frameSize = id3Data.readSynchSafeInt();
|
int frameSize = majorVersion == 2 ? id3Data.readUnsignedInt24() :
|
||||||
|
majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt();
|
||||||
|
|
||||||
if (frameSize <= 1) {
|
if (frameSize <= 1) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip frame flags.
|
// Frame flags.
|
||||||
id3Data.skipBytes(2);
|
boolean isCompressed = false;
|
||||||
|
boolean isEncrypted = false;
|
||||||
|
boolean isUnsynchronized = false;
|
||||||
|
boolean hasGroupIdentifier = false;
|
||||||
|
boolean hasDataLength = false;
|
||||||
|
|
||||||
try {
|
if (majorVersion > 2) {
|
||||||
Id3Frame frame;
|
int flags = id3Data.readShort();
|
||||||
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
|
if (majorVersion == 3) {
|
||||||
frame = decodeTxxxFrame(id3Data, frameSize);
|
isCompressed = (flags & 0x0080) != 0;
|
||||||
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
|
isEncrypted = (flags & 0x0040) != 0;
|
||||||
frame = decodePrivFrame(id3Data, frameSize);
|
hasDataLength = isCompressed;
|
||||||
} 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);
|
isCompressed = (flags & 0x0008) != 0;
|
||||||
frame = decodeBinaryFrame(id3Data, frameSize, id);
|
isEncrypted = (flags & 0x0004) != 0;
|
||||||
|
isUnsynchronized = (flags & 0x0002) != 0;
|
||||||
|
hasGroupIdentifier = (flags & 0x0040) != 0;
|
||||||
|
hasDataLength = (flags & 0x0001) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int headerSize = majorVersion == 2 ? 6 : 10;
|
||||||
|
|
||||||
|
if (hasGroupIdentifier) {
|
||||||
|
++headerSize;
|
||||||
|
--frameSize;
|
||||||
|
id3Data.skipBytes(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEncrypted) {
|
||||||
|
++headerSize;
|
||||||
|
--frameSize;
|
||||||
|
id3Data.skipBytes(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDataLength) {
|
||||||
|
headerSize += 4;
|
||||||
|
frameSize -= 4;
|
||||||
|
id3Data.skipBytes(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
id3Size -= frameSize + headerSize;
|
||||||
|
|
||||||
|
if (isCompressed || isEncrypted) {
|
||||||
|
id3Data.skipBytes(frameSize);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Id3Frame frame;
|
||||||
|
ParsableByteArray frameData = id3Data;
|
||||||
|
if (isUnsynchronized) {
|
||||||
|
frameData = removeUnsynchronization(id3Data, frameSize);
|
||||||
|
frameSize = frameData.bytesLeft();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
|
||||||
|
frame = decodeTxxxFrame(frameData, frameSize);
|
||||||
|
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
|
||||||
|
frame = decodePrivFrame(frameData, frameSize);
|
||||||
|
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
|
||||||
|
frame = decodeGeobFrame(frameData, frameSize);
|
||||||
|
} else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') {
|
||||||
|
frame = decodeApicFrame(frameData, frameSize);
|
||||||
|
} else if (frameId0 == 'T') {
|
||||||
|
String id = frameId3 != 0 ?
|
||||||
|
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
|
||||||
|
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
|
||||||
|
frame = decodeTextInformationFrame(frameData, frameSize, id);
|
||||||
|
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' &&
|
||||||
|
(frameId3 == 'M' || frameId3 == 0)) {
|
||||||
|
CommentFrame commentFrame = decodeCommentFrame(frameData, frameSize);
|
||||||
|
frame = commentFrame;
|
||||||
|
if (gaplessInfo == null) {
|
||||||
|
gaplessInfo = GaplessInfo.createFromComment(commentFrame.id, commentFrame.text);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String id = frameId3 != 0 ?
|
||||||
|
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
|
||||||
|
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
|
||||||
|
frame = decodeBinaryFrame(frameData, frameSize, id);
|
||||||
|
}
|
||||||
|
id3Frames.add(frame);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new MetadataDecoderException("Unsupported character encoding");
|
||||||
}
|
}
|
||||||
id3Frames.add(frame);
|
|
||||||
id3Size -= frameSize + 10 /* header size */;
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new MetadataDecoderException("Unsupported encoding", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Collections.unmodifiableList(id3Frames);
|
return new Metadata(id3Frames, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
|
private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
|
||||||
|
|
@ -96,7 +170,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;
|
||||||
|
|
@ -126,7 +200,7 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
|
||||||
* @return The size of ID3 frames in bytes, excluding the header and footer.
|
* @return The size of ID3 frames in bytes, excluding the header and footer.
|
||||||
* @throws ParserException If ID3 file identifier != "ID3".
|
* @throws ParserException If ID3 file identifier != "ID3".
|
||||||
*/
|
*/
|
||||||
private static int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException {
|
private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException {
|
||||||
int id1 = id3Buffer.readUnsignedByte();
|
int id1 = id3Buffer.readUnsignedByte();
|
||||||
int id2 = id3Buffer.readUnsignedByte();
|
int id2 = id3Buffer.readUnsignedByte();
|
||||||
int id3 = id3Buffer.readUnsignedByte();
|
int id3 = id3Buffer.readUnsignedByte();
|
||||||
|
|
@ -134,23 +208,41 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
|
||||||
throw new MetadataDecoderException(String.format(Locale.US,
|
throw new MetadataDecoderException(String.format(Locale.US,
|
||||||
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
|
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
|
||||||
}
|
}
|
||||||
id3Buffer.skipBytes(2); // Skip version.
|
|
||||||
|
majorVersion = id3Buffer.readUnsignedByte();
|
||||||
|
minorVersion = id3Buffer.readUnsignedByte();
|
||||||
|
|
||||||
int flags = id3Buffer.readUnsignedByte();
|
int flags = id3Buffer.readUnsignedByte();
|
||||||
int id3Size = id3Buffer.readSynchSafeInt();
|
int id3Size = id3Buffer.readSynchSafeInt();
|
||||||
|
|
||||||
// Check if extended header presents.
|
if (majorVersion < 4) {
|
||||||
if ((flags & 0x2) != 0) {
|
// this flag is advisory in version 4, use the frame flags instead
|
||||||
int extendedHeaderSize = id3Buffer.readSynchSafeInt();
|
isUnsynchronized = (flags & 0x80) != 0;
|
||||||
if (extendedHeaderSize > 4) {
|
|
||||||
id3Buffer.skipBytes(extendedHeaderSize - 4);
|
|
||||||
}
|
|
||||||
id3Size -= extendedHeaderSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if footer presents.
|
if (majorVersion == 3) {
|
||||||
if ((flags & 0x8) != 0) {
|
// check for extended header
|
||||||
id3Size -= 10;
|
if ((flags & 0x40) != 0) {
|
||||||
|
int extendedHeaderSize = id3Buffer.readInt(); // size excluding size field
|
||||||
|
if (extendedHeaderSize == 6 || extendedHeaderSize == 10) {
|
||||||
|
id3Buffer.skipBytes(extendedHeaderSize);
|
||||||
|
id3Size -= (extendedHeaderSize + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (majorVersion >= 4) {
|
||||||
|
// check for extended header
|
||||||
|
if ((flags & 0x40) != 0) {
|
||||||
|
int extendedHeaderSize = id3Buffer.readSynchSafeInt(); // size including size field
|
||||||
|
if (extendedHeaderSize > 4) {
|
||||||
|
id3Buffer.skipBytes(extendedHeaderSize - 4);
|
||||||
|
}
|
||||||
|
id3Size -= extendedHeaderSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if footer presents.
|
||||||
|
if ((flags & 0x10) != 0) {
|
||||||
|
id3Size -= 10;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return id3Size;
|
return id3Size;
|
||||||
|
|
@ -253,6 +345,28 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
|
||||||
return new TextInformationFrame(id, description);
|
return new TextInformationFrame(id, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
|
||||||
|
int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
|
||||||
|
String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
|
||||||
|
|
||||||
|
return new CommentFrame(language, description, value);
|
||||||
|
}
|
||||||
|
|
||||||
private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,
|
private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,
|
||||||
String id) {
|
String id) {
|
||||||
byte[] frame = new byte[frameSize];
|
byte[] frame = new byte[frameSize];
|
||||||
|
|
@ -261,6 +375,37 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
|
||||||
return new BinaryFrame(id, frame);
|
return new BinaryFrame(id, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo the unsynchronization applied to one or more frames.
|
||||||
|
* @param dataSource The original data, positioned at the beginning of a frame.
|
||||||
|
* @param count The number of valid bytes in the frames to be processed.
|
||||||
|
* @return replacement data for the frames.
|
||||||
|
*/
|
||||||
|
private static ParsableByteArray removeUnsynchronization(ParsableByteArray dataSource, int count) {
|
||||||
|
byte[] source = dataSource.data;
|
||||||
|
int sourceIndex = dataSource.getPosition();
|
||||||
|
int limit = sourceIndex + count;
|
||||||
|
byte[] dest = new byte[count];
|
||||||
|
int destIndex = 0;
|
||||||
|
|
||||||
|
while (sourceIndex < limit) {
|
||||||
|
byte b = source[sourceIndex++];
|
||||||
|
if ((b & 0xFF) == 0xFF) {
|
||||||
|
int nextIndex = sourceIndex+1;
|
||||||
|
if (nextIndex < limit) {
|
||||||
|
int b2 = source[nextIndex];
|
||||||
|
if (b2 == 0) {
|
||||||
|
// skip the 0 byte
|
||||||
|
++sourceIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dest[destIndex++] = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ParsableByteArray(dest, destIndex);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
|
@ -281,4 +426,52 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final static String[] standardGenres = 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"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static String decodeGenre(int n)
|
||||||
|
{
|
||||||
|
n--;
|
||||||
|
|
||||||
|
if (n < 0 || n >= standardGenres.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return standardGenres[n];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,13 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.metadata.id3;
|
package com.google.android.exoplayer2.metadata.id3;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for ID3 frames.
|
* Base class for ID3 frames.
|
||||||
*/
|
*/
|
||||||
public abstract class Id3Frame {
|
public abstract class Id3Frame implements Parcelable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The frame ID.
|
* The frame ID.
|
||||||
|
|
@ -29,4 +32,13 @@ public abstract class Id3Frame {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Id3Frame(Parcel in) {
|
||||||
|
id = in.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PRIV (Private) ID3 frame.
|
* PRIV (Private) ID3 frame.
|
||||||
*/
|
*/
|
||||||
|
|
@ -31,4 +35,52 @@ public final class PrivFrame extends Id3Frame {
|
||||||
this.privateData = privateData;
|
this.privateData = privateData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PrivFrame(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
owner = in.readString();
|
||||||
|
privateData = in.createByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
PrivFrame that = (PrivFrame) o;
|
||||||
|
|
||||||
|
if (id != null ? !id.equals(that.id) : that.id != null) return false;
|
||||||
|
if (owner != null ? !owner.equals(that.owner) : that.owner != null) return false;
|
||||||
|
return Arrays.equals(privateData, that.privateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = id != null ? id.hashCode() : 0;
|
||||||
|
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(id);
|
||||||
|
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,9 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.metadata.id3;
|
package com.google.android.exoplayer2.metadata.id3;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame.
|
* Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame.
|
||||||
*/
|
*/
|
||||||
|
|
@ -27,4 +30,48 @@ public final class TextInformationFrame extends Id3Frame {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TextInformationFrame(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
description = in.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
TextInformationFrame that = (TextInformationFrame) o;
|
||||||
|
|
||||||
|
if (id != null ? !id.equals(that.id) : that.id != null) return false;
|
||||||
|
return description != null ? description.equals(that.description) : that.description == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = id != null ? id.hashCode() : 0;
|
||||||
|
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,9 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.metadata.id3;
|
package com.google.android.exoplayer2.metadata.id3;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TXXX (User defined text information) ID3 frame.
|
* TXXX (User defined text information) ID3 frame.
|
||||||
*/
|
*/
|
||||||
|
|
@ -31,4 +34,53 @@ public final class TxxxFrame extends Id3Frame {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TxxxFrame(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
description = in.readString();
|
||||||
|
value = in.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
TxxxFrame that = (TxxxFrame) o;
|
||||||
|
|
||||||
|
if (id != null ? !id.equals(that.id) : that.id != null) return false;
|
||||||
|
if (description != null ? !description.equals(that.description) : that.description != null)
|
||||||
|
return false;
|
||||||
|
return value != null ? value.equals(that.value) : that.value == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = id != null ? id.hashCode() : 0;
|
||||||
|
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(id);
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,22 +142,13 @@ public final class ContentDataSource implements DataSource {
|
||||||
@Override
|
@Override
|
||||||
public void close() throws ContentDataSourceException {
|
public void close() throws ContentDataSourceException {
|
||||||
uri = null;
|
uri = null;
|
||||||
try {
|
if (inputStream != null) {
|
||||||
if (inputStream != null) {
|
|
||||||
inputStream.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ContentDataSourceException(e);
|
|
||||||
} finally {
|
|
||||||
inputStream = null;
|
|
||||||
try {
|
try {
|
||||||
if (assetFileDescriptor != null) {
|
inputStream.close();
|
||||||
assetFileDescriptor.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ContentDataSourceException(e);
|
throw new ContentDataSourceException(e);
|
||||||
} finally {
|
} finally {
|
||||||
assetFileDescriptor = null;
|
inputStream = null;
|
||||||
if (opened) {
|
if (opened) {
|
||||||
opened = false;
|
opened = false;
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
|
|
@ -166,6 +157,13 @@ public final class ContentDataSource implements DataSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if (assetFileDescriptor != null) {
|
||||||
|
try {
|
||||||
|
assetFileDescriptor.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
assetFileDescriptor = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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…
Reference in a new issue