mirror of
https://github.com/androidx/media.git
synced 2025-05-21 23:56:32 +08:00
Support ID3/Apple metadata parsing in MP3 and MP4 files
This commit is contained in:
parent
c54169c192
commit
18ab96349e
@ -27,8 +27,11 @@ import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.GeobFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
|
||||
@ -54,7 +57,7 @@ import java.util.Locale;
|
||||
/* package */ final class EventLogger implements ExoPlayer.EventListener,
|
||||
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
|
||||
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener,
|
||||
MappingTrackSelector.EventListener, MetadataRenderer.Output<List<Id3Frame>> {
|
||||
MappingTrackSelector.EventListener, MetadataRenderer.Output<Metadata> {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
private static final int MAX_TIMELINE_ITEM_LINES = 3;
|
||||
@ -173,10 +176,11 @@ import java.util.Locale;
|
||||
Log.d(TAG, "]");
|
||||
}
|
||||
|
||||
// MetadataRenderer.Output<List<Id3Frame>>
|
||||
// MetadataRenderer.Output<Metadata>
|
||||
|
||||
@Override
|
||||
public void onMetadata(List<Id3Frame> id3Frames) {
|
||||
public void onMetadata(Metadata metadata) {
|
||||
List<Id3Frame> id3Frames = metadata.getFrames();
|
||||
for (Id3Frame id3Frame : id3Frames) {
|
||||
if (id3Frame instanceof TxxxFrame) {
|
||||
TxxxFrame txxxFrame = (TxxxFrame) id3Frame;
|
||||
@ -197,10 +201,19 @@ import java.util.Locale;
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame;
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id,
|
||||
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 {
|
||||
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
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.test.MoreAsserts;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import java.util.List;
|
||||
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,
|
||||
54, 52, 95, 115, 116, 97, 114, 116, 0};
|
||||
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());
|
||||
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0);
|
||||
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,
|
||||
111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
|
||||
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());
|
||||
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0);
|
||||
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,
|
||||
3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0};
|
||||
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());
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0);
|
||||
assertEquals("TIT2", textInformationFrame.id);
|
||||
|
@ -21,6 +21,8 @@ import android.media.MediaFormat;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -100,6 +102,11 @@ public final class Format implements Parcelable {
|
||||
* DRM initialization data if the stream is protected, or null otherwise.
|
||||
*/
|
||||
public final DrmInitData drmInitData;
|
||||
/**
|
||||
* Static metadata
|
||||
*/
|
||||
public final Metadata metadata;
|
||||
|
||||
|
||||
// Video specific.
|
||||
|
||||
@ -196,7 +203,7 @@ public final class Format implements Parcelable {
|
||||
float frameRate, List<byte[]> initializationData) {
|
||||
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,
|
||||
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,
|
||||
@ -222,7 +229,7 @@ public final class Format implements Parcelable {
|
||||
return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height,
|
||||
frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData,
|
||||
drmInitData);
|
||||
drmInitData, null);
|
||||
}
|
||||
|
||||
// Audio.
|
||||
@ -233,7 +240,7 @@ public final class Format implements Parcelable {
|
||||
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, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, initializationData,
|
||||
null);
|
||||
null, null);
|
||||
}
|
||||
|
||||
public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
@ -260,7 +267,7 @@ public final class Format implements Parcelable {
|
||||
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,
|
||||
encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE,
|
||||
initializationData, drmInitData);
|
||||
initializationData, drmInitData, null);
|
||||
}
|
||||
|
||||
// Text.
|
||||
@ -269,7 +276,7 @@ public final class Format implements Parcelable {
|
||||
String sampleMimeType, String codecs, int bitrate, int selectionFlags, String language) {
|
||||
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, 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,
|
||||
@ -283,7 +290,7 @@ public final class Format implements Parcelable {
|
||||
long subsampleOffsetUs) {
|
||||
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, selectionFlags, language, subsampleOffsetUs, null, drmInitData);
|
||||
NO_VALUE, selectionFlags, language, subsampleOffsetUs, null, drmInitData, null);
|
||||
}
|
||||
|
||||
// Image.
|
||||
@ -292,7 +299,7 @@ public final class Format implements Parcelable {
|
||||
int bitrate, List<byte[]> initializationData, String language, DrmInitData drmInitData) {
|
||||
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, 0, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData);
|
||||
NO_VALUE, 0, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null);
|
||||
}
|
||||
|
||||
// Generic.
|
||||
@ -301,14 +308,14 @@ public final class Format implements Parcelable {
|
||||
String sampleMimeType, int bitrate) {
|
||||
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, 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,
|
||||
int bitrate, DrmInitData drmInitData) {
|
||||
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, 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,
|
||||
@ -316,7 +323,7 @@ public final class Format implements Parcelable {
|
||||
float pixelWidthHeightRatio, byte[] projectionData, int stereoMode, int channelCount,
|
||||
int sampleRate, int pcmEncoding, int encoderDelay, int encoderPadding, int selectionFlags,
|
||||
String language, long subsampleOffsetUs, List<byte[]> initializationData,
|
||||
DrmInitData drmInitData) {
|
||||
DrmInitData drmInitData, Metadata metadata) {
|
||||
this.id = id;
|
||||
this.containerMimeType = containerMimeType;
|
||||
this.sampleMimeType = sampleMimeType;
|
||||
@ -341,6 +348,7 @@ public final class Format implements Parcelable {
|
||||
this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
|
||||
: initializationData;
|
||||
this.drmInitData = drmInitData;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
/* package */ Format(Parcel in) {
|
||||
@ -372,20 +380,21 @@ public final class Format implements Parcelable {
|
||||
initializationData.add(in.createByteArray());
|
||||
}
|
||||
drmInitData = in.readParcelable(DrmInitData.class.getClassLoader());
|
||||
metadata = in.readParcelable(Metadata.class.getClassLoader());
|
||||
}
|
||||
|
||||
public Format copyWithMaxInputSize(int maxInputSize) {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
|
||||
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
|
||||
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
|
||||
}
|
||||
|
||||
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
|
||||
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
|
||||
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
|
||||
}
|
||||
|
||||
public Format copyWithContainerInfo(String id, int bitrate, int width, int height,
|
||||
@ -393,7 +402,7 @@ public final class Format implements Parcelable {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
|
||||
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
|
||||
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
|
||||
}
|
||||
|
||||
public Format copyWithManifestFormatInfo(Format manifestFormat,
|
||||
@ -409,21 +418,32 @@ public final class Format implements Parcelable {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
|
||||
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
|
||||
channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags,
|
||||
language, subsampleOffsetUs, initializationData, drmInitData);
|
||||
language, subsampleOffsetUs, initializationData, drmInitData, null);
|
||||
}
|
||||
|
||||
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
|
||||
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
|
||||
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
|
||||
}
|
||||
|
||||
public Format copyWithDrmInitData(DrmInitData drmInitData) {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
|
||||
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
|
||||
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -483,6 +503,7 @@ public final class Format implements Parcelable {
|
||||
result = 31 * result + sampleRate;
|
||||
result = 31 * result + (language == null ? 0 : language.hashCode());
|
||||
result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode());
|
||||
result = 31 * result + (metadata == null ? 0 : metadata.hashCode());
|
||||
hashCode = result;
|
||||
}
|
||||
return hashCode;
|
||||
@ -510,6 +531,7 @@ public final class Format implements Parcelable {
|
||||
|| !Util.areEqual(sampleMimeType, other.sampleMimeType)
|
||||
|| !Util.areEqual(codecs, other.codecs)
|
||||
|| !Util.areEqual(drmInitData, other.drmInitData)
|
||||
|| !Util.areEqual(metadata, other.metadata)
|
||||
|| !Arrays.equals(projectionData, other.projectionData)
|
||||
|| initializationData.size() != other.initializationData.size()) {
|
||||
return false;
|
||||
@ -582,6 +604,7 @@ public final class Format implements Parcelable {
|
||||
dest.writeByteArray(initializationData.get(i));
|
||||
}
|
||||
dest.writeParcelable(drmInitData, 0);
|
||||
dest.writeParcelable(metadata, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,9 +31,9 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
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.id3.Id3Decoder;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.text.TextRenderer;
|
||||
@ -100,7 +100,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||
|
||||
private SurfaceHolder surfaceHolder;
|
||||
private TextRenderer.Output textOutput;
|
||||
private MetadataRenderer.Output<List<Id3Frame>> id3Output;
|
||||
private MetadataRenderer.Output<Metadata> id3Output;
|
||||
private VideoListener videoListener;
|
||||
private AudioRendererEventListener audioDebugListener;
|
||||
private VideoRendererEventListener videoDebugListener;
|
||||
@ -345,7 +345,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||
*
|
||||
* @param output The output.
|
||||
*/
|
||||
public void setId3Output(MetadataRenderer.Output<List<Id3Frame>> output) {
|
||||
public void setId3Output(MetadataRenderer.Output<Metadata> output) {
|
||||
id3Output = output;
|
||||
}
|
||||
|
||||
@ -484,7 +484,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||
Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper());
|
||||
renderersList.add(textRenderer);
|
||||
|
||||
MetadataRenderer<List<Id3Frame>> id3Renderer = new MetadataRenderer<>(componentListener,
|
||||
MetadataRenderer<Metadata> id3Renderer = new MetadataRenderer<>(componentListener,
|
||||
mainHandler.getLooper(), new Id3Decoder());
|
||||
renderersList.add(id3Renderer);
|
||||
}
|
||||
@ -565,7 +565,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||
}
|
||||
|
||||
private final class ComponentListener implements VideoRendererEventListener,
|
||||
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<List<Id3Frame>>,
|
||||
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<Metadata>,
|
||||
SurfaceHolder.Callback {
|
||||
|
||||
// VideoRendererEventListener implementation
|
||||
@ -696,12 +696,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
// MetadataRenderer.Output<List<Id3Frame>> implementation
|
||||
// MetadataRenderer.Output<Metadata> implementation
|
||||
|
||||
@Override
|
||||
public void onMetadata(List<Id3Frame> id3Frames) {
|
||||
public void onMetadata(Metadata metadata) {
|
||||
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;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Holder for gapless playback information.
|
||||
*/
|
||||
public final class GaplessInfoHolder {
|
||||
|
||||
private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
|
||||
private static final Pattern GAPLESS_COMMENT_PATTERN =
|
||||
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
|
||||
|
||||
/**
|
||||
* The number of samples to trim from the start of the decoded audio stream, or
|
||||
* {@link Format#NO_VALUE} if not set.
|
||||
*/
|
||||
public int encoderDelay;
|
||||
|
||||
/**
|
||||
* The number of samples to trim from the end of the decoded audio stream, or
|
||||
* {@link Format#NO_VALUE} if not set.
|
||||
*/
|
||||
public int encoderPadding;
|
||||
|
||||
/**
|
||||
* Creates a new holder for gapless playback information.
|
||||
*/
|
||||
public GaplessInfoHolder() {
|
||||
encoderDelay = Format.NO_VALUE;
|
||||
encoderPadding = Format.NO_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the holder with data from an MP3 Xing header, if valid and non-zero.
|
||||
*
|
||||
* @param value The 24-bit value to decode.
|
||||
* @return Whether the holder was populated.
|
||||
*/
|
||||
public boolean setFromXingHeaderValue(int value) {
|
||||
int encoderDelay = value >> 12;
|
||||
int encoderPadding = value & 0x0FFF;
|
||||
if (encoderDelay > 0 || encoderPadding > 0) {
|
||||
this.encoderDelay = encoderDelay;
|
||||
this.encoderPadding = encoderPadding;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
|
||||
* or MPEG 4 user data), if valid and non-zero.
|
||||
*
|
||||
* @param name The comment's identifier.
|
||||
* @param data The comment's payload data.
|
||||
* @return Whether the holder was populated.
|
||||
*/
|
||||
public boolean setFromComment(String name, String data) {
|
||||
if (!GAPLESS_COMMENT_ID.equals(name)) {
|
||||
return false;
|
||||
}
|
||||
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
int encoderDelay = Integer.parseInt(matcher.group(1), 16);
|
||||
int encoderPadding = Integer.parseInt(matcher.group(2), 16);
|
||||
if (encoderDelay > 0 || encoderPadding > 0) {
|
||||
this.encoderDelay = encoderDelay;
|
||||
this.encoderPadding = encoderPadding;
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore incorrectly formatted comments.
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set.
|
||||
*/
|
||||
public boolean hasGaplessInfo() {
|
||||
return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;
|
||||
}
|
||||
public GaplessInfo gaplessInfo;
|
||||
|
||||
}
|
||||
|
@ -15,13 +15,13 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor.mp3;
|
||||
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer2.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.Util;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* 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 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 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 InterruptedException If the thread was interrupted.
|
||||
*/
|
||||
public static void parseId3(ExtractorInput input, GaplessInfoHolder out)
|
||||
public static Metadata parseId3(ExtractorInput input)
|
||||
throws IOException, InterruptedException {
|
||||
Metadata result = null;
|
||||
ParsableByteArray scratch = new ParsableByteArray(10);
|
||||
int peekedId3Bytes = 0;
|
||||
while (true) {
|
||||
@ -60,18 +59,26 @@ import java.nio.charset.Charset;
|
||||
int minorVersion = scratch.readUnsignedByte();
|
||||
int flags = scratch.readUnsignedByte();
|
||||
int length = scratch.readSynchSafeInt();
|
||||
if (!out.hasGaplessInfo() && canParseMetadata(majorVersion, minorVersion, flags, length)) {
|
||||
byte[] frame = new byte[length];
|
||||
input.peekFully(frame, 0, length);
|
||||
parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags, out);
|
||||
} else {
|
||||
input.advancePeekPosition(length);
|
||||
int frameLength = length + 10;
|
||||
|
||||
try {
|
||||
if (canParseMetadata(majorVersion, minorVersion, flags, length)) {
|
||||
input.resetPeekPosition();
|
||||
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.advancePeekPosition(peekedId3Bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
|
||||
@ -83,211 +90,6 @@ import java.nio.charset.Charset;
|
||||
&& !(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() {}
|
||||
|
||||
}
|
||||
|
@ -22,11 +22,12 @@ import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.EOFException;
|
||||
@ -69,7 +70,7 @@ public final class Mp3Extractor implements Extractor {
|
||||
private final long forcedFirstSampleTimestampUs;
|
||||
private final ParsableByteArray scratch;
|
||||
private final MpegAudioHeader synchronizedHeader;
|
||||
private final GaplessInfoHolder gaplessInfoHolder;
|
||||
private Metadata metadata;
|
||||
|
||||
// Extractor outputs.
|
||||
private ExtractorOutput extractorOutput;
|
||||
@ -99,7 +100,6 @@ public final class Mp3Extractor implements Extractor {
|
||||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||
scratch = new ParsableByteArray(4);
|
||||
synchronizedHeader = new MpegAudioHeader();
|
||||
gaplessInfoHolder = new GaplessInfoHolder();
|
||||
basisTimeUs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
@ -137,10 +137,21 @@ public final class Mp3Extractor implements Extractor {
|
||||
if (seeker == null) {
|
||||
seeker = setupSeeker(input);
|
||||
extractorOutput.seekMap(seeker);
|
||||
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
|
||||
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
|
||||
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
|
||||
gaplessInfoHolder.encoderPadding, null, null, 0, null));
|
||||
|
||||
GaplessInfo gaplessInfo = metadata != null ? metadata.getGaplessInfo() : null;
|
||||
|
||||
Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
|
||||
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
|
||||
synchronizedHeader.sampleRate, Format.NO_VALUE,
|
||||
gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE,
|
||||
gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE,
|
||||
null, null, 0, null);
|
||||
|
||||
if (metadata != null) {
|
||||
format = format.copyWithMetadata(metadata);
|
||||
}
|
||||
|
||||
trackOutput.format(format);
|
||||
}
|
||||
return readSample(input);
|
||||
}
|
||||
@ -220,7 +231,7 @@ public final class Mp3Extractor implements Extractor {
|
||||
int peekedId3Bytes = 0;
|
||||
input.resetPeekPosition();
|
||||
if (input.getPosition() == 0) {
|
||||
Id3Util.parseId3(input, gaplessInfoHolder);
|
||||
metadata = Id3Util.parseId3(input);
|
||||
peekedId3Bytes = (int) input.getPeekPosition();
|
||||
if (!sniffing) {
|
||||
input.skipFully(peekedId3Bytes);
|
||||
@ -303,13 +314,16 @@ public final class Mp3Extractor implements Extractor {
|
||||
Seeker seeker = null;
|
||||
if (headerData == XING_HEADER || headerData == INFO_HEADER) {
|
||||
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.
|
||||
input.resetPeekPosition();
|
||||
input.advancePeekPosition(xingBase + 141);
|
||||
input.peekFully(scratch.data, 0, 3);
|
||||
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);
|
||||
} else {
|
||||
|
@ -21,7 +21,15 @@ import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.audio.Ac3Util;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataBuilder;
|
||||
import com.google.android.exoplayer2.metadata.id3.BinaryFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
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.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
@ -270,7 +278,7 @@ import java.util.List;
|
||||
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.
|
||||
// This implementation does not support applying both gapless metadata and an edit list.
|
||||
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
|
||||
@ -299,10 +307,9 @@ import java.util.List;
|
||||
track.format.sampleRate, track.timescale);
|
||||
long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,
|
||||
track.format.sampleRate, track.timescale);
|
||||
if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE
|
||||
if ((encoderDelay > 0 || encoderPadding > 0) && encoderDelay <= Integer.MAX_VALUE
|
||||
&& encoderPadding <= Integer.MAX_VALUE) {
|
||||
gaplessInfoHolder.encoderDelay = (int) encoderDelay;
|
||||
gaplessInfoHolder.encoderPadding = (int) encoderPadding;
|
||||
gaplessInfoHolder.gaplessInfo = new GaplessInfo((int) encoderDelay, (int) encoderPadding);
|
||||
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
|
||||
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
|
||||
}
|
||||
@ -387,17 +394,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 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) {
|
||||
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
|
||||
// decode one.
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
ParsableByteArray udtaData = udtaAtom.data;
|
||||
udtaData.setPosition(Atom.HEADER_SIZE);
|
||||
@ -407,14 +414,15 @@ import java.util.List;
|
||||
if (atomType == Atom.TYPE_meta) {
|
||||
udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE);
|
||||
udtaData.setLimit(udtaData.getPosition() + atomSize);
|
||||
parseMetaAtom(udtaData, out);
|
||||
parseMetaAtom(udtaData);
|
||||
break;
|
||||
}
|
||||
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);
|
||||
ParsableByteArray ilst = new ParsableByteArray();
|
||||
while (data.bytesLeft() >= Atom.HEADER_SIZE) {
|
||||
@ -423,47 +431,333 @@ import java.util.List;
|
||||
if (atomType == Atom.TYPE_ilst) {
|
||||
ilst.reset(data.data, data.getPosition() + payloadSize);
|
||||
ilst.setPosition(data.getPosition());
|
||||
parseIlst(ilst, out);
|
||||
if (out.hasGaplessInfo()) {
|
||||
return;
|
||||
Metadata result = parseIlst(ilst);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
int position = ilst.getPosition();
|
||||
int endPosition = position + ilst.readInt();
|
||||
int type = ilst.readInt();
|
||||
if (type == Atom.TYPE_DASHES) {
|
||||
String lastCommentMean = null;
|
||||
String lastCommentName = null;
|
||||
String lastCommentData = null;
|
||||
while (ilst.getPosition() < endPosition) {
|
||||
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||
int key = ilst.readInt();
|
||||
ilst.skipBytes(4);
|
||||
if (key == Atom.TYPE_mean) {
|
||||
lastCommentMean = ilst.readString(length);
|
||||
} else if (key == Atom.TYPE_name) {
|
||||
lastCommentName = ilst.readString(length);
|
||||
} else if (key == Atom.TYPE_data) {
|
||||
ilst.skipBytes(4);
|
||||
lastCommentData = ilst.readString(length - 4);
|
||||
} else {
|
||||
ilst.skipBytes(length);
|
||||
parseIlstElement(ilst, type, endPosition, builder);
|
||||
ilst.setPosition(endPosition);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static final String P1 = "\u00a9";
|
||||
private static final String P2 = "\ufffd";
|
||||
private static final int TYPE_NAME_1 = Util.getIntegerCodeForString(P1 + "nam");
|
||||
private static final int TYPE_NAME_2 = Util.getIntegerCodeForString(P2 + "nam");
|
||||
private static final int TYPE_NAME_3 = Util.getIntegerCodeForString(P1 + "trk");
|
||||
private static final int TYPE_NAME_4 = Util.getIntegerCodeForString(P2 + "trk");
|
||||
private static final int TYPE_COMMENT_1 = Util.getIntegerCodeForString(P1 + "cmt");
|
||||
private static final int TYPE_COMMENT_2 = Util.getIntegerCodeForString(P2 + "cmt");
|
||||
private static final int TYPE_YEAR_1 = Util.getIntegerCodeForString(P1 + "day");
|
||||
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.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
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.NalUnitUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
@ -309,11 +311,16 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
||||
long durationUs = C.TIME_UNSET;
|
||||
List<Mp4Track> tracks = new ArrayList<>();
|
||||
long earliestSampleOffset = Long.MAX_VALUE;
|
||||
GaplessInfo gaplessInfo = null;
|
||||
Metadata metadata = null;
|
||||
|
||||
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
|
||||
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
|
||||
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++) {
|
||||
@ -330,7 +337,10 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
||||
|
||||
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
|
||||
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
|
||||
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
|
||||
gaplessInfoHolder.gaplessInfo = gaplessInfo;
|
||||
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
|
||||
gaplessInfo = gaplessInfoHolder.gaplessInfo;
|
||||
if (trackSampleTable.sampleCount == 0) {
|
||||
continue;
|
||||
}
|
||||
@ -340,9 +350,11 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
||||
// Allow ten source samples per output sample, like the platform extractor.
|
||||
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
|
||||
Format format = track.format.copyWithMaxInputSize(maxInputSize);
|
||||
if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) {
|
||||
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
|
||||
gaplessInfoHolder.encoderPadding);
|
||||
if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfo != null) {
|
||||
format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding);
|
||||
}
|
||||
if (metadata != null) {
|
||||
format = format.copyWithMetadata(metadata);
|
||||
}
|
||||
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;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* APIC (Attached Picture) ID3 frame.
|
||||
*/
|
||||
@ -35,4 +39,62 @@ public final class ApicFrame extends Id3Frame {
|
||||
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;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Binary ID3 frame.
|
||||
*/
|
||||
@ -27,4 +31,49 @@ public final class BinaryFrame extends Id3Frame {
|
||||
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;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* GEOB (General Encapsulated Object) ID3 frame.
|
||||
*/
|
||||
@ -35,4 +39,63 @@ public final class GeobFrame extends Id3Frame {
|
||||
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;
|
||||
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoder;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
@ -23,69 +25,141 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 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_UTF_16 = 1;
|
||||
private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
|
||||
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
|
||||
|
||||
private int majorVersion;
|
||||
private int minorVersion;
|
||||
private boolean isUnsynchronized;
|
||||
private GaplessInfo gaplessInfo;
|
||||
|
||||
@Override
|
||||
public boolean canDecode(String mimeType) {
|
||||
return mimeType.equals(MimeTypes.APPLICATION_ID3);
|
||||
}
|
||||
|
||||
@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<>();
|
||||
ParsableByteArray id3Data = new ParsableByteArray(data, size);
|
||||
int id3Size = decodeId3Header(id3Data);
|
||||
|
||||
if (isUnsynchronized) {
|
||||
id3Data = removeUnsynchronization(id3Data, id3Size);
|
||||
id3Size = id3Data.bytesLeft();
|
||||
}
|
||||
|
||||
while (id3Size > 0) {
|
||||
int frameId0 = id3Data.readUnsignedByte();
|
||||
int frameId1 = id3Data.readUnsignedByte();
|
||||
int frameId2 = id3Data.readUnsignedByte();
|
||||
int frameId3 = id3Data.readUnsignedByte();
|
||||
int frameSize = id3Data.readSynchSafeInt();
|
||||
int frameId3 = majorVersion > 2 ? id3Data.readUnsignedByte() : 0;
|
||||
int frameSize = majorVersion == 2 ? id3Data.readUnsignedInt24() :
|
||||
majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt();
|
||||
|
||||
if (frameSize <= 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip frame flags.
|
||||
id3Data.skipBytes(2);
|
||||
// Frame flags.
|
||||
boolean isCompressed = false;
|
||||
boolean isEncrypted = false;
|
||||
boolean isUnsynchronized = false;
|
||||
boolean hasGroupIdentifier = false;
|
||||
boolean hasDataLength = false;
|
||||
|
||||
try {
|
||||
Id3Frame frame;
|
||||
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
|
||||
frame = decodeTxxxFrame(id3Data, frameSize);
|
||||
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
|
||||
frame = decodePrivFrame(id3Data, frameSize);
|
||||
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
|
||||
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);
|
||||
if (majorVersion > 2) {
|
||||
int flags = id3Data.readShort();
|
||||
if (majorVersion == 3) {
|
||||
isCompressed = (flags & 0x0080) != 0;
|
||||
isEncrypted = (flags & 0x0040) != 0;
|
||||
hasDataLength = isCompressed;
|
||||
} else {
|
||||
String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
|
||||
frame = decodeBinaryFrame(id3Data, frameSize, id);
|
||||
isCompressed = (flags & 0x0008) != 0;
|
||||
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) {
|
||||
@ -96,7 +170,7 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
|
||||
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) {
|
||||
if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) {
|
||||
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.
|
||||
* @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 id2 = 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,
|
||||
"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 id3Size = id3Buffer.readSynchSafeInt();
|
||||
|
||||
// Check if extended header presents.
|
||||
if ((flags & 0x2) != 0) {
|
||||
int extendedHeaderSize = id3Buffer.readSynchSafeInt();
|
||||
if (extendedHeaderSize > 4) {
|
||||
id3Buffer.skipBytes(extendedHeaderSize - 4);
|
||||
}
|
||||
id3Size -= extendedHeaderSize;
|
||||
if (majorVersion < 4) {
|
||||
// this flag is advisory in version 4, use the frame flags instead
|
||||
isUnsynchronized = (flags & 0x80) != 0;
|
||||
}
|
||||
|
||||
// Check if footer presents.
|
||||
if ((flags & 0x8) != 0) {
|
||||
id3Size -= 10;
|
||||
if (majorVersion == 3) {
|
||||
// check for extended header
|
||||
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;
|
||||
@ -253,6 +345,28 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
|
||||
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,
|
||||
String id) {
|
||||
byte[] frame = new byte[frameSize];
|
||||
@ -261,6 +375,37 @@ public final class Id3Decoder implements MetadataDecoder<List<Id3Frame>> {
|
||||
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.
|
||||
* @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;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Base class for ID3 frames.
|
||||
*/
|
||||
public abstract class Id3Frame {
|
||||
public abstract class Id3Frame implements Parcelable {
|
||||
|
||||
/**
|
||||
* The frame ID.
|
||||
@ -29,4 +32,13 @@ public abstract class Id3Frame {
|
||||
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;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* PRIV (Private) ID3 frame.
|
||||
*/
|
||||
@ -31,4 +35,52 @@ public final class PrivFrame extends Id3Frame {
|
||||
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;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame.
|
||||
*/
|
||||
@ -27,4 +30,48 @@ public final class TextInformationFrame extends Id3Frame {
|
||||
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;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* TXXX (User defined text information) ID3 frame.
|
||||
*/
|
||||
@ -31,4 +34,53 @@ public final class TxxxFrame extends Id3Frame {
|
||||
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];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -136,22 +136,13 @@ public final class ContentDataSource implements DataSource {
|
||||
@Override
|
||||
public void close() throws ContentDataSourceException {
|
||||
uri = null;
|
||||
try {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ContentDataSourceException(e);
|
||||
} finally {
|
||||
inputStream = null;
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
if (assetFileDescriptor != null) {
|
||||
assetFileDescriptor.close();
|
||||
}
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
throw new ContentDataSourceException(e);
|
||||
} finally {
|
||||
assetFileDescriptor = null;
|
||||
inputStream = null;
|
||||
if (opened) {
|
||||
opened = false;
|
||||
if (listener != null) {
|
||||
@ -160,6 +151,13 @@ public final class ContentDataSource implements DataSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (assetFileDescriptor != null) {
|
||||
try {
|
||||
assetFileDescriptor.close();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
assetFileDescriptor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -416,6 +416,24 @@ public final class ParsableByteArray {
|
||||
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}.
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user