Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
SDK
/
exoplayer
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
7d8141e4
authored
Apr 11, 2015
by
Oliver Woodman
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Add new style mp3 extractor.
parent
4a1fed9e
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
981 additions
and
0 deletions
library/src/main/java/com/google/android/exoplayer/extractor/mp3/BufferingInput.java
library/src/main/java/com/google/android/exoplayer/extractor/mp3/ConstantBitrateSeeker.java
library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java
library/src/main/java/com/google/android/exoplayer/extractor/mp3/MpegAudioHeader.java
library/src/main/java/com/google/android/exoplayer/extractor/mp3/VbriSeeker.java
library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java
library/src/main/java/com/google/android/exoplayer/util/Util.java
library/src/test/java/com/google/android/exoplayer/extractor/mp3/BufferingInputTest.java
library/src/test/java/com/google/android/exoplayer/testutil/Util.java
library/src/main/java/com/google/android/exoplayer/extractor/mp3/BufferingInput.java
0 → 100644
View file @
7d8141e4
/*
* Copyright (C) 2014 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
.
exoplayer
.
extractor
.
mp3
;
import
com.google.android.exoplayer.extractor.ExtractorInput
;
import
com.google.android.exoplayer.extractor.TrackOutput
;
import
com.google.android.exoplayer.util.ParsableByteArray
;
import
java.io.EOFException
;
import
java.io.IOException
;
import
java.nio.BufferOverflowException
;
/**
* Buffers bytes read from an {@link ExtractorInput} to allow re-reading buffered bytes within a
* window starting at a marked position.
*/
/* package */
final
class
BufferingInput
{
private
final
ParsableByteArray
buffer
;
private
final
int
capacity
;
private
int
readPosition
;
private
int
writePosition
;
private
int
markPosition
;
/**
* Constructs a new buffer for reading from extractor inputs that can store up to {@code capacity}
* bytes.
*
* @param capacity Number of bytes that can be stored in the buffer.
*/
public
BufferingInput
(
int
capacity
)
{
this
.
capacity
=
capacity
;
buffer
=
new
ParsableByteArray
(
capacity
*
2
);
}
/** Discards any pending data in the buffer and returns the writing position to zero. */
public
void
reset
()
{
readPosition
=
0
;
writePosition
=
0
;
markPosition
=
0
;
}
/**
* Moves the mark to be at the reading position. Any data before the reading position is
* discarded. After calling this method, calling {@link #returnToMark} will move the reading
* position back to the mark position.
*/
public
void
mark
()
{
if
(
readPosition
>
capacity
)
{
System
.
arraycopy
(
buffer
.
data
,
readPosition
,
buffer
.
data
,
0
,
writePosition
-
readPosition
);
writePosition
-=
readPosition
;
readPosition
=
0
;
}
markPosition
=
readPosition
;
}
/** Moves the reading position back to the mark position. */
public
void
returnToMark
()
{
readPosition
=
markPosition
;
}
/** Returns the number of bytes available for reading from the current position. */
public
int
getAvailableByteCount
()
{
return
writePosition
-
readPosition
;
}
/**
* Buffers any more data required to read {@code length} bytes from the reading position, and
* returns a {@link ParsableByteArray} that wraps the buffer's byte array, with its position set
* to the current reading position. The read position is then updated for having read
* {@code length} bytes.
*
* @param extractorInput {@link ExtractorInput} from which additional data should be read.
* @param length Number of bytes that will be readable in the returned array.
* @return {@link ParsableByteArray} from which {@code length} bytes can be read.
* @throws IOException Thrown if there was an error reading from the stream.
* @throws InterruptedException Thrown if reading from the stream was interrupted.
*/
public
ParsableByteArray
getParsableByteArray
(
ExtractorInput
extractorInput
,
int
length
)
throws
IOException
,
InterruptedException
{
if
(!
ensureLoaded
(
extractorInput
,
length
))
{
throw
new
EOFException
();
}
ParsableByteArray
parsableByteArray
=
new
ParsableByteArray
(
buffer
.
data
,
writePosition
);
parsableByteArray
.
setPosition
(
readPosition
);
readPosition
+=
length
;
return
parsableByteArray
;
}
/**
* Drains as much buffered data as possible up to {@code length} bytes to {@code trackOutput}.
*
* @param trackOutput Track output to populate with up to {@code length} bytes of sample data.
* @param length Number of bytes to try to read from the buffer.
* @return The number of buffered bytes written.
*/
public
int
drainToOutput
(
TrackOutput
trackOutput
,
int
length
)
{
if
(
length
==
0
)
{
return
0
;
}
buffer
.
setPosition
(
readPosition
);
int
bytesToDrain
=
Math
.
min
(
writePosition
-
readPosition
,
length
);
trackOutput
.
sampleData
(
buffer
,
bytesToDrain
);
readPosition
+=
bytesToDrain
;
return
bytesToDrain
;
}
/**
* Skips {@code length} bytes from the reading position, reading from {@code extractorInput} to
* populate the buffer if required.
*
* @param extractorInput {@link ExtractorInput} from which additional data should be read.
* @param length Number of bytes to skip.
* @throws IOException Thrown if there was an error reading from the stream.
* @throws InterruptedException Thrown if reading from the stream was interrupted.
*/
public
void
skip
(
ExtractorInput
extractorInput
,
int
length
)
throws
IOException
,
InterruptedException
{
if
(!
readInternal
(
extractorInput
,
null
,
0
,
length
))
{
throw
new
EOFException
();
}
}
/**
* Reads {@code length} bytes from the reading position, reading from {@code extractorInput} to
* populate the buffer if required.
*
* @param extractorInput {@link ExtractorInput} from which additional data should be read.
* @param length Number of bytes to read.
* @throws IOException Thrown if there was an error reading from the stream.
* @throws InterruptedException Thrown if reading from the stream was interrupted.
* @throws EOFException Thrown if the end of the file was reached.
*/
public
void
read
(
ExtractorInput
extractorInput
,
byte
[]
target
,
int
offset
,
int
length
)
throws
IOException
,
InterruptedException
{
if
(!
readInternal
(
extractorInput
,
target
,
offset
,
length
))
{
throw
new
EOFException
();
}
}
/**
* Reads {@code length} bytes from the reading position, reading from {@code extractorInput} to
* populate the buffer if required.
*
* <p>Returns {@code false} if the end of the stream has been reached. Throws {@link EOFException}
* if the read request could only be partially satisfied. Returns {@code true} otherwise.
*
* @param extractorInput {@link ExtractorInput} from which additional data should be read.
* @param length Number of bytes to read.
* @return Whether the extractor input is at the end of the stream.
* @throws IOException Thrown if there was an error reading from the stream.
* @throws InterruptedException Thrown if reading from the stream was interrupted.
* @throws EOFException Thrown if the end of the file was reached.
*/
public
boolean
readAllowingEndOfInput
(
ExtractorInput
extractorInput
,
byte
[]
target
,
int
offset
,
int
length
)
throws
IOException
,
InterruptedException
{
return
readInternal
(
extractorInput
,
target
,
offset
,
length
);
}
private
boolean
readInternal
(
ExtractorInput
extractorInput
,
byte
[]
target
,
int
offset
,
int
length
)
throws
InterruptedException
,
IOException
{
if
(!
ensureLoaded
(
extractorInput
,
length
))
{
return
false
;
}
if
(
target
!=
null
)
{
System
.
arraycopy
(
buffer
.
data
,
readPosition
,
target
,
offset
,
length
);
}
readPosition
+=
length
;
return
true
;
}
/** Ensures the buffer contains enough data to read {@code length} bytes. */
private
boolean
ensureLoaded
(
ExtractorInput
extractorInput
,
int
length
)
throws
InterruptedException
,
IOException
{
if
(
length
+
readPosition
-
markPosition
>
capacity
)
{
throw
new
BufferOverflowException
();
}
int
bytesToLoad
=
length
-
(
writePosition
-
readPosition
);
if
(
bytesToLoad
>
0
)
{
if
(!
extractorInput
.
readFully
(
buffer
.
data
,
writePosition
,
bytesToLoad
,
true
))
{
return
false
;
}
writePosition
+=
bytesToLoad
;
}
return
true
;
}
}
library/src/main/java/com/google/android/exoplayer/extractor/mp3/ConstantBitrateSeeker.java
0 → 100644
View file @
7d8141e4
/*
* Copyright (C) 2014 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
.
exoplayer
.
extractor
.
mp3
;
import
com.google.android.exoplayer.C
;
/**
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
*/
/* package */
final
class
ConstantBitrateSeeker
implements
Mp3Extractor
.
Seeker
{
private
static
final
int
MICROSECONDS_PER_SECOND
=
1000000
;
private
static
final
int
BITS_PER_BYTE
=
8
;
private
final
long
firstFramePosition
;
private
final
int
bitrate
;
private
final
long
durationUs
;
public
ConstantBitrateSeeker
(
long
firstFramePosition
,
int
bitrate
,
long
inputLength
)
{
this
.
firstFramePosition
=
firstFramePosition
;
this
.
bitrate
=
bitrate
;
durationUs
=
inputLength
==
C
.
LENGTH_UNBOUNDED
?
C
.
UNKNOWN_TIME_US
:
getTimeUs
(
inputLength
);
}
@Override
public
long
getPosition
(
long
timeUs
)
{
return
firstFramePosition
+
(
timeUs
*
bitrate
)
/
(
MICROSECONDS_PER_SECOND
*
BITS_PER_BYTE
);
}
@Override
public
long
getTimeUs
(
long
position
)
{
return
((
position
-
firstFramePosition
)
*
MICROSECONDS_PER_SECOND
*
BITS_PER_BYTE
)
/
bitrate
;
}
@Override
public
long
getDurationUs
()
{
return
durationUs
;
}
}
library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java
0 → 100644
View file @
7d8141e4
This diff is collapsed.
Click to expand it.
library/src/main/java/com/google/android/exoplayer/extractor/mp3/MpegAudioHeader.java
0 → 100644
View file @
7d8141e4
/*
* Copyright (C) 2014 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
.
exoplayer
.
extractor
.
mp3
;
/** Parsed MPEG audio frame header. */
/* package */
final
class
MpegAudioHeader
{
private
static
final
int
[]
SAMPLING_RATE_V1
=
{
44100
,
48000
,
32000
};
private
static
final
int
[]
BITRATE_V1_L1
=
{
32
,
64
,
96
,
128
,
160
,
192
,
224
,
256
,
288
,
320
,
352
,
384
,
416
,
448
};
private
static
final
int
[]
BITRATE_V2_L1
=
{
32
,
48
,
56
,
64
,
80
,
96
,
112
,
128
,
144
,
160
,
176
,
192
,
224
,
256
};
private
static
final
int
[]
BITRATE_V1_L2
=
{
32
,
48
,
56
,
64
,
80
,
96
,
112
,
128
,
160
,
192
,
224
,
256
,
320
,
384
};
private
static
final
int
[]
BITRATE_V1_L3
=
{
32
,
40
,
48
,
56
,
64
,
80
,
96
,
112
,
128
,
160
,
192
,
224
,
256
,
320
};
private
static
final
int
[]
BITRATE_V2
=
{
8
,
16
,
24
,
32
,
40
,
48
,
56
,
64
,
80
,
96
,
112
,
128
,
144
,
160
};
/** Returns the size of the frame associated with {@code header}, or -1 if it is invalid. */
public
static
int
getFrameSize
(
int
header
)
{
if
((
header
&
0xFFE00000
)
!=
0xFFE00000
)
{
return
-
1
;
}
int
version
=
(
header
>>>
19
)
&
3
;
if
(
version
==
1
)
{
return
-
1
;
}
int
layer
=
(
header
>>>
17
)
&
3
;
if
(
layer
==
0
)
{
return
-
1
;
}
int
bitrateIndex
=
(
header
>>>
12
)
&
15
;
if
(
bitrateIndex
==
0
||
bitrateIndex
==
0xF
)
{
// Disallow "free" bitrate.
return
-
1
;
}
int
samplingRateIndex
=
(
header
>>>
10
)
&
3
;
if
(
samplingRateIndex
==
3
)
{
return
-
1
;
}
int
samplingRate
=
SAMPLING_RATE_V1
[
samplingRateIndex
];
if
(
version
==
2
)
{
// Version 2
samplingRate
/=
2
;
}
else
if
(
version
==
0
)
{
// Version 2.5
samplingRate
/=
4
;
}
int
bitrate
;
int
padding
=
(
header
>>>
9
)
&
1
;
if
(
layer
==
3
)
{
// Layer I (layer == 3)
bitrate
=
version
==
3
?
BITRATE_V1_L1
[
bitrateIndex
-
1
]
:
BITRATE_V2_L1
[
bitrateIndex
-
1
];
return
(
12000
*
bitrate
/
samplingRate
+
padding
)
*
4
;
}
else
{
// Layer II (layer == 2) or III (layer == 1)
if
(
version
==
3
)
{
bitrate
=
layer
==
2
?
BITRATE_V1_L2
[
bitrateIndex
-
1
]
:
BITRATE_V1_L3
[
bitrateIndex
-
1
];
}
else
{
// Version 2 or 2.5.
bitrate
=
BITRATE_V2
[
bitrateIndex
-
1
];
}
}
if
(
version
==
3
)
{
// Version 1
return
144000
*
bitrate
/
samplingRate
+
padding
;
}
else
{
// Version 2 or 2.5
return
(
layer
==
1
?
72000
:
144000
)
*
bitrate
/
samplingRate
+
padding
;
}
}
/**
* Returns the header represented by {@code header}, if it is valid; {@code null} otherwise.
*
* @param headerData Header data to parse.
* @param header Header to populate with data from {@code headerData}.
*/
public
static
void
populateHeader
(
int
headerData
,
MpegAudioHeader
header
)
{
if
((
headerData
&
0xFFE00000
)
!=
0xFFE00000
)
{
return
;
}
int
version
=
(
headerData
>>>
19
)
&
3
;
if
(
version
==
1
)
{
return
;
}
int
layer
=
(
headerData
>>>
17
)
&
3
;
if
(
layer
==
0
)
{
return
;
}
int
bitrateIndex
=
(
headerData
>>>
12
)
&
15
;
if
(
bitrateIndex
==
0
||
bitrateIndex
==
0xF
)
{
// Disallow "free" bitrate.
return
;
}
int
samplingRateIndex
=
(
headerData
>>>
10
)
&
3
;
if
(
samplingRateIndex
==
3
)
{
return
;
}
int
sampleRate
=
SAMPLING_RATE_V1
[
samplingRateIndex
];
if
(
version
==
2
)
{
// Version 2
sampleRate
/=
2
;
}
else
if
(
version
==
0
)
{
// Version 2.5
sampleRate
/=
4
;
}
int
padding
=
(
headerData
>>>
9
)
&
1
;
int
bitrate
,
frameSize
,
samplesPerFrame
;
if
(
layer
==
3
)
{
// Layer I (layer == 3)
bitrate
=
version
==
3
?
BITRATE_V1_L1
[
bitrateIndex
-
1
]
:
BITRATE_V2_L1
[
bitrateIndex
-
1
];
frameSize
=
(
12000
*
bitrate
/
sampleRate
+
padding
)
*
4
;
samplesPerFrame
=
384
;
}
else
{
// Layer II (layer == 2) or III (layer == 1)
if
(
version
==
3
)
{
// Version 1
bitrate
=
layer
==
2
?
BITRATE_V1_L2
[
bitrateIndex
-
1
]
:
BITRATE_V1_L3
[
bitrateIndex
-
1
];
samplesPerFrame
=
1152
;
frameSize
=
144000
*
bitrate
/
sampleRate
+
padding
;
}
else
{
// Version 2 or 2.5.
bitrate
=
BITRATE_V2
[
bitrateIndex
-
1
];
samplesPerFrame
=
layer
==
1
?
576
:
1152
;
frameSize
=
(
layer
==
1
?
72000
:
144000
)
*
bitrate
/
sampleRate
+
padding
;
}
}
int
channels
=
((
headerData
>>
6
)
&
3
)
==
3
?
1
:
2
;
int
layerIndex
=
3
-
layer
;
header
.
setValues
(
version
,
layerIndex
,
frameSize
,
sampleRate
,
channels
,
bitrate
,
samplesPerFrame
);
}
/** MPEG audio header version. */
public
int
version
;
/** MPEG audio layer index, starting at zero. */
public
int
layerIndex
;
/** Size of the frame associated with this header, in bytes. */
public
int
frameSize
;
/** Sample rate in samples per second. */
public
int
sampleRate
;
/** Number of audio channels in the frame. */
public
int
channels
;
/** Bitrate of the frame in kbit/s. */
public
int
bitrate
;
/** Number of samples stored in the frame. */
public
int
samplesPerFrame
;
private
void
setValues
(
int
version
,
int
layerIndex
,
int
frameSize
,
int
sampleRate
,
int
channels
,
int
bitrate
,
int
samplesPerFrame
)
{
this
.
version
=
version
;
this
.
layerIndex
=
layerIndex
;
this
.
frameSize
=
frameSize
;
this
.
sampleRate
=
sampleRate
;
this
.
channels
=
channels
;
this
.
bitrate
=
bitrate
;
this
.
samplesPerFrame
=
samplesPerFrame
;
}
}
library/src/main/java/com/google/android/exoplayer/extractor/mp3/VbriSeeker.java
0 → 100644
View file @
7d8141e4
/*
* Copyright (C) 2014 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
.
exoplayer
.
extractor
.
mp3
;
import
com.google.android.exoplayer.util.ParsableByteArray
;
import
com.google.android.exoplayer.util.Util
;
/**
* MP3 seeker that uses metadata from a VBRI header.
*/
/* package */
final
class
VbriSeeker
implements
Mp3Extractor
.
Seeker
{
private
static
final
int
VBRI_HEADER
=
Util
.
getIntegerCodeForString
(
"VBRI"
);
/**
* If {@code frame} contains a VBRI header and it is usable for seeking, returns a
* {@link Mp3Extractor.Seeker} for seeking in the containing stream. Otherwise, returns
* {@code null}, which indicates that the information in the frame was not a VBRI header, or was
* unusable for seeking.
*/
public
static
VbriSeeker
create
(
MpegAudioHeader
mpegAudioHeader
,
ParsableByteArray
frame
,
long
position
)
{
long
basePosition
=
position
+
mpegAudioHeader
.
frameSize
;
// Read the VBRI header.
frame
.
skip
(
32
);
int
headerData
=
frame
.
readInt
();
if
(
headerData
!=
VBRI_HEADER
)
{
return
null
;
}
frame
.
skip
(
10
);
int
numFrames
=
frame
.
readInt
();
if
(
numFrames
<=
0
)
{
return
null
;
}
int
sampleRate
=
mpegAudioHeader
.
sampleRate
;
long
durationUs
=
Util
.
scaleLargeTimestamp
(
numFrames
,
1000000L
*
(
sampleRate
>=
32000
?
1152
:
576
),
sampleRate
);
int
numEntries
=
frame
.
readUnsignedShort
();
int
scale
=
frame
.
readUnsignedShort
();
int
entrySize
=
frame
.
readUnsignedShort
();
// Read entries in the VBRI header.
long
[]
timesUs
=
new
long
[
numEntries
];
long
[]
offsets
=
new
long
[
numEntries
];
long
segmentDurationUs
=
durationUs
/
numEntries
;
long
now
=
0
;
int
segmentIndex
=
0
;
while
(
segmentIndex
<
numEntries
)
{
int
numBytes
;
switch
(
entrySize
)
{
case
1
:
numBytes
=
frame
.
readUnsignedByte
();
break
;
case
2
:
numBytes
=
frame
.
readUnsignedShort
();
break
;
case
3
:
numBytes
=
frame
.
readUnsignedInt24
();
break
;
case
4
:
numBytes
=
frame
.
readUnsignedIntToInt
();
break
;
default
:
return
null
;
}
now
+=
segmentDurationUs
;
timesUs
[
segmentIndex
]
=
now
;
position
+=
numBytes
*
scale
;
offsets
[
segmentIndex
]
=
position
;
segmentIndex
++;
}
return
new
VbriSeeker
(
timesUs
,
offsets
,
basePosition
,
durationUs
);
}
private
final
long
[]
timesUs
;
private
final
long
[]
positions
;
private
final
long
basePosition
;
private
final
long
durationUs
;
private
VbriSeeker
(
long
[]
timesUs
,
long
[]
positions
,
long
basePosition
,
long
durationUs
)
{
this
.
timesUs
=
timesUs
;
this
.
positions
=
positions
;
this
.
basePosition
=
basePosition
;
this
.
durationUs
=
durationUs
;
}
@Override
public
long
getPosition
(
long
timeUs
)
{
int
index
=
Util
.
binarySearchFloor
(
timesUs
,
timeUs
,
false
,
false
);
return
basePosition
+
(
index
==
-
1
?
0L
:
positions
[
index
]);
}
@Override
public
long
getTimeUs
(
long
position
)
{
return
timesUs
[
Util
.
binarySearchFloor
(
positions
,
position
,
true
,
true
)];
}
@Override
public
long
getDurationUs
()
{
return
durationUs
;
}
}
library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java
0 → 100644
View file @
7d8141e4
/*
* Copyright (C) 2014 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
.
exoplayer
.
extractor
.
mp3
;
import
com.google.android.exoplayer.C
;
import
com.google.android.exoplayer.util.ParsableByteArray
;
import
com.google.android.exoplayer.util.Util
;
/**
* MP3 seeker that uses metadata from a XING header.
*/
/* package */
final
class
XingSeeker
implements
Mp3Extractor
.
Seeker
{
private
static
final
int
XING_HEADER
=
Util
.
getIntegerCodeForString
(
"Xing"
);
private
static
final
int
INFO_HEADER
=
Util
.
getIntegerCodeForString
(
"Info"
);
/**
* If {@code frame} contains a XING header and it is usable for seeking, returns a
* {@link Mp3Extractor.Seeker} for seeking in the containing stream. Otherwise, returns
* {@code null}, which indicates that the information in the frame was not a XING header, or was
* unusable for seeking.
*/
public
static
XingSeeker
create
(
MpegAudioHeader
mpegAudioHeader
,
ParsableByteArray
frame
,
long
position
,
long
inputLength
)
{
int
samplesPerFrame
=
mpegAudioHeader
.
samplesPerFrame
;
int
sampleRate
=
mpegAudioHeader
.
sampleRate
;
long
firstFramePosition
=
position
+
mpegAudioHeader
.
frameSize
;
// Skip to the XING header.
int
xingBase
;
if
((
mpegAudioHeader
.
version
&
1
)
==
1
)
{
// MPEG 1.
if
(
mpegAudioHeader
.
channels
!=
1
)
{
xingBase
=
32
;
}
else
{
xingBase
=
17
;
}
}
else
{
// MPEG 2 or 2.5.
if
(
mpegAudioHeader
.
channels
!=
1
)
{
xingBase
=
17
;
}
else
{
xingBase
=
9
;
}
}
frame
.
skip
(
4
+
xingBase
);
int
headerData
=
frame
.
readInt
();
if
(
headerData
!=
XING_HEADER
&&
headerData
!=
INFO_HEADER
)
{
return
null
;
}
int
flags
=
frame
.
readInt
();
// Frame count, size and table of contents are required to use this header.
if
((
flags
&
0x07
)
!=
0x07
)
{
return
null
;
}
// Read frame count, as (flags & 1) == 1.
int
frameCount
=
frame
.
readUnsignedIntToInt
();
if
(
frameCount
==
0
)
{
return
null
;
}
long
durationUs
=
Util
.
scaleLargeTimestamp
(
frameCount
,
samplesPerFrame
*
1000000L
,
sampleRate
);
// Read size in bytes, as (flags & 2) == 2.
long
sizeBytes
=
frame
.
readUnsignedIntToInt
();
// Read table-of-contents as (flags & 4) == 4.
frame
.
skip
(
1
);
long
[]
tableOfContents
=
new
long
[
99
];
for
(
int
i
=
0
;
i
<
99
;
i
++)
{
tableOfContents
[
i
]
=
frame
.
readUnsignedByte
();
}
// TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
// delay = ((frame.readUnsignedByte() & 0xFF) << 4) + ((frame.readUnsignedByte() & 0xFF) >>> 4);
// padding = ((frame.readUnsignedByte() & 0x0F) << 8) + (frame.readUnsignedByte() & 0xFF);
return
new
XingSeeker
(
tableOfContents
,
firstFramePosition
,
sizeBytes
,
durationUs
,
inputLength
);
}
/** Entries are in the range [0, 255], but are stored as long integers for convenience. */
private
final
long
[]
tableOfContents
;
private
final
long
firstFramePosition
;
private
final
long
sizeBytes
;
private
final
long
durationUs
;
private
final
long
inputLength
;
private
XingSeeker
(
long
[]
tableOfContents
,
long
firstFramePosition
,
long
sizeBytes
,
long
durationUs
,
long
inputLength
)
{
this
.
tableOfContents
=
tableOfContents
;
this
.
firstFramePosition
=
firstFramePosition
;
this
.
sizeBytes
=
sizeBytes
;
this
.
durationUs
=
durationUs
;
this
.
inputLength
=
inputLength
;
}
@Override
public
long
getPosition
(
long
timeUs
)
{
float
percent
=
timeUs
*
100
f
/
durationUs
;
float
fx
;
if
(
percent
<=
0
f
)
{
fx
=
0
f
;
}
else
if
(
percent
>=
100
f
)
{
fx
=
256
f
;
}
else
{
int
a
=
(
int
)
percent
;
float
fa
,
fb
;
if
(
a
==
0
)
{
fa
=
0
f
;
}
else
{
fa
=
tableOfContents
[
a
-
1
];
}
if
(
a
<
99
)
{
fb
=
tableOfContents
[
a
];
}
else
{
fb
=
256
f
;
}
fx
=
fa
+
(
fb
-
fa
)
*
(
percent
-
a
);
}
long
position
=
(
long
)
((
1
f
/
256
)
*
fx
*
sizeBytes
)
+
firstFramePosition
;
return
inputLength
!=
C
.
LENGTH_UNBOUNDED
?
Math
.
min
(
position
,
inputLength
)
:
position
;
}
@Override
public
long
getTimeUs
(
long
position
)
{
long
offsetByte
=
256
*
(
position
-
firstFramePosition
)
/
sizeBytes
;
int
previousIndex
=
Util
.
binarySearchFloor
(
tableOfContents
,
offsetByte
,
true
,
false
);
long
previousTime
=
getTimeUsForTocIndex
(
previousIndex
);
if
(
previousIndex
==
98
)
{
return
previousTime
;
}
// Linearly interpolate the time taking into account the next entry.
long
previousByte
=
previousIndex
==
-
1
?
0
:
tableOfContents
[
previousIndex
];
long
nextByte
=
tableOfContents
[
previousIndex
+
1
];
long
nextTime
=
getTimeUsForTocIndex
(
previousIndex
+
1
);
long
timeOffset
=
(
nextTime
-
previousTime
)
*
(
offsetByte
-
previousByte
)
/
(
nextByte
-
previousByte
);
return
previousTime
+
timeOffset
;
}
@Override
public
long
getDurationUs
()
{
return
durationUs
;
}
/** Returns the time in microseconds corresponding to an index in the table of contents. */
private
long
getTimeUsForTocIndex
(
int
tocIndex
)
{
return
durationUs
*
(
tocIndex
+
1
)
/
100
;
}
}
library/src/main/java/com/google/android/exoplayer/util/Util.java
View file @
7d8141e4
...
...
@@ -460,4 +460,19 @@ public final class Util {
}
}
/**
* Returns the integer equal to the big-endian concatenation of the characters in {@code string}
* as bytes. {@code string} must contain four or fewer characters.
*/
public
static
int
getIntegerCodeForString
(
String
string
)
{
int
length
=
string
.
length
();
Assertions
.
checkArgument
(
length
<=
4
);
int
result
=
0
;
for
(
int
i
=
0
;
i
<
length
;
i
++)
{
result
<<=
8
;
result
|=
string
.
charAt
(
i
);
}
return
result
;
}
}
library/src/test/java/com/google/android/exoplayer/extractor/mp3/BufferingInputTest.java
0 → 100644
View file @
7d8141e4
/*
* Copyright (C) 2014 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
.
exoplayer
.
extractor
.
mp3
;
import
static
org
.
mockito
.
Matchers
.
any
;
import
static
org
.
mockito
.
Matchers
.
eq
;
import
static
org
.
mockito
.
Mockito
.
verify
;
import
static
org
.
mockito
.
Mockito
.
verifyZeroInteractions
;
import
com.google.android.exoplayer.extractor.DefaultExtractorInput
;
import
com.google.android.exoplayer.extractor.ExtractorInput
;
import
com.google.android.exoplayer.extractor.TrackOutput
;
import
com.google.android.exoplayer.testutil.FakeDataSource
;
import
com.google.android.exoplayer.testutil.Util
;
import
com.google.android.exoplayer.upstream.DataSpec
;
import
com.google.android.exoplayer.util.ParsableByteArray
;
import
android.net.Uri
;
import
android.test.InstrumentationTestCase
;
import
org.mockito.Mock
;
import
java.nio.BufferOverflowException
;
import
java.util.Arrays
;
/**
* Tests for {@link BufferingInput}.
*/
public
class
BufferingInputTest
extends
InstrumentationTestCase
{
private
static
final
String
TEST_URI
=
"http://www.google.com"
;
private
static
final
byte
[]
STREAM_DATA
=
new
byte
[]
{
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
};
private
ExtractorInput
fakeExtractorInput
;
/** Used for verifying interactions. */
@Mock
private
ExtractorInput
mockExtractorInput
;
@Mock
private
TrackOutput
mockTrackOutput
;
@Override
public
void
setUp
()
throws
Exception
{
Util
.
setUpMockito
(
this
);
FakeDataSource
.
Builder
builder
=
new
FakeDataSource
.
Builder
();
builder
.
appendReadData
(
STREAM_DATA
);
FakeDataSource
fakeDataSource
=
builder
.
build
();
fakeDataSource
.
open
(
new
DataSpec
(
Uri
.
parse
(
TEST_URI
)));
fakeExtractorInput
=
new
DefaultExtractorInput
(
fakeDataSource
,
0
,
STREAM_DATA
.
length
);
}
public
void
testReadFromExtractor
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
byte
[]
target
=
new
byte
[
4
];
input
.
read
(
fakeExtractorInput
,
target
,
0
,
4
);
assertMatchesStreamData
(
target
,
0
,
4
);
}
public
void
testReadCapacityFromExtractor
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
byte
[]
target
=
new
byte
[
5
];
input
.
read
(
fakeExtractorInput
,
target
,
0
,
5
);
assertMatchesStreamData
(
target
,
0
,
5
);
}
public
void
testReadOverCapacityFromExtractorFails
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
byte
[]
target
=
new
byte
[
6
];
try
{
input
.
read
(
fakeExtractorInput
,
target
,
0
,
6
);
fail
();
}
catch
(
BufferOverflowException
e
)
{
// Expected.
}
}
public
void
testReadFromBuffer
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
byte
[]
target
=
new
byte
[
5
];
input
.
read
(
fakeExtractorInput
,
target
,
0
,
5
);
// When reading already-buffered data
input
.
returnToMark
();
input
.
read
(
mockExtractorInput
,
target
,
0
,
5
);
assertMatchesStreamData
(
target
,
0
,
5
);
// There is no interaction with the extractor input.
verifyZeroInteractions
(
mockExtractorInput
);
}
public
void
testReadFromBufferPartially
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
byte
[]
target
=
new
byte
[
5
];
input
.
read
(
fakeExtractorInput
,
target
,
0
,
5
);
// When reading already-buffered data
input
.
returnToMark
();
input
.
read
(
mockExtractorInput
,
target
,
0
,
4
);
assertMatchesStreamData
(
target
,
0
,
4
);
// There is no interaction with the extractor input.
verifyZeroInteractions
(
mockExtractorInput
);
}
public
void
testResetDiscardsData
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
byte
[]
target
=
new
byte
[
5
];
input
.
read
(
fakeExtractorInput
,
target
,
0
,
5
);
// When the buffer is reset
input
.
reset
();
// Then it is possible to read up to the capacity again.
input
.
read
(
fakeExtractorInput
,
target
,
0
,
5
);
assertMatchesStreamData
(
target
,
5
,
5
);
}
public
void
testGetAvailableByteCountAtWritePosition
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
byte
[]
target
=
new
byte
[
5
];
input
.
read
(
fakeExtractorInput
,
target
,
0
,
5
);
assertEquals
(
0
,
input
.
getAvailableByteCount
());
}
public
void
testGetAvailableByteCountBeforeWritePosition
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
byte
[]
target
=
new
byte
[
5
];
input
.
read
(
fakeExtractorInput
,
target
,
0
,
3
);
input
.
mark
();
input
.
read
(
fakeExtractorInput
,
target
,
0
,
3
);
input
.
mark
();
input
.
read
(
fakeExtractorInput
,
target
,
0
,
2
);
input
.
returnToMark
();
// The reading position is calculated correctly.
assertEquals
(
2
,
input
.
getAvailableByteCount
());
assertEquals
(
8
,
fakeExtractorInput
.
getPosition
());
}
public
void
testGetParsableByteArray
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
input
.
skip
(
fakeExtractorInput
,
4
);
input
.
mark
();
input
.
skip
(
fakeExtractorInput
,
3
);
input
.
returnToMark
();
ParsableByteArray
parsableByteArray
=
input
.
getParsableByteArray
(
fakeExtractorInput
,
4
);
// The returned array matches the input's internal buffer.
assertMatchesStreamData
(
parsableByteArray
.
data
,
0
,
7
);
}
public
void
testGetParsableByteArrayPastCapacity
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
input
.
skip
(
fakeExtractorInput
,
4
);
input
.
mark
();
input
.
skip
(
fakeExtractorInput
,
3
);
input
.
mark
();
input
.
skip
(
fakeExtractorInput
,
1
);
input
.
returnToMark
();
ParsableByteArray
parsableByteArray
=
input
.
getParsableByteArray
(
fakeExtractorInput
,
2
);
// The second call to mark() copied the buffer data to the start.
assertMatchesStreamData
(
parsableByteArray
.
data
,
7
,
2
);
}
public
void
testDrainEntireBuffer
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
input
.
skip
(
fakeExtractorInput
,
3
);
input
.
returnToMark
();
// When draining the first three bytes
input
.
drainToOutput
(
mockTrackOutput
,
3
);
// They are appended as sample data.
verify
(
mockTrackOutput
).
sampleData
(
any
(
ParsableByteArray
.
class
),
eq
(
3
));
}
public
void
testDrainTwice
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
input
.
skip
(
fakeExtractorInput
,
3
);
input
.
returnToMark
();
// When draining one then two bytes
input
.
drainToOutput
(
mockTrackOutput
,
1
);
assertEquals
(
2
,
input
.
drainToOutput
(
mockTrackOutput
,
3
));
// They are appended as sample data.
verify
(
mockTrackOutput
).
sampleData
(
any
(
ParsableByteArray
.
class
),
eq
(
1
));
verify
(
mockTrackOutput
).
sampleData
(
any
(
ParsableByteArray
.
class
),
eq
(
2
));
}
public
void
testDrainPastCapacity
()
throws
Exception
{
BufferingInput
input
=
new
BufferingInput
(
5
);
input
.
skip
(
fakeExtractorInput
,
4
);
input
.
mark
();
input
.
skip
(
fakeExtractorInput
,
5
);
input
.
returnToMark
();
// When draining the entire buffer
input
.
drainToOutput
(
mockTrackOutput
,
5
);
// The sample data is appended as one whole buffer.
verify
(
mockTrackOutput
).
sampleData
(
any
(
ParsableByteArray
.
class
),
eq
(
5
));
}
private
static
void
assertMatchesStreamData
(
byte
[]
read
,
int
offset
,
int
length
)
{
assertTrue
(
Arrays
.
equals
(
Arrays
.
copyOfRange
(
STREAM_DATA
,
offset
,
offset
+
length
),
Arrays
.
copyOfRange
(
read
,
0
,
length
)));
}
}
library/src/test/java/com/google/android/exoplayer/testutil/Util.java
View file @
7d8141e4
...
...
@@ -15,6 +15,10 @@
*/
package
com
.
google
.
android
.
exoplayer
.
testutil
;
import
android.test.InstrumentationTestCase
;
import
org.mockito.MockitoAnnotations
;
import
java.util.Random
;
/**
...
...
@@ -35,4 +39,11 @@ public class Util {
return
source
;
}
public
static
void
setUpMockito
(
InstrumentationTestCase
instrumentationTestCase
)
{
// Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
System
.
setProperty
(
"dexmaker.dexcache"
,
instrumentationTestCase
.
getInstrumentation
().
getTargetContext
().
getCacheDir
().
getPath
());
MockitoAnnotations
.
initMocks
(
instrumentationTestCase
);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment