mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
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:
parent
6dc6f79f64
commit
986095a4a3
14 changed files with 843 additions and 88 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
androidTestImplementation project(modulePrefix + 'testutils')
|
||||
}
|
||||
|
|
|
|||
BIN
extensions/flac/src/androidTest/assets/bear_with_id3.flac
Normal file
BIN
extensions/flac/src/androidTest/assets/bear_with_id3.flac
Normal file
Binary file not shown.
162
extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump
Normal file
162
extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump
Normal 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
|
||||
122
extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump
Normal file
122
extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue