Support FLAC files with ID3 headers.

Support parsing ID3 tags at the beginning of FLAC files, even though FLAC spec
does not require this.

GitHub: #4055.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=192127929
This commit is contained in:
hoangtc 2018-04-09 08:06:01 -07:00 committed by Oliver Woodman
parent 6dc6f79f64
commit 986095a4a3
14 changed files with 843 additions and 88 deletions

View file

@ -30,6 +30,8 @@
* Allow setting tags for all media sources in their factories. The tag of the
current window can be retrieved with `ExoPlayer.getCurrentTag`.
* Audio:
* FLAC: Sniff FLAC files correctly if they have ID3 headers
([#4055](https://github.com/google/ExoPlayer/issues/4055)).
* Factor out `AudioTrack` position tracking from `DefaultAudioSink`.
* Fix an issue where the playback position would pause just after playback
begins, and poll the audio timestamp less frequently once it starts

View file

@ -31,6 +31,7 @@ android {
}
dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core')
androidTestImplementation project(modulePrefix + 'testutils')
}

View file

@ -0,0 +1,162 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = [[timeUs=0, position=55284]]
numberOfTracks = 1
track 0:
format:
bitrate = 768000
id = null
containerMimeType = null
sampleMimeType = audio/raw
maxInputSize = 16384
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 526272
sample count = 33
sample 0:
time = 0
flags = 1
data = length 16384, hash 61D2C5C2
sample 1:
time = 85333
flags = 1
data = length 16384, hash E6D7F214
sample 2:
time = 170666
flags = 1
data = length 16384, hash 59BF0D5D
sample 3:
time = 256000
flags = 1
data = length 16384, hash 3625F468
sample 4:
time = 341333
flags = 1
data = length 16384, hash F66A323
sample 5:
time = 426666
flags = 1
data = length 16384, hash CDBAE629
sample 6:
time = 512000
flags = 1
data = length 16384, hash 536F3A91
sample 7:
time = 597333
flags = 1
data = length 16384, hash D4F35C9C
sample 8:
time = 682666
flags = 1
data = length 16384, hash EE04CEBF
sample 9:
time = 768000
flags = 1
data = length 16384, hash 647E2A67
sample 10:
time = 853333
flags = 1
data = length 16384, hash 31583F2C
sample 11:
time = 938666
flags = 1
data = length 16384, hash E433A93D
sample 12:
time = 1024000
flags = 1
data = length 16384, hash 5E1C7051
sample 13:
time = 1109333
flags = 1
data = length 16384, hash 43E6E358
sample 14:
time = 1194666
flags = 1
data = length 16384, hash 5DC1B256
sample 15:
time = 1280000
flags = 1
data = length 16384, hash 3D9D95CF
sample 16:
time = 1365333
flags = 1
data = length 16384, hash 2A5BD2C0
sample 17:
time = 1450666
flags = 1
data = length 16384, hash 93E25061
sample 18:
time = 1536000
flags = 1
data = length 16384, hash B81793D8
sample 19:
time = 1621333
flags = 1
data = length 16384, hash 1A3BD49F
sample 20:
time = 1706666
flags = 1
data = length 16384, hash FB672FF1
sample 21:
time = 1792000
flags = 1
data = length 16384, hash 48AB8B45
sample 22:
time = 1877333
flags = 1
data = length 16384, hash 13C9640A
sample 23:
time = 1962666
flags = 1
data = length 16384, hash 499E4A0B
sample 24:
time = 2048000
flags = 1
data = length 16384, hash F9A783E6
sample 25:
time = 2133333
flags = 1
data = length 16384, hash D2B77598
sample 26:
time = 2218666
flags = 1
data = length 16384, hash CE5B826C
sample 27:
time = 2304000
flags = 1
data = length 16384, hash E99EE956
sample 28:
time = 2389333
flags = 1
data = length 16384, hash F2DB1486
sample 29:
time = 2474666
flags = 1
data = length 16384, hash 1636EAB
sample 30:
time = 2560000
flags = 1
data = length 16384, hash 23457C08
sample 31:
time = 2645333
flags = 1
data = length 16384, hash 30EB8381
sample 32:
time = 2730666
flags = 1
data = length 1984, hash 59CFDE1B
tracksEnded = true

View file

@ -0,0 +1,122 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = [[timeUs=0, position=55284]]
numberOfTracks = 1
track 0:
format:
bitrate = 768000
id = null
containerMimeType = null
sampleMimeType = audio/raw
maxInputSize = 16384
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 362432
sample count = 23
sample 0:
time = 853333
flags = 1
data = length 16384, hash 31583F2C
sample 1:
time = 938666
flags = 1
data = length 16384, hash E433A93D
sample 2:
time = 1024000
flags = 1
data = length 16384, hash 5E1C7051
sample 3:
time = 1109333
flags = 1
data = length 16384, hash 43E6E358
sample 4:
time = 1194666
flags = 1
data = length 16384, hash 5DC1B256
sample 5:
time = 1280000
flags = 1
data = length 16384, hash 3D9D95CF
sample 6:
time = 1365333
flags = 1
data = length 16384, hash 2A5BD2C0
sample 7:
time = 1450666
flags = 1
data = length 16384, hash 93E25061
sample 8:
time = 1536000
flags = 1
data = length 16384, hash B81793D8
sample 9:
time = 1621333
flags = 1
data = length 16384, hash 1A3BD49F
sample 10:
time = 1706666
flags = 1
data = length 16384, hash FB672FF1
sample 11:
time = 1792000
flags = 1
data = length 16384, hash 48AB8B45
sample 12:
time = 1877333
flags = 1
data = length 16384, hash 13C9640A
sample 13:
time = 1962666
flags = 1
data = length 16384, hash 499E4A0B
sample 14:
time = 2048000
flags = 1
data = length 16384, hash F9A783E6
sample 15:
time = 2133333
flags = 1
data = length 16384, hash D2B77598
sample 16:
time = 2218666
flags = 1
data = length 16384, hash CE5B826C
sample 17:
time = 2304000
flags = 1
data = length 16384, hash E99EE956
sample 18:
time = 2389333
flags = 1
data = length 16384, hash F2DB1486
sample 19:
time = 2474666
flags = 1
data = length 16384, hash 1636EAB
sample 20:
time = 2560000
flags = 1
data = length 16384, hash 23457C08
sample 21:
time = 2645333
flags = 1
data = length 16384, hash 30EB8381
sample 22:
time = 2730666
flags = 1
data = length 1984, hash 59CFDE1B
tracksEnded = true

View file

@ -0,0 +1,78 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = [[timeUs=0, position=55284]]
numberOfTracks = 1
track 0:
format:
bitrate = 768000
id = null
containerMimeType = null
sampleMimeType = audio/raw
maxInputSize = 16384
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 182208
sample count = 12
sample 0:
time = 1792000
flags = 1
data = length 16384, hash 48AB8B45
sample 1:
time = 1877333
flags = 1
data = length 16384, hash 13C9640A
sample 2:
time = 1962666
flags = 1
data = length 16384, hash 499E4A0B
sample 3:
time = 2048000
flags = 1
data = length 16384, hash F9A783E6
sample 4:
time = 2133333
flags = 1
data = length 16384, hash D2B77598
sample 5:
time = 2218666
flags = 1
data = length 16384, hash CE5B826C
sample 6:
time = 2304000
flags = 1
data = length 16384, hash E99EE956
sample 7:
time = 2389333
flags = 1
data = length 16384, hash F2DB1486
sample 8:
time = 2474666
flags = 1
data = length 16384, hash 1636EAB
sample 9:
time = 2560000
flags = 1
data = length 16384, hash 23457C08
sample 10:
time = 2645333
flags = 1
data = length 16384, hash 30EB8381
sample 11:
time = 2730666
flags = 1
data = length 1984, hash 59CFDE1B
tracksEnded = true

View file

@ -0,0 +1,38 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = [[timeUs=0, position=55284]]
numberOfTracks = 1
track 0:
format:
bitrate = 768000
id = null
containerMimeType = null
sampleMimeType = audio/raw
maxInputSize = 16384
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 18368
sample count = 2
sample 0:
time = 2645333
flags = 1
data = length 16384, hash 30EB8381
sample 1:
time = 2730666
flags = 1
data = length 1984, hash 59CFDE1B
tracksEnded = true

View file

@ -33,7 +33,7 @@ public class FlacExtractorTest extends InstrumentationTestCase {
}
}
public void testSample() throws Exception {
public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior(
new ExtractorFactory() {
@Override
@ -44,4 +44,16 @@ public class FlacExtractorTest extends InstrumentationTestCase {
"bear.flac",
getInstrumentation().getContext());
}
public void testExtractFlacSampleWithId3Header() throws Exception {
ExtractorAsserts.assertBehavior(
new ExtractorFactory() {
@Override
public Extractor create() {
return new FlacExtractor();
}
},
"bear_with_id3.flac",
getInstrumentation().getContext());
}
}

View file

@ -17,20 +17,27 @@ package com.google.android.exoplayer2.ext.flac;
import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
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.Id3Peeker;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.Arrays;
@ -51,22 +58,57 @@ public final class FlacExtractor implements Extractor {
};
/** Flags controlling the behavior of the extractor. */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {FLAG_DISABLE_ID3_METADATA}
)
public @interface Flags {}
/**
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
* required.
*/
public static final int FLAG_DISABLE_ID3_METADATA = 1;
/**
* FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the
* mandatory STREAMINFO.
*/
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
private ExtractorOutput extractorOutput;
private TrackOutput trackOutput;
private final Id3Peeker id3Peeker;
private final @Flags int flags;
private FlacDecoderJni decoderJni;
private boolean metadataParsed;
private ExtractorOutput extractorOutput;
private TrackOutput trackOutput;
private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer;
private Metadata id3Metadata;
private long id3SectionSize;
private boolean metadataParsed;
/** Constructs an instance with flags = 0. */
public FlacExtractor() {
this(0);
}
/**
* Constructs an instance.
*
* @param flags Flags that control the extractor's behavior.
*/
public FlacExtractor(int flags) {
this.flags = flags;
id3Peeker = new Id3Peeker();
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
@ -81,14 +123,27 @@ public final class FlacExtractor implements Extractor {
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length];
input.peekFully(header, 0, FLAC_SIGNATURE.length);
return Arrays.equals(header, FLAC_SIGNATURE);
if (input.getPosition() == 0) {
id3Metadata = peekId3Data(input);
id3SectionSize = input.getPeekPosition();
}
boolean isFlacFormat = peekFlacSignature(input);
if (isFlacFormat) {
// If this is FLAC format, we should skip the whole ID3 section.
skipFullyId3Section(input);
}
return isFlacFormat;
}
@Override
public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (input.getPosition() == 0) {
id3Metadata = peekId3Data(input);
id3SectionSize = input.getPeekPosition();
}
skipFullyId3Section(input);
decoderJni.setData(input);
if (!metadataParsed) {
@ -100,7 +155,7 @@ public final class FlacExtractor implements Extractor {
}
} catch (IOException e) {
decoderJni.reset(0);
input.setRetryPosition(0, e);
input.setRetryPosition(id3SectionSize, e);
throw e; // never executes
}
metadataParsed = true;
@ -108,22 +163,25 @@ public final class FlacExtractor implements Extractor {
boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
extractorOutput.seekMap(
isSeekable
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
? new FlacSeekMap(streamInfo.durationUs(), decoderJni, id3SectionSize)
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
Format mediaFormat =
Format.createAudioSampleFormat(
null,
/* id= */ null,
MimeTypes.AUDIO_RAW,
null,
/* codecs= */ null,
streamInfo.bitRate(),
streamInfo.maxDecodedFrameSize(),
streamInfo.channels,
streamInfo.sampleRate,
getPcmEncoding(streamInfo.bitsPerSample),
null,
null,
0,
null);
/* encoderDelay= */ 0,
/* encoderPadding= */ 0,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : id3Metadata);
trackOutput.format(mediaFormat);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
@ -138,7 +196,7 @@ public final class FlacExtractor implements Extractor {
} catch (IOException e) {
if (lastDecodePosition >= 0) {
decoderJni.reset(lastDecodePosition);
input.setRetryPosition(lastDecodePosition, e);
input.setRetryPosition(id3SectionSize + lastDecodePosition, e);
}
throw e;
}
@ -154,11 +212,12 @@ public final class FlacExtractor implements Extractor {
@Override
public void seek(long position, long timeUs) {
if (position == 0) {
if (position <= id3SectionSize) {
metadataParsed = false;
}
long flacStreamPosition = Math.max(0, position - id3SectionSize);
if (decoderJni != null) {
decoderJni.reset(position);
decoderJni.reset(flacStreamPosition);
}
}
@ -170,14 +229,48 @@ public final class FlacExtractor implements Extractor {
}
}
/**
* Peeks ID3 tag data (if present) at the beginning of the input.
*
* @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
* present in the input.
*/
@Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
boolean disableId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
Id3Decoder.FramePredicate id3FramePredicate =
disableId3Frames ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate);
}
/**
* Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
*
* @return Whether the input begins with {@link #FLAC_SIGNATURE}.
*/
private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length];
input.peekFully(header, 0, FLAC_SIGNATURE.length);
return Arrays.equals(header, FLAC_SIGNATURE);
}
/** Skips input until we have passed the whole Id3 section. */
private void skipFullyId3Section(ExtractorInput input) throws IOException, InterruptedException {
int bytesToSkip = Math.max(0, (int) (id3SectionSize - input.getPosition()));
input.skipFully(bytesToSkip);
}
private static final class FlacSeekMap implements SeekMap {
private final long durationUs;
private final FlacDecoderJni decoderJni;
private final long id3SectionSize;
public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) {
public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni, long id3SectionSize) {
this.durationUs = durationUs;
this.decoderJni = decoderJni;
this.id3SectionSize = id3SectionSize;
}
@Override
@ -188,7 +281,8 @@ public final class FlacExtractor implements Extractor {
@Override
public SeekPoints getSeekPoints(long timeUs) {
// TODO: Access the seek table via JNI to return two seek points when appropriate.
return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs)));
return new SeekPoints(
new SeekPoint(timeUs, id3SectionSize + decoderJni.getSeekPosition(timeUs)));
}
@Override

