Support ID3/Apple metadata parsing in MP3 and MP4 files

This commit is contained in:
Alan Snyder 2016-09-02 20:11:26 -07:00
parent c54169c192
commit 18ab96349e
23 changed files with 1386 additions and 437 deletions

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {}
}

View File

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

View File

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

View File

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

View File

@ -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];
}
};
}

View File

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

View File

@ -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];
}
};
}

View File

@ -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];
}
};
}

View File

@ -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];
}
};
}

View File

@ -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];
}
};
}

View File

@ -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];
}
}

View File

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

View File

@ -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];
}
};
}

View File

@ -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];
}
};
}

View File

@ -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];
}
};
}

View File

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

View File

@ -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}.
*