mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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
|
* Allow setting tags for all media sources in their factories. The tag of the
|
||||||
current window can be retrieved with `ExoPlayer.getCurrentTag`.
|
current window can be retrieved with `ExoPlayer.getCurrentTag`.
|
||||||
* Audio:
|
* 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`.
|
* Factor out `AudioTrack` position tracking from `DefaultAudioSink`.
|
||||||
* Fix an issue where the playback position would pause just after playback
|
* Fix an issue where the playback position would pause just after playback
|
||||||
begins, and poll the audio timestamp less frequently once it starts
|
begins, and poll the audio timestamp less frequently once it starts
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
androidTestImplementation project(modulePrefix + 'testutils')
|
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(
|
ExtractorAsserts.assertBehavior(
|
||||||
new ExtractorFactory() {
|
new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -44,4 +44,16 @@ public class FlacExtractorTest extends InstrumentationTestCase {
|
||||||
"bear.flac",
|
"bear.flac",
|
||||||
getInstrumentation().getContext());
|
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 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.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
|
import com.google.android.exoplayer2.extractor.Id3Peeker;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
|
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
||||||
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
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
|
* FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the
|
||||||
* mandatory STREAMINFO.
|
* mandatory STREAMINFO.
|
||||||
*/
|
*/
|
||||||
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
|
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
|
||||||
|
|
||||||
private ExtractorOutput extractorOutput;
|
private final Id3Peeker id3Peeker;
|
||||||
private TrackOutput trackOutput;
|
private final @Flags int flags;
|
||||||
|
|
||||||
private FlacDecoderJni decoderJni;
|
private FlacDecoderJni decoderJni;
|
||||||
|
|
||||||
private boolean metadataParsed;
|
private ExtractorOutput extractorOutput;
|
||||||
|
private TrackOutput trackOutput;
|
||||||
|
|
||||||
private ParsableByteArray outputBuffer;
|
private ParsableByteArray outputBuffer;
|
||||||
private ByteBuffer outputByteBuffer;
|
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
|
@Override
|
||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput output) {
|
||||||
extractorOutput = output;
|
extractorOutput = output;
|
||||||
|
|
@ -81,14 +123,27 @@ public final class FlacExtractor implements Extractor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
byte[] header = new byte[FLAC_SIGNATURE.length];
|
if (input.getPosition() == 0) {
|
||||||
input.peekFully(header, 0, FLAC_SIGNATURE.length);
|
id3Metadata = peekId3Data(input);
|
||||||
return Arrays.equals(header, FLAC_SIGNATURE);
|
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
|
@Override
|
||||||
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
if (input.getPosition() == 0) {
|
||||||
|
id3Metadata = peekId3Data(input);
|
||||||
|
id3SectionSize = input.getPeekPosition();
|
||||||
|
}
|
||||||
|
skipFullyId3Section(input);
|
||||||
|
|
||||||
decoderJni.setData(input);
|
decoderJni.setData(input);
|
||||||
|
|
||||||
if (!metadataParsed) {
|
if (!metadataParsed) {
|
||||||
|
|
@ -100,7 +155,7 @@ public final class FlacExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
decoderJni.reset(0);
|
decoderJni.reset(0);
|
||||||
input.setRetryPosition(0, e);
|
input.setRetryPosition(id3SectionSize, e);
|
||||||
throw e; // never executes
|
throw e; // never executes
|
||||||
}
|
}
|
||||||
metadataParsed = true;
|
metadataParsed = true;
|
||||||
|
|
@ -108,22 +163,25 @@ public final class FlacExtractor implements Extractor {
|
||||||
boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
|
boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
|
||||||
extractorOutput.seekMap(
|
extractorOutput.seekMap(
|
||||||
isSeekable
|
isSeekable
|
||||||
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
|
? new FlacSeekMap(streamInfo.durationUs(), decoderJni, id3SectionSize)
|
||||||
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
|
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
|
||||||
Format mediaFormat =
|
Format mediaFormat =
|
||||||
Format.createAudioSampleFormat(
|
Format.createAudioSampleFormat(
|
||||||
null,
|
/* id= */ null,
|
||||||
MimeTypes.AUDIO_RAW,
|
MimeTypes.AUDIO_RAW,
|
||||||
null,
|
/* codecs= */ null,
|
||||||
streamInfo.bitRate(),
|
streamInfo.bitRate(),
|
||||||
streamInfo.maxDecodedFrameSize(),
|
streamInfo.maxDecodedFrameSize(),
|
||||||
streamInfo.channels,
|
streamInfo.channels,
|
||||||
streamInfo.sampleRate,
|
streamInfo.sampleRate,
|
||||||
getPcmEncoding(streamInfo.bitsPerSample),
|
getPcmEncoding(streamInfo.bitsPerSample),
|
||||||
null,
|
/* encoderDelay= */ 0,
|
||||||
null,
|
/* encoderPadding= */ 0,
|
||||||
0,
|
/* initializationData= */ null,
|
||||||
null);
|
/* drmInitData= */ null,
|
||||||
|
/* selectionFlags= */ 0,
|
||||||
|
/* language= */ null,
|
||||||
|
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : id3Metadata);
|
||||||
trackOutput.format(mediaFormat);
|
trackOutput.format(mediaFormat);
|
||||||
|
|
||||||
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
||||||
|
|
@ -138,7 +196,7 @@ public final class FlacExtractor implements Extractor {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (lastDecodePosition >= 0) {
|
if (lastDecodePosition >= 0) {
|
||||||
decoderJni.reset(lastDecodePosition);
|
decoderJni.reset(lastDecodePosition);
|
||||||
input.setRetryPosition(lastDecodePosition, e);
|
input.setRetryPosition(id3SectionSize + lastDecodePosition, e);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
@ -154,11 +212,12 @@ public final class FlacExtractor implements Extractor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seek(long position, long timeUs) {
|
public void seek(long position, long timeUs) {
|
||||||
if (position == 0) {
|
if (position <= id3SectionSize) {
|
||||||
metadataParsed = false;
|
metadataParsed = false;
|
||||||
}
|
}
|
||||||
|
long flacStreamPosition = Math.max(0, position - id3SectionSize);
|
||||||
if (decoderJni != null) {
|
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 static final class FlacSeekMap implements SeekMap {
|
||||||
|
|
||||||
private final long durationUs;
|
private final long durationUs;
|
||||||
private final FlacDecoderJni decoderJni;
|
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.durationUs = durationUs;
|
||||||
this.decoderJni = decoderJni;
|
this.decoderJni = decoderJni;
|
||||||
|
this.id3SectionSize = id3SectionSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -188,7 +281,8 @@ public final class FlacExtractor implements Extractor {
|
||||||
@Override
|
@Override
|
||||||
public SeekPoints getSeekPoints(long timeUs) {
|
public SeekPoints getSeekPoints(long timeUs) {
|
||||||
// TODO: Access the seek table via JNI to return two seek points when appropriate.
|
// 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
|
@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.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||||
|
import com.google.android.exoplayer2.extractor.Id3Peeker;
|
||||||
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
|
|
@ -99,6 +100,7 @@ public final class Mp3Extractor implements Extractor {
|
||||||
private final ParsableByteArray scratch;
|
private final ParsableByteArray scratch;
|
||||||
private final MpegAudioHeader synchronizedHeader;
|
private final MpegAudioHeader synchronizedHeader;
|
||||||
private final GaplessInfoHolder gaplessInfoHolder;
|
private final GaplessInfoHolder gaplessInfoHolder;
|
||||||
|
private final Id3Peeker id3Peeker;
|
||||||
|
|
||||||
// Extractor outputs.
|
// Extractor outputs.
|
||||||
private ExtractorOutput extractorOutput;
|
private ExtractorOutput extractorOutput;
|
||||||
|
|
@ -135,6 +137,7 @@ public final class Mp3Extractor implements Extractor {
|
||||||
synchronizedHeader = new MpegAudioHeader();
|
synchronizedHeader = new MpegAudioHeader();
|
||||||
gaplessInfoHolder = new GaplessInfoHolder();
|
gaplessInfoHolder = new GaplessInfoHolder();
|
||||||
basisTimeUs = C.TIME_UNSET;
|
basisTimeUs = C.TIME_UNSET;
|
||||||
|
id3Peeker = new Id3Peeker();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extractor implementation.
|
// Extractor implementation.
|
||||||
|
|
@ -181,10 +184,22 @@ public final class Mp3Extractor implements Extractor {
|
||||||
seeker = getConstantBitrateSeeker(input);
|
seeker = getConstantBitrateSeeker(input);
|
||||||
}
|
}
|
||||||
extractorOutput.seekMap(seeker);
|
extractorOutput.seekMap(seeker);
|
||||||
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
|
trackOutput.format(
|
||||||
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
|
Format.createAudioSampleFormat(
|
||||||
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
|
/* id= */ null,
|
||||||
gaplessInfoHolder.encoderPadding, null, null, 0, 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));
|
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
|
||||||
}
|
}
|
||||||
return readSample(input);
|
return readSample(input);
|
||||||
|
|
@ -242,7 +257,15 @@ public final class Mp3Extractor implements Extractor {
|
||||||
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
|
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
|
||||||
input.resetPeekPosition();
|
input.resetPeekPosition();
|
||||||
if (input.getPosition() == 0) {
|
if (input.getPosition() == 0) {
|
||||||
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();
|
peekedId3Bytes = (int) input.getPeekPosition();
|
||||||
if (!sniffing) {
|
if (!sniffing) {
|
||||||
input.skipFully(peekedId3Bytes);
|
input.skipFully(peekedId3Bytes);
|
||||||
|
|
@ -296,49 +319,6 @@ public final class Mp3Extractor implements Extractor {
|
||||||
return true;
|
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,
|
* 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.
|
* 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";
|
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.metadata.MetadataDecoderException;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Arrays;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
|
@ -32,7 +33,7 @@ import org.robolectric.RobolectricTestRunner;
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
public final class Id3DecoderTest {
|
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 FRAME_HEADER_LENGTH = 10;
|
||||||
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
|
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
|
||||||
|
|
||||||
|
|
@ -202,19 +203,64 @@ public final class Id3DecoderTest {
|
||||||
assertThat(commentFrame.text).isEmpty();
|
assertThat(commentFrame.text).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] buildSingleFrameTag(String frameId, byte[] frameData) {
|
@Test
|
||||||
byte[] frameIdBytes = frameId.getBytes(Charset.forName(C.UTF8_NAME));
|
public void testDecodeMultiFrames() throws MetadataDecoderException {
|
||||||
Assertions.checkState(frameIdBytes.length == 4);
|
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];
|
assertThat(commentFrame.language).isEqualTo("eng");
|
||||||
System.arraycopy(TAG_HEADER, 0, tagData, 0, TAG_HEADER.length);
|
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.
|
// Fill in the size part of the tag header.
|
||||||
int offset = TAG_HEADER.length - 4;
|
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 >> 21) & 0x7F);
|
||||||
tagData[offset++] = (byte) ((tagSize >> 14) & 0x7F);
|
tagData[offset++] = (byte) ((tagSize >> 14) & 0x7F);
|
||||||
tagData[offset++] = (byte) ((tagSize >> 7) & 0x7F);
|
tagData[offset++] = (byte) ((tagSize >> 7) & 0x7F);
|
||||||
tagData[offset++] = (byte) (tagSize & 0x7F);
|
tagData[offset++] = (byte) (tagSize & 0x7F);
|
||||||
|
|
||||||
|
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.
|
// Fill in the frame header.
|
||||||
tagData[offset++] = frameIdBytes[0];
|
tagData[offset++] = frameIdBytes[0];
|
||||||
tagData[offset++] = frameIdBytes[1];
|
tagData[offset++] = frameIdBytes[1];
|
||||||
|
|
@ -225,10 +271,22 @@ public final class Id3DecoderTest {
|
||||||
tagData[offset++] = (byte) ((frameData.length >> 8) & 0xFF);
|
tagData[offset++] = (byte) ((frameData.length >> 8) & 0xFF);
|
||||||
tagData[offset++] = (byte) (frameData.length & 0xFF);
|
tagData[offset++] = (byte) (frameData.length & 0xFF);
|
||||||
offset += 2; // Frame flags set to 0
|
offset += 2; // Frame flags set to 0
|
||||||
|
|
||||||
// Fill in the frame data.
|
// Fill in the frame data.
|
||||||
System.arraycopy(frameData, 0, tagData, offset, frameData.length);
|
System.arraycopy(frameData, 0, tagData, offset, frameData.length);
|
||||||
|
offset += frameData.length;
|
||||||
|
}
|
||||||
return tagData;
|
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