View file

@ -0,0 +1,87 @@
/*
* Copyright (C) 2018 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.support.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException;
import java.io.IOException;
/**
* Peeks data from the beginning of an {@link ExtractorInput} to determine if there is any ID3 tag.
*/
public final class Id3Peeker {
private final ParsableByteArray scratch;
public Id3Peeker() {
scratch = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
}
/**
* Peeks ID3 data from the input and parses the first ID3 tag.
*
* @param input The {@link ExtractorInput} from which data should be peeked.
* @param id3FramePredicate Determines which ID3 frames are decoded. May be null to decode all
* frames.
* @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
* present in the input.
* @throws IOException If an error occurred peeking from the input.
* @throws InterruptedException If the thread was interrupted.
*/
@Nullable
public Metadata peekId3Data(
ExtractorInput input, @Nullable Id3Decoder.FramePredicate id3FramePredicate)
throws IOException, InterruptedException {
int peekedId3Bytes = 0;
Metadata metadata = null;
while (true) {
try {
input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
} catch (EOFException e) {
// If input has less than ID3_HEADER_LENGTH, ignore the rest.
break;
}
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {
// Not an ID3 tag.
break;
}
scratch.skipBytes(3); // Skip major version, minor version and flags.
int framesLength = scratch.readSynchSafeInt();
int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength;
if (metadata == null) {
byte[] id3Data = new byte[tagLength];
System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);
} else {
input.advancePeekPosition(framesLength);
}
peekedId3Bytes += tagLength;
}
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes);
return metadata;
}
}

View file

@ -24,6 +24,7 @@ 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.Id3Peeker;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
@ -99,6 +100,7 @@ public final class Mp3Extractor implements Extractor {
private final ParsableByteArray scratch;
private final MpegAudioHeader synchronizedHeader;
private final GaplessInfoHolder gaplessInfoHolder;
private final Id3Peeker id3Peeker;
// Extractor outputs.
private ExtractorOutput extractorOutput;
@ -135,6 +137,7 @@ public final class Mp3Extractor implements Extractor {
synchronizedHeader = new MpegAudioHeader();
gaplessInfoHolder = new GaplessInfoHolder();
basisTimeUs = C.TIME_UNSET;
id3Peeker = new Id3Peeker();
}
// Extractor implementation.
@ -181,11 +184,23 @@ public final class Mp3Extractor implements Extractor {
seeker = getConstantBitrateSeeker(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,
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
trackOutput.format(
Format.createAudioSampleFormat(
/* id= */ null,
synchronizedHeader.mimeType,
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
MpegAudioHeader.MAX_FRAME_SIZE_BYTES,
synchronizedHeader.channels,
synchronizedHeader.sampleRate,
/* pcmEncoding= */ Format.NO_VALUE,
gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
}
return readSample(input);
}
@ -242,7 +257,15 @@ public final class Mp3Extractor implements Extractor {
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
input.resetPeekPosition();
if (input.getPosition() == 0) {
peekId3Data(input);
// We need to parse enough ID3 metadata to retrieve any gapless playback information even
// if ID3 metadata parsing is disabled.
boolean onlyDecodeGaplessInfoFrames = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
Id3Decoder.FramePredicate id3FramePredicate =
onlyDecodeGaplessInfoFrames ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null;
metadata = id3Peeker.peekId3Data(input, id3FramePredicate);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
}
peekedId3Bytes = (int) input.getPeekPosition();
if (!sniffing) {
input.skipFully(peekedId3Bytes);
@ -296,49 +319,6 @@ public final class Mp3Extractor implements Extractor {
return true;
}
/**
* Peeks ID3 data from the input, including gapless playback information.
*
* @param input The {@link ExtractorInput} from which data should be peeked.
* @throws IOException If an error occurred peeking from the input.
* @throws InterruptedException If the thread was interrupted.
*/
private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
int peekedId3Bytes = 0;
while (true) {
input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {
// Not an ID3 tag.
break;
}
scratch.skipBytes(3); // Skip major version, minor version and flags.
int framesLength = scratch.readSynchSafeInt();
int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength;
if (metadata == null) {
byte[] id3Data = new byte[tagLength];
System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
// We need to parse enough ID3 metadata to retrieve any gapless playback information even
// if ID3 metadata parsing is disabled.
Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0
? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null;
metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
}
} else {
input.advancePeekPosition(framesLength);
}
peekedId3Bytes += tagLength;
}
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes);
}
/**
* Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata,
* returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise.

View file

@ -53,6 +53,16 @@ public final class Id3Decoder implements MetadataDecoder {
}
/** A predicate that indicates no frames should be decoded. */
public static final FramePredicate NO_FRAMES_PREDICATE =
new FramePredicate() {
@Override
public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) {
return false;
}
};
private static final String TAG = "Id3Decoder";
/**

View file

@ -0,0 +1,111 @@
/*
* Copyright (C) 2018 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 static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.Id3DecoderTest;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link Id3Peeker}. */
@RunWith(RobolectricTestRunner.class)
public final class Id3PeekerTest {
@Test
public void testPeekId3Data_returnNull_ifId3TagNotPresentAtBeginningOfInput()
throws IOException, InterruptedException {
Id3Peeker id3Peeker = new Id3Peeker();
FakeExtractorInput input =
new FakeExtractorInput.Builder()
.setData(new byte[] {1, 'I', 'D', '3', 2, 3, 4, 5, 6, 7, 8, 9, 10})
.build();
Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null);
assertThat(metadata).isNull();
}
@Test
public void testPeekId3Data_returnId3Tag_ifId3TagPresent()
throws IOException, InterruptedException {
Id3Peeker id3Peeker = new Id3Peeker();
byte[] rawId3 =
Id3DecoderTest.buildSingleFrameTag(
"APIC",
new byte[] {
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
});
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(rawId3).build();
Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null);
assertThat(metadata.length()).isEqualTo(1);
ApicFrame apicFrame = (ApicFrame) metadata.get(0);
assertThat(apicFrame.mimeType).isEqualTo("image/jpeg");
assertThat(apicFrame.pictureType).isEqualTo(16);
assertThat(apicFrame.description).isEqualTo("Hello World");
assertThat(apicFrame.pictureData).hasLength(10);
assertThat(apicFrame.pictureData).isEqualTo(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0});
}
@Test
public void testPeekId3Data_returnId3TagAccordingToGivenPredicate_ifId3TagPresent()
throws IOException, InterruptedException {
Id3Peeker id3Peeker = new Id3Peeker();
byte[] rawId3 =
Id3DecoderTest.buildMultiFramesTag(
new Id3DecoderTest.FrameSpec(
"COMM",
new byte[] {
3, 101, 110, 103, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116,
101, 120, 116, 0
}),
new Id3DecoderTest.FrameSpec(
"APIC",
new byte[] {
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
}));
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(rawId3).build();
Metadata metadata =
id3Peeker.peekId3Data(
input,
new Id3Decoder.FramePredicate() {
@Override
public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) {
return id0 == 'C' && id1 == 'O' && id2 == 'M' && id3 == 'M';
}
});
assertThat(metadata.length()).isEqualTo(1);
CommentFrame commentFrame = (CommentFrame) metadata.get(0);
assertThat(commentFrame.language).isEqualTo("eng");
assertThat(commentFrame.description).isEqualTo("description");
assertThat(commentFrame.text).isEqualTo("text");
}
}

View file

@ -22,6 +22,7 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import com.google.android.exoplayer2.util.Assertions;
import java.nio.charset.Charset;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@ -32,7 +33,7 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public final class Id3DecoderTest {
private static final byte[] TAG_HEADER = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 0};
private static final byte[] TAG_HEADER = new byte[] {'I', 'D', '3', 4, 0, 0, 0, 0, 0, 0};
private static final int FRAME_HEADER_LENGTH = 10;
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
@ -202,33 +203,90 @@ public final class Id3DecoderTest {
assertThat(commentFrame.text).isEmpty();
}
private static byte[] buildSingleFrameTag(String frameId, byte[] frameData) {
byte[] frameIdBytes = frameId.getBytes(Charset.forName(C.UTF8_NAME));
Assertions.checkState(frameIdBytes.length == 4);
@Test
public void testDecodeMultiFrames() throws MetadataDecoderException {
byte[] rawId3 =
buildMultiFramesTag(
new FrameSpec(
"COMM",
new byte[] {
3, 101, 110, 103, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116,
101, 120, 116, 0
}),
new FrameSpec(
"APIC",
new byte[] {
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();
Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertThat(metadata.length()).isEqualTo(2);
CommentFrame commentFrame = (CommentFrame) metadata.get(0);
ApicFrame apicFrame = (ApicFrame) metadata.get(1);
byte[] tagData = new byte[TAG_HEADER.length + FRAME_HEADER_LENGTH + frameData.length];
System.arraycopy(TAG_HEADER, 0, tagData, 0, TAG_HEADER.length);
assertThat(commentFrame.language).isEqualTo("eng");
assertThat(commentFrame.description).isEqualTo("description");
assertThat(commentFrame.text).isEqualTo("text");
assertThat(apicFrame.mimeType).isEqualTo("image/jpeg");
assertThat(apicFrame.pictureType).isEqualTo(16);
assertThat(apicFrame.description).isEqualTo("Hello World");
assertThat(apicFrame.pictureData).hasLength(10);
assertThat(apicFrame.pictureData).isEqualTo(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0});
}
public static byte[] buildSingleFrameTag(String frameId, byte[] frameData) {
return buildMultiFramesTag(new FrameSpec(frameId, frameData));
}
public static byte[] buildMultiFramesTag(FrameSpec... frames) {
int totalLength = TAG_HEADER.length;
for (FrameSpec frame : frames) {
byte[] frameData = frame.frameData;
totalLength += FRAME_HEADER_LENGTH + frameData.length;
}
byte[] tagData = Arrays.copyOf(TAG_HEADER, totalLength);
// Fill in the size part of the tag header.
int offset = TAG_HEADER.length - 4;
int tagSize = frameData.length + FRAME_HEADER_LENGTH;
int tagSize = totalLength - TAG_HEADER.length;
tagData[offset++] = (byte) ((tagSize >> 21) & 0x7F);
tagData[offset++] = (byte) ((tagSize >> 14) & 0x7F);
tagData[offset++] = (byte) ((tagSize >> 7) & 0x7F);
tagData[offset++] = (byte) (tagSize & 0x7F);
// Fill in the frame header.
tagData[offset++] = frameIdBytes[0];
tagData[offset++] = frameIdBytes[1];
tagData[offset++] = frameIdBytes[2];
tagData[offset++] = frameIdBytes[3];
tagData[offset++] = (byte) ((frameData.length >> 24) & 0xFF);
tagData[offset++] = (byte) ((frameData.length >> 16) & 0xFF);
tagData[offset++] = (byte) ((frameData.length >> 8) & 0xFF);
tagData[offset++] = (byte) (frameData.length & 0xFF);
offset += 2; // Frame flags set to 0
// Fill in the frame data.
System.arraycopy(frameData, 0, tagData, offset, frameData.length);
for (FrameSpec frame : frames) {
byte[] frameData = frame.frameData;
String frameId = frame.frameId;
byte[] frameIdBytes = frameId.getBytes(Charset.forName(C.UTF8_NAME));
Assertions.checkState(frameIdBytes.length == 4);
// Fill in the frame header.
tagData[offset++] = frameIdBytes[0];
tagData[offset++] = frameIdBytes[1];
tagData[offset++] = frameIdBytes[2];
tagData[offset++] = frameIdBytes[3];
tagData[offset++] = (byte) ((frameData.length >> 24) & 0xFF);
tagData[offset++] = (byte) ((frameData.length >> 16) & 0xFF);
tagData[offset++] = (byte) ((frameData.length >> 8) & 0xFF);
tagData[offset++] = (byte) (frameData.length & 0xFF);
offset += 2; // Frame flags set to 0
// Fill in the frame data.
System.arraycopy(frameData, 0, tagData, offset, frameData.length);
offset += frameData.length;
}
return tagData;
}
/** Specify an ID3 frame. */
public static final class FrameSpec {
public final String frameId;
public final byte[] frameData;
public FrameSpec(String frameId, byte[] frameData) {
this.frameId = frameId;
this.frameData = frameData;
}
}
}