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
3ada4e17
authored
Jun 14, 2017
by
ojw28
Committed by
GitHub
Jun 14, 2017
Browse files
Options
_('Browse Files')
Download
Plain Diff
Merge pull request #2952 from google/dev-v2-r2.4.2
r2.4.2
parents
9f814850
4e578a1b
Hide whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
368 additions
and
236 deletions
RELEASENOTES.md
build.gradle
demo/src/main/AndroidManifest.xml
demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java
library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java
library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java
RELEASENOTES.md
View file @
3ada4e17
# Release notes #
# Release notes #
### r2.4.2 ###
*
Stability: Work around Nexus 10 reboot when playing certain content
(
[
2806
](
https://github.com/google/ExoPlayer/issues/2806
)
).
*
MP3: Correctly treat MP3s with INFO headers as constant bitrate
(
[
2895
](
https://github.com/google/ExoPlayer/issues/2895
)
).
*
HLS: Use average rather than peak bandwidth when available
(
[
#2863
](
https://github.com/google/ExoPlayer/issues/2863
)
).
*
SmoothStreaming: Fix timeline for live streams
(
[
#2760
](
https://github.com/google/ExoPlayer/issues/2760
)
).
*
UI: Fix DefaultTimeBar invalidation
(
[
#2871
](
https://github.com/google/ExoPlayer/issues/2871
)
).
*
Misc bugfixes.
### r2.4.1 ###
### r2.4.1 ###
*
Stability: Avoid OutOfMemoryError in extractors when parsing malformed media
*
Stability: Avoid OutOfMemoryError in extractors when parsing malformed media
...
...
build.gradle
View file @
3ada4e17
...
@@ -48,7 +48,7 @@ allprojects {
...
@@ -48,7 +48,7 @@ allprojects {
releaseRepoName
=
getBintrayRepo
()
releaseRepoName
=
getBintrayRepo
()
releaseUserOrg
=
'google'
releaseUserOrg
=
'google'
releaseGroupId
=
'com.google.android.exoplayer'
releaseGroupId
=
'com.google.android.exoplayer'
releaseVersion
=
'r2.4.
1
'
releaseVersion
=
'r2.4.
2
'
releaseWebsite
=
'https://github.com/google/ExoPlayer'
releaseWebsite
=
'https://github.com/google/ExoPlayer'
}
}
if
(
it
.
hasProperty
(
'externalBuildDir'
))
{
if
(
it
.
hasProperty
(
'externalBuildDir'
))
{
...
...
demo/src/main/AndroidManifest.xml
View file @
3ada4e17
...
@@ -16,8 +16,8 @@
...
@@ -16,8 +16,8 @@
<manifest
xmlns:android=
"http://schemas.android.com/apk/res/android"
<manifest
xmlns:android=
"http://schemas.android.com/apk/res/android"
package=
"com.google.android.exoplayer2.demo"
package=
"com.google.android.exoplayer2.demo"
android:versionCode=
"240
1
"
android:versionCode=
"240
2
"
android:versionName=
"2.4.
1
"
>
android:versionName=
"2.4.
2
"
>
<uses-permission
android:name=
"android.permission.INTERNET"
/>
<uses-permission
android:name=
"android.permission.INTERNET"
/>
<uses-permission
android:name=
"android.permission.READ_EXTERNAL_STORAGE"
/>
<uses-permission
android:name=
"android.permission.READ_EXTERNAL_STORAGE"
/>
...
...
demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
View file @
3ada4e17
...
@@ -234,6 +234,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
...
@@ -234,6 +234,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
Intent
intent
=
getIntent
();
Intent
intent
=
getIntent
();
boolean
needNewPlayer
=
player
==
null
;
boolean
needNewPlayer
=
player
==
null
;
if
(
needNewPlayer
)
{
if
(
needNewPlayer
)
{
TrackSelection
.
Factory
adaptiveTrackSelectionFactory
=
new
AdaptiveTrackSelection
.
Factory
(
BANDWIDTH_METER
);
trackSelector
=
new
DefaultTrackSelector
(
adaptiveTrackSelectionFactory
);
trackSelectionHelper
=
new
TrackSelectionHelper
(
trackSelector
,
adaptiveTrackSelectionFactory
);
lastSeenTrackGroupArray
=
null
;
eventLogger
=
new
EventLogger
(
trackSelector
);
UUID
drmSchemeUuid
=
intent
.
hasExtra
(
DRM_SCHEME_UUID_EXTRA
)
UUID
drmSchemeUuid
=
intent
.
hasExtra
(
DRM_SCHEME_UUID_EXTRA
)
?
UUID
.
fromString
(
intent
.
getStringExtra
(
DRM_SCHEME_UUID_EXTRA
))
:
null
;
?
UUID
.
fromString
(
intent
.
getStringExtra
(
DRM_SCHEME_UUID_EXTRA
))
:
null
;
DrmSessionManager
<
FrameworkMediaCrypto
>
drmSessionManager
=
null
;
DrmSessionManager
<
FrameworkMediaCrypto
>
drmSessionManager
=
null
;
...
@@ -261,16 +268,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
...
@@ -261,16 +268,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
DefaultRenderersFactory
renderersFactory
=
new
DefaultRenderersFactory
(
this
,
DefaultRenderersFactory
renderersFactory
=
new
DefaultRenderersFactory
(
this
,
drmSessionManager
,
extensionRendererMode
);
drmSessionManager
,
extensionRendererMode
);
TrackSelection
.
Factory
adaptiveTrackSelectionFactory
=
new
AdaptiveTrackSelection
.
Factory
(
BANDWIDTH_METER
);
trackSelector
=
new
DefaultTrackSelector
(
adaptiveTrackSelectionFactory
);
trackSelectionHelper
=
new
TrackSelectionHelper
(
trackSelector
,
adaptiveTrackSelectionFactory
);
lastSeenTrackGroupArray
=
null
;
player
=
ExoPlayerFactory
.
newSimpleInstance
(
renderersFactory
,
trackSelector
);
player
=
ExoPlayerFactory
.
newSimpleInstance
(
renderersFactory
,
trackSelector
);
player
.
addListener
(
this
);
player
.
addListener
(
this
);
eventLogger
=
new
EventLogger
(
trackSelector
);
player
.
addListener
(
eventLogger
);
player
.
addListener
(
eventLogger
);
player
.
setAudioDebugListener
(
eventLogger
);
player
.
setAudioDebugListener
(
eventLogger
);
player
.
setVideoDebugListener
(
eventLogger
);
player
.
setVideoDebugListener
(
eventLogger
);
...
...
library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java
View file @
3ada4e17
...
@@ -146,6 +146,7 @@ public class UtilTest extends TestCase {
...
@@ -146,6 +146,7 @@ public class UtilTest extends TestCase {
assertEquals
(
1411161535000L
,
Util
.
parseXsDateTime
(
"2014-09-19T13:18:55-08:00"
));
assertEquals
(
1411161535000L
,
Util
.
parseXsDateTime
(
"2014-09-19T13:18:55-08:00"
));
assertEquals
(
1411161535000L
,
Util
.
parseXsDateTime
(
"2014-09-19T13:18:55-0800"
));
assertEquals
(
1411161535000L
,
Util
.
parseXsDateTime
(
"2014-09-19T13:18:55-0800"
));
assertEquals
(
1411161535000L
,
Util
.
parseXsDateTime
(
"2014-09-19T13:18:55.000-0800"
));
assertEquals
(
1411161535000L
,
Util
.
parseXsDateTime
(
"2014-09-19T13:18:55.000-0800"
));
assertEquals
(
1411161535000L
,
Util
.
parseXsDateTime
(
"2014-09-19T13:18:55.000-800"
));
}
}
public
void
testUnescapeInvalidFileName
()
{
public
void
testUnescapeInvalidFileName
()
{
...
...
library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
View file @
3ada4e17
...
@@ -305,10 +305,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
...
@@ -305,10 +305,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
if
(
timeline
.
isEmpty
())
{
if
(
timeline
.
isEmpty
())
{
return
0
;
return
0
;
}
}
long
bufferedP
osition
=
getBufferedPosition
();
long
p
osition
=
getBufferedPosition
();
long
duration
=
getDuration
();
long
duration
=
getDuration
();
return
(
bufferedPosition
==
C
.
TIME_UNSET
||
duration
==
C
.
TIME_UNSET
)
?
0
return
position
==
C
.
TIME_UNSET
||
duration
==
C
.
TIME_UNSET
?
0
:
(
int
)
(
duration
==
0
?
100
:
(
bufferedPosition
*
100
)
/
duration
);
:
(
duration
==
0
?
100
:
Util
.
constrainValue
((
int
)
((
position
*
100
)
/
duration
),
0
,
100
)
);
}
}
@Override
@Override
...
...
library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
View file @
3ada4e17
...
@@ -24,13 +24,13 @@ public interface ExoPlayerLibraryInfo {
...
@@ -24,13 +24,13 @@ public interface ExoPlayerLibraryInfo {
* The version of the library expressed as a string, for example "1.2.3".
* The version of the library expressed as a string, for example "1.2.3".
*/
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
String
VERSION
=
"2.4.
1
"
;
String
VERSION
=
"2.4.
2
"
;
/**
/**
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
String
VERSION_SLASHY
=
"ExoPlayerLib/2.4.
1
"
;
String
VERSION_SLASHY
=
"ExoPlayerLib/2.4.
2
"
;
/**
/**
* The version of the library expressed as an integer, for example 1002003.
* The version of the library expressed as an integer, for example 1002003.
...
@@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo {
...
@@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
* integer version 123045006 (123-045-006).
*/
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
int
VERSION_INT
=
200400
1
;
int
VERSION_INT
=
200400
2
;
/**
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
...
...
library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
View file @
3ada4e17
...
@@ -16,6 +16,7 @@
...
@@ -16,6 +16,7 @@
package
com
.
google
.
android
.
exoplayer2
.
extractor
.
mkv
;
package
com
.
google
.
android
.
exoplayer2
.
extractor
.
mkv
;
import
android.support.annotation.IntDef
;
import
android.support.annotation.IntDef
;
import
android.util.Log
;
import
android.util.SparseArray
;
import
android.util.SparseArray
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.Format
;
import
com.google.android.exoplayer2.Format
;
...
@@ -84,6 +85,8 @@ public final class MatroskaExtractor implements Extractor {
...
@@ -84,6 +85,8 @@ public final class MatroskaExtractor implements Extractor {
*/
*/
public
static
final
int
FLAG_DISABLE_SEEK_FOR_CUES
=
1
;
public
static
final
int
FLAG_DISABLE_SEEK_FOR_CUES
=
1
;
private
static
final
String
TAG
=
"MatroskaExtractor"
;
private
static
final
int
UNSET_ENTRY_ID
=
-
1
;
private
static
final
int
UNSET_ENTRY_ID
=
-
1
;
private
static
final
int
BLOCK_STATE_START
=
0
;
private
static
final
int
BLOCK_STATE_START
=
0
;
...
@@ -1558,7 +1561,12 @@ public final class MatroskaExtractor implements Extractor {
...
@@ -1558,7 +1561,12 @@ public final class MatroskaExtractor implements Extractor {
break
;
break
;
case
CODEC_ID_FOURCC:
case
CODEC_ID_FOURCC:
initializationData
=
parseFourCcVc1Private
(
new
ParsableByteArray
(
codecPrivate
));
initializationData
=
parseFourCcVc1Private
(
new
ParsableByteArray
(
codecPrivate
));
mimeType
=
initializationData
==
null
?
MimeTypes
.
VIDEO_UNKNOWN
:
MimeTypes
.
VIDEO_VC1
;
if
(
initializationData
!=
null
)
{
mimeType
=
MimeTypes
.
VIDEO_VC1
;
}
else
{
Log
.
w
(
TAG
,
"Unsupported FourCC. Setting mimeType to "
+
MimeTypes
.
VIDEO_UNKNOWN
);
mimeType
=
MimeTypes
.
VIDEO_UNKNOWN
;
}
break
;
break
;
case
CODEC_ID_THEORA:
case
CODEC_ID_THEORA:
// TODO: This can be set to the real mimeType if/when we work out what initializationData
// TODO: This can be set to the real mimeType if/when we work out what initializationData
...
@@ -1614,19 +1622,27 @@ public final class MatroskaExtractor implements Extractor {
...
@@ -1614,19 +1622,27 @@ public final class MatroskaExtractor implements Extractor {
break
;
break
;
case
CODEC_ID_ACM:
case
CODEC_ID_ACM:
mimeType
=
MimeTypes
.
AUDIO_RAW
;
mimeType
=
MimeTypes
.
AUDIO_RAW
;
if
(!
parseMsAcmCodecPrivate
(
new
ParsableByteArray
(
codecPrivate
)))
{
if
(
parseMsAcmCodecPrivate
(
new
ParsableByteArray
(
codecPrivate
)))
{
throw
new
ParserException
(
"Non-PCM MS/ACM is unsupported"
);
pcmEncoding
=
Util
.
getPcmEncoding
(
audioBitDepth
);
}
if
(
pcmEncoding
==
C
.
ENCODING_INVALID
)
{
pcmEncoding
=
Util
.
getPcmEncoding
(
audioBitDepth
);
pcmEncoding
=
Format
.
NO_VALUE
;
if
(
pcmEncoding
==
C
.
ENCODING_INVALID
)
{
mimeType
=
MimeTypes
.
AUDIO_UNKNOWN
;
throw
new
ParserException
(
"Unsupported PCM bit depth: "
+
audioBitDepth
);
Log
.
w
(
TAG
,
"Unsupported PCM bit depth: "
+
audioBitDepth
+
". Setting mimeType to "
+
mimeType
);
}
}
else
{
mimeType
=
MimeTypes
.
AUDIO_UNKNOWN
;
Log
.
w
(
TAG
,
"Non-PCM MS/ACM is unsupported. Setting mimeType to "
+
mimeType
);
}
}
break
;
break
;
case
CODEC_ID_PCM_INT_LIT:
case
CODEC_ID_PCM_INT_LIT:
mimeType
=
MimeTypes
.
AUDIO_RAW
;
mimeType
=
MimeTypes
.
AUDIO_RAW
;
pcmEncoding
=
Util
.
getPcmEncoding
(
audioBitDepth
);
pcmEncoding
=
Util
.
getPcmEncoding
(
audioBitDepth
);
if
(
pcmEncoding
==
C
.
ENCODING_INVALID
)
{
if
(
pcmEncoding
==
C
.
ENCODING_INVALID
)
{
throw
new
ParserException
(
"Unsupported PCM bit depth: "
+
audioBitDepth
);
pcmEncoding
=
Format
.
NO_VALUE
;
mimeType
=
MimeTypes
.
AUDIO_UNKNOWN
;
Log
.
w
(
TAG
,
"Unsupported PCM bit depth: "
+
audioBitDepth
+
". Setting mimeType to "
+
mimeType
);
}
}
break
;
break
;
case
CODEC_ID_SUBRIP:
case
CODEC_ID_SUBRIP:
...
...
library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
View file @
3ada4e17
...
@@ -16,6 +16,7 @@
...
@@ -16,6 +16,7 @@
package
com
.
google
.
android
.
exoplayer2
.
extractor
.
mp3
;
package
com
.
google
.
android
.
exoplayer2
.
extractor
.
mp3
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.util.Util
;
/**
/**
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
...
@@ -41,8 +42,11 @@ import com.google.android.exoplayer2.C;
...
@@ -41,8 +42,11 @@ import com.google.android.exoplayer2.C;
@Override
@Override
public
long
getPosition
(
long
timeUs
)
{
public
long
getPosition
(
long
timeUs
)
{
return
durationUs
==
C
.
TIME_UNSET
?
0
if
(
durationUs
==
C
.
TIME_UNSET
)
{
:
firstFramePosition
+
(
timeUs
*
bitrate
)
/
(
C
.
MICROS_PER_SECOND
*
BITS_PER_BYTE
);
return
0
;
}
timeUs
=
Util
.
constrainValue
(
timeUs
,
0
,
durationUs
);
return
firstFramePosition
+
(
timeUs
*
bitrate
)
/
(
C
.
MICROS_PER_SECOND
*
BITS_PER_BYTE
);
}
}
@Override
@Override
...
...
library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
View file @
3ada4e17
...
@@ -78,7 +78,7 @@ public final class Mp3Extractor implements Extractor {
...
@@ -78,7 +78,7 @@ public final class Mp3Extractor implements Extractor {
/**
/**
* The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
* The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
*/
*/
private
static
final
int
MAX_SNIFF_BYTES
=
MpegAudioHeader
.
MAX_FRAME_SIZE_BYTES
;
private
static
final
int
MAX_SNIFF_BYTES
=
16
*
1024
;
/**
/**
* Maximum length of data read into {@link #scratch}.
* Maximum length of data read into {@link #scratch}.
*/
*/
...
@@ -87,10 +87,12 @@ public final class Mp3Extractor implements Extractor {
...
@@ -87,10 +87,12 @@ public final class Mp3Extractor implements Extractor {
/**
/**
* Mask that includes the audio header values that must match between frames.
* Mask that includes the audio header values that must match between frames.
*/
*/
private
static
final
int
HEADER_MASK
=
0xFFFE0C00
;
private
static
final
int
MPEG_AUDIO_HEADER_MASK
=
0xFFFE0C00
;
private
static
final
int
XING_HEADER
=
Util
.
getIntegerCodeForString
(
"Xing"
);
private
static
final
int
INFO_HEADER
=
Util
.
getIntegerCodeForString
(
"Info"
);
private
static
final
int
SEEK_HEADER_XING
=
Util
.
getIntegerCodeForString
(
"Xing"
);
private
static
final
int
VBRI_HEADER
=
Util
.
getIntegerCodeForString
(
"VBRI"
);
private
static
final
int
SEEK_HEADER_INFO
=
Util
.
getIntegerCodeForString
(
"Info"
);
private
static
final
int
SEEK_HEADER_VBRI
=
Util
.
getIntegerCodeForString
(
"VBRI"
);
private
static
final
int
SEEK_HEADER_UNSET
=
0
;
@Flags
private
final
int
flags
;
@Flags
private
final
int
flags
;
private
final
long
forcedFirstSampleTimestampUs
;
private
final
long
forcedFirstSampleTimestampUs
;
...
@@ -178,7 +180,11 @@ public final class Mp3Extractor implements Extractor {
...
@@ -178,7 +180,11 @@ public final class Mp3Extractor implements Extractor {
}
}
}
}
if
(
seeker
==
null
)
{
if
(
seeker
==
null
)
{
seeker
=
setupSeeker
(
input
);
seeker
=
maybeReadSeekFrame
(
input
);
if
(
seeker
==
null
||
(!
seeker
.
isSeekable
()
&&
(
flags
&
FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
)
!=
0
))
{
seeker
=
getConstantBitrateSeeker
(
input
);
}
extractorOutput
.
seekMap
(
seeker
);
extractorOutput
.
seekMap
(
seeker
);
trackOutput
.
format
(
Format
.
createAudioSampleFormat
(
null
,
synchronizedHeader
.
mimeType
,
null
,
trackOutput
.
format
(
Format
.
createAudioSampleFormat
(
null
,
synchronizedHeader
.
mimeType
,
null
,
Format
.
NO_VALUE
,
MpegAudioHeader
.
MAX_FRAME_SIZE_BYTES
,
synchronizedHeader
.
channels
,
Format
.
NO_VALUE
,
MpegAudioHeader
.
MAX_FRAME_SIZE_BYTES
,
synchronizedHeader
.
channels
,
...
@@ -197,7 +203,7 @@ public final class Mp3Extractor implements Extractor {
...
@@ -197,7 +203,7 @@ public final class Mp3Extractor implements Extractor {
}
}
scratch
.
setPosition
(
0
);
scratch
.
setPosition
(
0
);
int
sampleHeaderData
=
scratch
.
readInt
();
int
sampleHeaderData
=
scratch
.
readInt
();
if
(
(
sampleHeaderData
&
HEADER_MASK
)
!=
(
synchronizedHeaderData
&
HEADER_MASK
)
if
(
!
headersMatch
(
sampleHeaderData
,
synchronizedHeaderData
)
||
MpegAudioHeader
.
getFrameSize
(
sampleHeaderData
)
==
C
.
LENGTH_UNSET
)
{
||
MpegAudioHeader
.
getFrameSize
(
sampleHeaderData
)
==
C
.
LENGTH_UNSET
)
{
// We have lost synchronization, so attempt to resynchronize starting at the next byte.
// We have lost synchronization, so attempt to resynchronize starting at the next byte.
extractorInput
.
skipFully
(
1
);
extractorInput
.
skipFully
(
1
);
...
@@ -254,7 +260,7 @@ public final class Mp3Extractor implements Extractor {
...
@@ -254,7 +260,7 @@ public final class Mp3Extractor implements Extractor {
int
headerData
=
scratch
.
readInt
();
int
headerData
=
scratch
.
readInt
();
int
frameSize
;
int
frameSize
;
if
((
candidateSynchronizedHeaderData
!=
0
if
((
candidateSynchronizedHeaderData
!=
0
&&
(
headerData
&
HEADER_MASK
)
!=
(
candidateSynchronizedHeaderData
&
HEADER_MASK
))
&&
!
headersMatch
(
headerData
,
candidateSynchronizedHeaderData
))
||
(
frameSize
=
MpegAudioHeader
.
getFrameSize
(
headerData
))
==
C
.
LENGTH_UNSET
)
{
||
(
frameSize
=
MpegAudioHeader
.
getFrameSize
(
headerData
))
==
C
.
LENGTH_UNSET
)
{
// The header doesn't match the candidate header or is invalid. Try the next byte offset.
// The header doesn't match the candidate header or is invalid. Try the next byte offset.
if
(
searchedBytes
++
==
searchLimitBytes
)
{
if
(
searchedBytes
++
==
searchLimitBytes
)
{
...
@@ -337,37 +343,27 @@ public final class Mp3Extractor implements Extractor {
...
@@ -337,37 +343,27 @@ public final class Mp3Extractor implements Extractor {
}
}
/**
/**
*
Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide
*
Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata,
*
data from the start of the first frame in the stream. On returning, the input's position will
*
returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise.
*
be set to
the start of the first frame of audio.
*
After this method returns, the input position is
the start of the first frame of audio.
*
*
* @param input The {@link ExtractorInput} from which to read.
* @param input The {@link ExtractorInput} from which to read.
* @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise.
* @throws IOException Thrown if there was an error reading from the stream. Not expected if the
* @throws IOException Thrown if there was an error reading from the stream. Not expected if the
* next two frames were already peeked during synchronization.
* next two frames were already peeked during synchronization.
* @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
* @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
* the next two frames were already peeked during synchronization.
* the next two frames were already peeked during synchronization.
* @return a {@link Seeker}.
*/
*/
private
Seeker
setupSeeker
(
ExtractorInput
input
)
throws
IOException
,
InterruptedException
{
private
Seeker
maybeReadSeekFrame
(
ExtractorInput
input
)
throws
IOException
,
InterruptedException
{
// Read the first frame which may contain a Xing or VBRI header with seeking metadata.
ParsableByteArray
frame
=
new
ParsableByteArray
(
synchronizedHeader
.
frameSize
);
ParsableByteArray
frame
=
new
ParsableByteArray
(
synchronizedHeader
.
frameSize
);
input
.
peekFully
(
frame
.
data
,
0
,
synchronizedHeader
.
frameSize
);
input
.
peekFully
(
frame
.
data
,
0
,
synchronizedHeader
.
frameSize
);
long
position
=
input
.
getPosition
();
long
length
=
input
.
getLength
();
int
headerData
=
0
;
Seeker
seeker
=
null
;
// Check if there is a Xing header.
int
xingBase
=
(
synchronizedHeader
.
version
&
1
)
!=
0
int
xingBase
=
(
synchronizedHeader
.
version
&
1
)
!=
0
?
(
synchronizedHeader
.
channels
!=
1
?
36
:
21
)
// MPEG 1
?
(
synchronizedHeader
.
channels
!=
1
?
36
:
21
)
// MPEG 1
:
(
synchronizedHeader
.
channels
!=
1
?
21
:
13
);
// MPEG 2 or 2.5
:
(
synchronizedHeader
.
channels
!=
1
?
21
:
13
);
// MPEG 2 or 2.5
if
(
frame
.
limit
()
>=
xingBase
+
4
)
{
int
seekHeader
=
getSeekFrameHeader
(
frame
,
xingBase
);
frame
.
setPosition
(
xingBase
);
Seeker
seeker
;
headerData
=
frame
.
readInt
();
if
(
seekHeader
==
SEEK_HEADER_XING
||
seekHeader
==
SEEK_HEADER_INFO
)
{
}
seeker
=
XingSeeker
.
create
(
synchronizedHeader
,
frame
,
input
.
getPosition
(),
input
.
getLength
());
if
(
headerData
==
XING_HEADER
||
headerData
==
INFO_HEADER
)
{
seeker
=
XingSeeker
.
create
(
synchronizedHeader
,
frame
,
position
,
length
);
if
(
seeker
!=
null
&&
!
gaplessInfoHolder
.
hasGaplessInfo
())
{
if
(
seeker
!=
null
&&
!
gaplessInfoHolder
.
hasGaplessInfo
())
{
// If there is a Xing header, read gapless playback metadata at a fixed offset.
// If there is a Xing header, read gapless playback metadata at a fixed offset.
input
.
resetPeekPosition
();
input
.
resetPeekPosition
();
...
@@ -377,31 +373,63 @@ public final class Mp3Extractor implements Extractor {
...
@@ -377,31 +373,63 @@ public final class Mp3Extractor implements Extractor {
gaplessInfoHolder
.
setFromXingHeaderValue
(
scratch
.
readUnsignedInt24
());
gaplessInfoHolder
.
setFromXingHeaderValue
(
scratch
.
readUnsignedInt24
());
}
}
input
.
skipFully
(
synchronizedHeader
.
frameSize
);
input
.
skipFully
(
synchronizedHeader
.
frameSize
);
}
else
if
(
frame
.
limit
()
>=
40
)
{
if
(
seeker
!=
null
&&
!
seeker
.
isSeekable
()
&&
seekHeader
==
SEEK_HEADER_INFO
)
{
// Check if there is a VBRI header.
// Fall back to constant bitrate seeking for Info headers missing a table of contents.
frame
.
setPosition
(
36
);
// MPEG audio header (4 bytes) + 32 bytes.
return
getConstantBitrateSeeker
(
input
);
headerData
=
frame
.
readInt
();
if
(
headerData
==
VBRI_HEADER
)
{
seeker
=
VbriSeeker
.
create
(
synchronizedHeader
,
frame
,
position
,
length
);
input
.
skipFully
(
synchronizedHeader
.
frameSize
);
}
}
}
}
else
if
(
seekHeader
==
SEEK_HEADER_VBRI
)
{
seeker
=
VbriSeeker
.
create
(
synchronizedHeader
,
frame
,
input
.
getPosition
(),
input
.
getLength
());
if
(
seeker
==
null
||
(!
seeker
.
isSeekable
()
input
.
skipFully
(
synchronizedHeader
.
frameSize
);
&&
(
flags
&
FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
)
!=
0
))
{
}
else
{
// seekerHeader == SEEK_HEADER_UNSET
//
Repopulate the synchronized header in case we had to skip an invalid seeking header, which
//
This frame doesn't contain seeking information, so reset the peek position.
// would give an invalid CBR bitrate.
seeker
=
null
;
input
.
resetPeekPosition
();
input
.
resetPeekPosition
();
input
.
peekFully
(
scratch
.
data
,
0
,
4
);
scratch
.
setPosition
(
0
);
MpegAudioHeader
.
populateHeader
(
scratch
.
readInt
(),
synchronizedHeader
);
seeker
=
new
ConstantBitrateSeeker
(
input
.
getPosition
(),
synchronizedHeader
.
bitrate
,
length
);
}
}
return
seeker
;
return
seeker
;
}
}
/**
/**
* Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate.
*/
private
Seeker
getConstantBitrateSeeker
(
ExtractorInput
input
)
throws
IOException
,
InterruptedException
{
input
.
peekFully
(
scratch
.
data
,
0
,
4
);
scratch
.
setPosition
(
0
);
MpegAudioHeader
.
populateHeader
(
scratch
.
readInt
(),
synchronizedHeader
);
return
new
ConstantBitrateSeeker
(
input
.
getPosition
(),
synchronizedHeader
.
bitrate
,
input
.
getLength
());
}
/**
* Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}.
*/
private
static
boolean
headersMatch
(
int
headerA
,
long
headerB
)
{
return
(
headerA
&
MPEG_AUDIO_HEADER_MASK
)
==
(
headerB
&
MPEG_AUDIO_HEADER_MASK
);
}
/**
* Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if
* the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise.
* If seeking metadata is present, {@code frame}'s position is advanced past the header.
*/
private
static
int
getSeekFrameHeader
(
ParsableByteArray
frame
,
int
xingBase
)
{
if
(
frame
.
limit
()
>=
xingBase
+
4
)
{
frame
.
setPosition
(
xingBase
);
int
headerData
=
frame
.
readInt
();
if
(
headerData
==
SEEK_HEADER_XING
||
headerData
==
SEEK_HEADER_INFO
)
{
return
headerData
;
}
}
if
(
frame
.
limit
()
>=
40
)
{
frame
.
setPosition
(
36
);
// MPEG audio header (4 bytes) + 32 bytes.
if
(
frame
.
readInt
()
==
SEEK_HEADER_VBRI
)
{
return
SEEK_HEADER_VBRI
;
}
}
return
SEEK_HEADER_UNSET
;
}
/**
* {@link SeekMap} that also allows mapping from position (byte offset) back to time, which can be
* {@link SeekMap} that also allows mapping from position (byte offset) back to time, which can be
* used to work out the new sample basis timestamp after seeking and resynchronization.
* used to work out the new sample basis timestamp after seeking and resynchronization.
*/
*/
...
...
library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
View file @
3ada4e17
...
@@ -382,10 +382,14 @@ public final class TsExtractor implements Extractor {
...
@@ -382,10 +382,14 @@ public final class TsExtractor implements Extractor {
private
static
final
int
TS_PMT_DESC_DVBSUBS
=
0x59
;
private
static
final
int
TS_PMT_DESC_DVBSUBS
=
0x59
;
private
final
ParsableBitArray
pmtScratch
;
private
final
ParsableBitArray
pmtScratch
;
private
final
SparseArray
<
TsPayloadReader
>
trackIdToReaderScratch
;
private
final
SparseIntArray
trackIdToPidScratch
;
private
final
int
pid
;
private
final
int
pid
;
public
PmtReader
(
int
pid
)
{
public
PmtReader
(
int
pid
)
{
pmtScratch
=
new
ParsableBitArray
(
new
byte
[
5
]);
pmtScratch
=
new
ParsableBitArray
(
new
byte
[
5
]);
trackIdToReaderScratch
=
new
SparseArray
<>();
trackIdToPidScratch
=
new
SparseIntArray
();
this
.
pid
=
pid
;
this
.
pid
=
pid
;
}
}
...
@@ -436,6 +440,8 @@ public final class TsExtractor implements Extractor {
...
@@ -436,6 +440,8 @@ public final class TsExtractor implements Extractor {
new
TrackIdGenerator
(
programNumber
,
TS_STREAM_TYPE_ID3
,
MAX_PID_PLUS_ONE
));
new
TrackIdGenerator
(
programNumber
,
TS_STREAM_TYPE_ID3
,
MAX_PID_PLUS_ONE
));
}
}
trackIdToReaderScratch
.
clear
();
trackIdToPidScratch
.
clear
();
int
remainingEntriesLength
=
sectionData
.
bytesLeft
();
int
remainingEntriesLength
=
sectionData
.
bytesLeft
();
while
(
remainingEntriesLength
>
0
)
{
while
(
remainingEntriesLength
>
0
)
{
sectionData
.
readBytes
(
pmtScratch
,
5
);
sectionData
.
readBytes
(
pmtScratch
,
5
);
...
@@ -454,23 +460,30 @@ public final class TsExtractor implements Extractor {
...
@@ -454,23 +460,30 @@ public final class TsExtractor implements Extractor {
if
(
trackIds
.
get
(
trackId
))
{
if
(
trackIds
.
get
(
trackId
))
{
continue
;
continue
;
}
}
trackIds
.
put
(
trackId
,
true
);
TsPayloadReader
reader
;
TsPayloadReader
reader
=
mode
==
MODE_HLS
&&
streamType
==
TS_STREAM_TYPE_ID3
?
id3Reader
if
(
mode
==
MODE_HLS
&&
streamType
==
TS_STREAM_TYPE_ID3
)
{
:
payloadReaderFactory
.
createPayloadReader
(
streamType
,
esInfo
);
reader
=
id3Reader
;
if
(
mode
!=
MODE_HLS
}
else
{
||
elementaryPid
<
trackIdToPidScratch
.
get
(
trackId
,
MAX_PID_PLUS_ONE
))
{
reader
=
payloadReaderFactory
.
createPayloadReader
(
streamType
,
esInfo
);
trackIdToPidScratch
.
put
(
trackId
,
elementaryPid
);
if
(
reader
!=
null
)
{
trackIdToReaderScratch
.
put
(
trackId
,
reader
);
reader
.
init
(
timestampAdjuster
,
output
,
new
TrackIdGenerator
(
programNumber
,
trackId
,
MAX_PID_PLUS_ONE
));
}
}
}
}
int
trackIdCount
=
trackIdToPidScratch
.
size
();
for
(
int
i
=
0
;
i
<
trackIdCount
;
i
++)
{
int
trackId
=
trackIdToPidScratch
.
keyAt
(
i
);
trackIds
.
put
(
trackId
,
true
);
TsPayloadReader
reader
=
trackIdToReaderScratch
.
valueAt
(
i
);
if
(
reader
!=
null
)
{
if
(
reader
!=
null
)
{
tsPayloadReaders
.
put
(
elementaryPid
,
reader
);
if
(
reader
!=
id3Reader
)
{
reader
.
init
(
timestampAdjuster
,
output
,
new
TrackIdGenerator
(
programNumber
,
trackId
,
MAX_PID_PLUS_ONE
));
}
tsPayloadReaders
.
put
(
trackIdToPidScratch
.
valueAt
(
i
),
reader
);
}
}
}
}
if
(
mode
==
MODE_HLS
)
{
if
(
mode
==
MODE_HLS
)
{
if
(!
tracksEnded
)
{
if
(!
tracksEnded
)
{
output
.
endTracks
();
output
.
endTracks
();
...
...
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
View file @
3ada4e17
...
@@ -71,7 +71,7 @@ public final class MediaCodecInfo {
...
@@ -71,7 +71,7 @@ public final class MediaCodecInfo {
* @return The created instance.
* @return The created instance.
*/
*/
public
static
MediaCodecInfo
newPassthroughInstance
(
String
name
)
{
public
static
MediaCodecInfo
newPassthroughInstance
(
String
name
)
{
return
new
MediaCodecInfo
(
name
,
null
,
null
);
return
new
MediaCodecInfo
(
name
,
null
,
null
,
false
);
}
}
/**
/**
...
@@ -84,18 +84,29 @@ public final class MediaCodecInfo {
...
@@ -84,18 +84,29 @@ public final class MediaCodecInfo {
*/
*/
public
static
MediaCodecInfo
newInstance
(
String
name
,
String
mimeType
,
public
static
MediaCodecInfo
newInstance
(
String
name
,
String
mimeType
,
CodecCapabilities
capabilities
)
{
CodecCapabilities
capabilities
)
{
return
new
MediaCodecInfo
(
name
,
mimeType
,
capabilities
);
return
new
MediaCodecInfo
(
name
,
mimeType
,
capabilities
,
false
);
}
}
/**
/**
* @param name The name of the decoder.
* Creates an instance.
* @param capabilities The capabilities of the decoder.
*
* @param name The name of the {@link MediaCodec}.
* @param mimeType A mime type supported by the {@link MediaCodec}.
* @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
* @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}.
* @return The created instance.
*/
*/
private
MediaCodecInfo
(
String
name
,
String
mimeType
,
CodecCapabilities
capabilities
)
{
public
static
MediaCodecInfo
newInstance
(
String
name
,
String
mimeType
,
CodecCapabilities
capabilities
,
boolean
forceDisableAdaptive
)
{
return
new
MediaCodecInfo
(
name
,
mimeType
,
capabilities
,
forceDisableAdaptive
);
}
private
MediaCodecInfo
(
String
name
,
String
mimeType
,
CodecCapabilities
capabilities
,
boolean
forceDisableAdaptive
)
{
this
.
name
=
Assertions
.
checkNotNull
(
name
);
this
.
name
=
Assertions
.
checkNotNull
(
name
);
this
.
mimeType
=
mimeType
;
this
.
mimeType
=
mimeType
;
this
.
capabilities
=
capabilities
;
this
.
capabilities
=
capabilities
;
adaptive
=
capabilities
!=
null
&&
isAdaptive
(
capabilities
);
adaptive
=
!
forceDisableAdaptive
&&
capabilities
!=
null
&&
isAdaptive
(
capabilities
);
tunneling
=
capabilities
!=
null
&&
isTunneling
(
capabilities
);
tunneling
=
capabilities
!=
null
&&
isTunneling
(
capabilities
);
}
}
...
...
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
View file @
3ada4e17
...
@@ -339,7 +339,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
...
@@ -339,7 +339,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
}
String
codecName
=
decoderInfo
.
name
;
String
codecName
=
decoderInfo
.
name
;
codecIsAdaptive
=
decoderInfo
.
adaptive
&&
!
codecNeedsDisableAdaptationWorkaround
(
codecName
)
;
codecIsAdaptive
=
decoderInfo
.
adaptive
;
codecNeedsDiscardToSpsWorkaround
=
codecNeedsDiscardToSpsWorkaround
(
codecName
,
format
);
codecNeedsDiscardToSpsWorkaround
=
codecNeedsDiscardToSpsWorkaround
(
codecName
,
format
);
codecNeedsFlushWorkaround
=
codecNeedsFlushWorkaround
(
codecName
);
codecNeedsFlushWorkaround
=
codecNeedsFlushWorkaround
(
codecName
);
codecNeedsAdaptationWorkaround
=
codecNeedsAdaptationWorkaround
(
codecName
);
codecNeedsAdaptationWorkaround
=
codecNeedsAdaptationWorkaround
(
codecName
);
...
@@ -1188,18 +1188,4 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
...
@@ -1188,18 +1188,4 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&&
"OMX.MTK.AUDIO.DECODER.MP3"
.
equals
(
name
);
&&
"OMX.MTK.AUDIO.DECODER.MP3"
.
equals
(
name
);
}
}
/**
* Returns whether the decoder is known to fail when adapting, despite advertising itself as an
* adaptive decoder.
* <p>
* If true is returned then we explicitly disable adaptation for the decoder.
*
* @param name The decoder name.
* @return True if the decoder is known to fail when adapting.
*/
private
static
boolean
codecNeedsDisableAdaptationWorkaround
(
String
name
)
{
return
Util
.
SDK_INT
<=
19
&&
Util
.
MODEL
.
equals
(
"ODROID-XU3"
)
&&
(
"OMX.Exynos.AVC.Decoder"
.
equals
(
name
)
||
"OMX.Exynos.AVC.Decoder.secure"
.
equals
(
name
));
}
}
}
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
View file @
3ada4e17
...
@@ -160,6 +160,55 @@ public final class MediaCodecUtil {
...
@@ -160,6 +160,55 @@ public final class MediaCodecUtil {
return
decoderInfos
;
return
decoderInfos
;
}
}
/**
* Returns the maximum frame size supported by the default H264 decoder.
*
* @return The maximum frame size for an H264 stream that can be decoded on the device.
*/
public
static
int
maxH264DecodableFrameSize
()
throws
DecoderQueryException
{
if
(
maxH264DecodableFrameSize
==
-
1
)
{
int
result
=
0
;
MediaCodecInfo
decoderInfo
=
getDecoderInfo
(
MimeTypes
.
VIDEO_H264
,
false
);
if
(
decoderInfo
!=
null
)
{
for
(
CodecProfileLevel
profileLevel
:
decoderInfo
.
getProfileLevels
())
{
result
=
Math
.
max
(
avcLevelToMaxFrameSize
(
profileLevel
.
level
),
result
);
}
// We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are
// the levels mandated by the Android CDD.
result
=
Math
.
max
(
result
,
Util
.
SDK_INT
>=
21
?
(
720
*
480
)
:
(
480
*
360
));
}
maxH264DecodableFrameSize
=
result
;
}
return
maxH264DecodableFrameSize
;
}
/**
* Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given
* codec description string (as defined by RFC 6381).
*
* @param codec A codec description string, as defined by RFC 6381.
* @return A pair (profile constant, level constant) if {@code codec} is well-formed and
* recognized, or null otherwise
*/
public
static
Pair
<
Integer
,
Integer
>
getCodecProfileAndLevel
(
String
codec
)
{
if
(
codec
==
null
)
{
return
null
;
}
String
[]
parts
=
codec
.
split
(
"\\."
);
switch
(
parts
[
0
])
{
case
CODEC_ID_HEV1:
case
CODEC_ID_HVC1:
return
getHevcProfileAndLevel
(
codec
,
parts
);
case
CODEC_ID_AVC1:
case
CODEC_ID_AVC2:
return
getAvcProfileAndLevel
(
codec
,
parts
);
default
:
return
null
;
}
}
// Internal methods.
private
static
List
<
MediaCodecInfo
>
getDecoderInfosInternal
(
private
static
List
<
MediaCodecInfo
>
getDecoderInfosInternal
(
CodecKey
key
,
MediaCodecListCompat
mediaCodecList
)
throws
DecoderQueryException
{
CodecKey
key
,
MediaCodecListCompat
mediaCodecList
)
throws
DecoderQueryException
{
try
{
try
{
...
@@ -177,12 +226,14 @@ public final class MediaCodecUtil {
...
@@ -177,12 +226,14 @@ public final class MediaCodecUtil {
try
{
try
{
CodecCapabilities
capabilities
=
codecInfo
.
getCapabilitiesForType
(
supportedType
);
CodecCapabilities
capabilities
=
codecInfo
.
getCapabilitiesForType
(
supportedType
);
boolean
secure
=
mediaCodecList
.
isSecurePlaybackSupported
(
mimeType
,
capabilities
);
boolean
secure
=
mediaCodecList
.
isSecurePlaybackSupported
(
mimeType
,
capabilities
);
boolean
forceDisableAdaptive
=
codecNeedsDisableAdaptationWorkaround
(
codecName
);
if
((
secureDecodersExplicit
&&
key
.
secure
==
secure
)
if
((
secureDecodersExplicit
&&
key
.
secure
==
secure
)
||
(!
secureDecodersExplicit
&&
!
key
.
secure
))
{
||
(!
secureDecodersExplicit
&&
!
key
.
secure
))
{
decoderInfos
.
add
(
MediaCodecInfo
.
newInstance
(
codecName
,
mimeType
,
capabilities
));
decoderInfos
.
add
(
MediaCodecInfo
.
newInstance
(
codecName
,
mimeType
,
capabilities
,
forceDisableAdaptive
));
}
else
if
(!
secureDecodersExplicit
&&
secure
)
{
}
else
if
(!
secureDecodersExplicit
&&
secure
)
{
decoderInfos
.
add
(
MediaCodecInfo
.
newInstance
(
codecName
+
".secure"
,
mimeType
,
decoderInfos
.
add
(
MediaCodecInfo
.
newInstance
(
codecName
+
".secure"
,
mimeType
,
capabilities
));
capabilities
,
forceDisableAdaptive
));
// It only makes sense to have one synthesized secure decoder, return immediately.
// It only makes sense to have one synthesized secure decoder, return immediately.
return
decoderInfos
;
return
decoderInfos
;
}
}
...
@@ -289,50 +340,16 @@ public final class MediaCodecUtil {
...
@@ -289,50 +340,16 @@ public final class MediaCodecUtil {
}
}
/**
/**
* Returns the maximum frame size supported by the default H264 decoder.
* Returns whether the decoder is known to fail when adapting, despite advertising itself as an
* adaptive decoder.
*
*
* @return The maximum frame size for an H264 stream that can be decoded on the device.
* @param name The decoder name.
* @return True if the decoder is known to fail when adapting.
*/
*/
public
static
int
maxH264DecodableFrameSize
()
throws
DecoderQueryException
{
private
static
boolean
codecNeedsDisableAdaptationWorkaround
(
String
name
)
{
if
(
maxH264DecodableFrameSize
==
-
1
)
{
return
Util
.
SDK_INT
<=
22
int
result
=
0
;
&&
(
Util
.
MODEL
.
equals
(
"ODROID-XU3"
)
||
Util
.
MODEL
.
equals
(
"Nexus 10"
))
MediaCodecInfo
decoderInfo
=
getDecoderInfo
(
MimeTypes
.
VIDEO_H264
,
false
);
&&
(
"OMX.Exynos.AVC.Decoder"
.
equals
(
name
)
||
"OMX.Exynos.AVC.Decoder.secure"
.
equals
(
name
));
if
(
decoderInfo
!=
null
)
{
for
(
CodecProfileLevel
profileLevel
:
decoderInfo
.
getProfileLevels
())
{
result
=
Math
.
max
(
avcLevelToMaxFrameSize
(
profileLevel
.
level
),
result
);
}
// We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are
// the levels mandated by the Android CDD.
result
=
Math
.
max
(
result
,
Util
.
SDK_INT
>=
21
?
(
720
*
480
)
:
(
480
*
360
));
}
maxH264DecodableFrameSize
=
result
;
}
return
maxH264DecodableFrameSize
;
}
/**
* Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given
* codec description string (as defined by RFC 6381).
*
* @param codec A codec description string, as defined by RFC 6381.
* @return A pair (profile constant, level constant) if {@code codec} is well-formed and
* recognized, or null otherwise
*/
public
static
Pair
<
Integer
,
Integer
>
getCodecProfileAndLevel
(
String
codec
)
{
if
(
codec
==
null
)
{
return
null
;
}
String
[]
parts
=
codec
.
split
(
"\\."
);
switch
(
parts
[
0
])
{
case
CODEC_ID_HEV1:
case
CODEC_ID_HVC1:
return
getHevcProfileAndLevel
(
codec
,
parts
);
case
CODEC_ID_AVC1:
case
CODEC_ID_AVC2:
return
getAvcProfileAndLevel
(
codec
,
parts
);
default
:
return
null
;
}
}
}
private
static
Pair
<
Integer
,
Integer
>
getHevcProfileAndLevel
(
String
codec
,
String
[]
parts
)
{
private
static
Pair
<
Integer
,
Integer
>
getHevcProfileAndLevel
(
String
codec
,
String
[]
parts
)
{
...
...
library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
View file @
3ada4e17
...
@@ -154,23 +154,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
...
@@ -154,23 +154,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
@Override
@Override
public
void
updateSelectedTrack
(
long
bufferedDurationUs
)
{
public
void
updateSelectedTrack
(
long
bufferedDurationUs
)
{
long
nowMs
=
SystemClock
.
elapsedRealtime
();
long
nowMs
=
SystemClock
.
elapsedRealtime
();
//
Get the current and ideal selections
.
//
Stash the current selection, then make a new one
.
int
currentSelectedIndex
=
selectedIndex
;
int
currentSelectedIndex
=
selectedIndex
;
Format
currentFormat
=
getSelectedFormat
();
selectedIndex
=
determineIdealSelectedIndex
(
nowMs
);
int
idealSelectedIndex
=
determineIdealSelectedIndex
(
nowMs
);
if
(
selectedIndex
==
currentSelectedIndex
)
{
Format
idealFormat
=
getFormat
(
idealSelectedIndex
);
return
;
// Assume we can switch to the ideal selection.
}
selectedIndex
=
idealSelectedIndex
;
if
(!
isBlacklisted
(
currentSelectedIndex
,
nowMs
))
{
// Revert back to the current selection if conditions are not suitable for switching.
// Revert back to the current selection if conditions are not suitable for switching.
if
(
currentFormat
!=
null
&&
!
isBlacklisted
(
selectedIndex
,
nowMs
))
{
Format
currentFormat
=
getFormat
(
currentSelectedIndex
);
if
(
idealFormat
.
bitrate
>
currentFormat
.
bitrate
Format
selectedFormat
=
getFormat
(
selectedIndex
);
if
(
selectedFormat
.
bitrate
>
currentFormat
.
bitrate
&&
bufferedDurationUs
<
minDurationForQualityIncreaseUs
)
{
&&
bufferedDurationUs
<
minDurationForQualityIncreaseUs
)
{
// The
ideal
track is a higher quality, but we have insufficient buffer to safely switch
// The
selected
track is a higher quality, but we have insufficient buffer to safely switch
// up. Defer switching up for now.
// up. Defer switching up for now.
selectedIndex
=
currentSelectedIndex
;
selectedIndex
=
currentSelectedIndex
;
}
else
if
(
ideal
Format
.
bitrate
<
currentFormat
.
bitrate
}
else
if
(
selected
Format
.
bitrate
<
currentFormat
.
bitrate
&&
bufferedDurationUs
>=
maxDurationForQualityDecreaseUs
)
{
&&
bufferedDurationUs
>=
maxDurationForQualityDecreaseUs
)
{
// The
ideal
track is a lower quality, but we have sufficient buffer to defer switching
// The
selected
track is a lower quality, but we have sufficient buffer to defer switching
// down for now.
// down for now.
selectedIndex
=
currentSelectedIndex
;
selectedIndex
=
currentSelectedIndex
;
}
}
...
...
library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
View file @
3ada4e17
...
@@ -436,35 +436,48 @@ public class DefaultTrackSelector extends MappingTrackSelector {
...
@@ -436,35 +436,48 @@ public class DefaultTrackSelector extends MappingTrackSelector {
int
rendererCount
=
rendererCapabilities
.
length
;
int
rendererCount
=
rendererCapabilities
.
length
;
TrackSelection
[]
rendererTrackSelections
=
new
TrackSelection
[
rendererCount
];
TrackSelection
[]
rendererTrackSelections
=
new
TrackSelection
[
rendererCount
];
Parameters
params
=
paramsReference
.
get
();
Parameters
params
=
paramsReference
.
get
();
boolean
videoTrackAndRendererPresent
=
false
;
boolean
seenVideoRendererWithMappedTracks
=
false
;
boolean
selectedVideoTracks
=
false
;
for
(
int
i
=
0
;
i
<
rendererCount
;
i
++)
{
for
(
int
i
=
0
;
i
<
rendererCount
;
i
++)
{
if
(
C
.
TRACK_TYPE_VIDEO
==
rendererCapabilities
[
i
].
getTrackType
())
{
if
(
C
.
TRACK_TYPE_VIDEO
==
rendererCapabilities
[
i
].
getTrackType
())
{
rendererTrackSelections
[
i
]
=
selectVideoTrack
(
rendererCapabilities
[
i
],
if
(!
selectedVideoTracks
)
{
rendererTrackGroupArrays
[
i
],
rendererFormatSupports
[
i
],
params
.
maxVideoWidth
,
rendererTrackSelections
[
i
]
=
selectVideoTrack
(
rendererCapabilities
[
i
],
params
.
maxVideoHeight
,
params
.
maxVideoBitrate
,
params
.
allowNonSeamlessAdaptiveness
,
rendererTrackGroupArrays
[
i
],
rendererFormatSupports
[
i
],
params
.
maxVideoWidth
,
params
.
allowMixedMimeAdaptiveness
,
params
.
viewportWidth
,
params
.
viewportHeight
,
params
.
maxVideoHeight
,
params
.
maxVideoBitrate
,
params
.
allowNonSeamlessAdaptiveness
,
params
.
orientationMayChange
,
adaptiveTrackSelectionFactory
,
params
.
allowMixedMimeAdaptiveness
,
params
.
viewportWidth
,
params
.
viewportHeight
,
params
.
exceedVideoConstraintsIfNecessary
,
params
.
exceedRendererCapabilitiesIfNecessary
);
params
.
orientationMayChange
,
adaptiveTrackSelectionFactory
,
videoTrackAndRendererPresent
|=
rendererTrackGroupArrays
[
i
].
length
>
0
;
params
.
exceedVideoConstraintsIfNecessary
,
params
.
exceedRendererCapabilitiesIfNecessary
);
selectedVideoTracks
=
rendererTrackSelections
[
i
]
!=
null
;
}
seenVideoRendererWithMappedTracks
|=
rendererTrackGroupArrays
[
i
].
length
>
0
;
}
}
}
}
boolean
selectedAudioTracks
=
false
;
boolean
selectedTextTracks
=
false
;
for
(
int
i
=
0
;
i
<
rendererCount
;
i
++)
{
for
(
int
i
=
0
;
i
<
rendererCount
;
i
++)
{
switch
(
rendererCapabilities
[
i
].
getTrackType
())
{
switch
(
rendererCapabilities
[
i
].
getTrackType
())
{
case
C
.
TRACK_TYPE_VIDEO
:
case
C
.
TRACK_TYPE_VIDEO
:
// Already done. Do nothing.
// Already done. Do nothing.
break
;
break
;
case
C
.
TRACK_TYPE_AUDIO
:
case
C
.
TRACK_TYPE_AUDIO
:
rendererTrackSelections
[
i
]
=
selectAudioTrack
(
rendererTrackGroupArrays
[
i
],
if
(!
selectedAudioTracks
)
{
rendererFormatSupports
[
i
],
params
.
preferredAudioLanguage
,
rendererTrackSelections
[
i
]
=
selectAudioTrack
(
rendererTrackGroupArrays
[
i
],
params
.
exceedRendererCapabilitiesIfNecessary
,
params
.
allowMixedMimeAdaptiveness
,
rendererFormatSupports
[
i
],
params
.
preferredAudioLanguage
,
videoTrackAndRendererPresent
?
null
:
adaptiveTrackSelectionFactory
);
params
.
exceedRendererCapabilitiesIfNecessary
,
params
.
allowMixedMimeAdaptiveness
,
seenVideoRendererWithMappedTracks
?
null
:
adaptiveTrackSelectionFactory
);
selectedAudioTracks
=
rendererTrackSelections
[
i
]
!=
null
;
}
break
;
break
;
case
C
.
TRACK_TYPE_TEXT
:
case
C
.
TRACK_TYPE_TEXT
:
rendererTrackSelections
[
i
]
=
selectTextTrack
(
rendererTrackGroupArrays
[
i
],
if
(!
selectedTextTracks
)
{
rendererFormatSupports
[
i
],
params
.
preferredTextLanguage
,
rendererTrackSelections
[
i
]
=
selectTextTrack
(
rendererTrackGroupArrays
[
i
],
params
.
preferredAudioLanguage
,
params
.
exceedRendererCapabilitiesIfNecessary
);
rendererFormatSupports
[
i
],
params
.
preferredTextLanguage
,
params
.
preferredAudioLanguage
,
params
.
exceedRendererCapabilitiesIfNecessary
);
selectedTextTracks
=
rendererTrackSelections
[
i
]
!=
null
;
}
break
;
break
;
default
:
default
:
rendererTrackSelections
[
i
]
=
selectOtherTrack
(
rendererCapabilities
[
i
].
getTrackType
(),
rendererTrackSelections
[
i
]
=
selectOtherTrack
(
rendererCapabilities
[
i
].
getTrackType
(),
...
@@ -626,7 +639,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
...
@@ -626,7 +639,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
continue
;
continue
;
}
}
int
trackScore
=
isWithinConstraints
?
2
:
1
;
int
trackScore
=
isWithinConstraints
?
2
:
1
;
if
(
isSupported
(
trackFormatSupport
[
trackIndex
],
false
))
{
boolean
isWithinCapabilities
=
isSupported
(
trackFormatSupport
[
trackIndex
],
false
);
if
(
isWithinCapabilities
)
{
trackScore
+=
WITHIN_RENDERER_CAPABILITIES_BONUS
;
trackScore
+=
WITHIN_RENDERER_CAPABILITIES_BONUS
;
}
}
boolean
selectTrack
=
trackScore
>
selectedTrackScore
;
boolean
selectTrack
=
trackScore
>
selectedTrackScore
;
...
@@ -642,7 +656,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
...
@@ -642,7 +656,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
else
{
}
else
{
comparisonResult
=
compareFormatValues
(
format
.
bitrate
,
selectedBitrate
);
comparisonResult
=
compareFormatValues
(
format
.
bitrate
,
selectedBitrate
);
}
}
selectTrack
=
isWithinConstraints
?
comparisonResult
>
0
:
comparisonResult
<
0
;
selectTrack
=
isWithinCapabilities
&&
isWithinConstraints
?
comparisonResult
>
0
:
comparisonResult
<
0
;
}
}
if
(
selectTrack
)
{
if
(
selectTrack
)
{
selectedGroup
=
trackGroup
;
selectedGroup
=
trackGroup
;
...
...
library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
View file @
3ada4e17
...
@@ -61,6 +61,7 @@ public final class MimeTypes {
...
@@ -61,6 +61,7 @@ public final class MimeTypes {
public
static
final
String
AUDIO_AMR_WB
=
BASE_TYPE_AUDIO
+
"/amr-wb"
;
public
static
final
String
AUDIO_AMR_WB
=
BASE_TYPE_AUDIO
+
"/amr-wb"
;
public
static
final
String
AUDIO_FLAC
=
BASE_TYPE_AUDIO
+
"/x-flac"
;
public
static
final
String
AUDIO_FLAC
=
BASE_TYPE_AUDIO
+
"/x-flac"
;
public
static
final
String
AUDIO_ALAC
=
BASE_TYPE_AUDIO
+
"/alac"
;
public
static
final
String
AUDIO_ALAC
=
BASE_TYPE_AUDIO
+
"/alac"
;
public
static
final
String
AUDIO_UNKNOWN
=
BASE_TYPE_AUDIO
+
"/x-unknown"
;
public
static
final
String
TEXT_VTT
=
BASE_TYPE_TEXT
+
"/vtt"
;
public
static
final
String
TEXT_VTT
=
BASE_TYPE_TEXT
+
"/vtt"
;
...
...
library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
View file @
3ada4e17
...
@@ -98,7 +98,7 @@ public final class Util {
...
@@ -98,7 +98,7 @@ public final class Util {
private
static
final
Pattern
XS_DATE_TIME_PATTERN
=
Pattern
.
compile
(
private
static
final
Pattern
XS_DATE_TIME_PATTERN
=
Pattern
.
compile
(
"(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
"(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
+
"(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?"
+
"(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?"
+
"([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?"
);
+
"([Zz]|((\\+|\\-)(\\d
?
\\d):?(\\d\\d)))?"
);
private
static
final
Pattern
XS_DURATION_PATTERN
=
private
static
final
Pattern
XS_DURATION_PATTERN
=
Pattern
.
compile
(
"^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
Pattern
.
compile
(
"^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+
"(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"
);
+
"(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"
);
...
...
library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
View file @
3ada4e17
...
@@ -76,7 +76,7 @@ public final class DummySurface extends Surface {
...
@@ -76,7 +76,7 @@ public final class DummySurface extends Surface {
if
(
Util
.
SDK_INT
>=
17
)
{
if
(
Util
.
SDK_INT
>=
17
)
{
EGLDisplay
display
=
eglGetDisplay
(
EGL_DEFAULT_DISPLAY
);
EGLDisplay
display
=
eglGetDisplay
(
EGL_DEFAULT_DISPLAY
);
String
extensions
=
EGL14
.
eglQueryString
(
display
,
EGL10
.
EGL_EXTENSIONS
);
String
extensions
=
EGL14
.
eglQueryString
(
display
,
EGL10
.
EGL_EXTENSIONS
);
SECURE_SUPPORTED
=
extensions
.
contains
(
"EGL_EXT_protected_content"
);
SECURE_SUPPORTED
=
extensions
!=
null
&&
extensions
.
contains
(
"EGL_EXT_protected_content"
);
}
else
{
}
else
{
SECURE_SUPPORTED
=
false
;
SECURE_SUPPORTED
=
false
;
}
}
...
...
library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
View file @
3ada4e17
...
@@ -650,7 +650,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
...
@@ -650,7 +650,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* @return Suitable {@link CodecMaxValues}.
* @return Suitable {@link CodecMaxValues}.
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
*/
*/
pr
ivate
static
CodecMaxValues
getCodecMaxValues
(
MediaCodecInfo
codecInfo
,
Format
format
,
pr
otected
CodecMaxValues
getCodecMaxValues
(
MediaCodecInfo
codecInfo
,
Format
format
,
Format
[]
streamFormats
)
throws
DecoderQueryException
{
Format
[]
streamFormats
)
throws
DecoderQueryException
{
int
maxWidth
=
format
.
width
;
int
maxWidth
=
format
.
width
;
int
maxHeight
=
format
.
height
;
int
maxHeight
=
format
.
height
;
...
@@ -838,7 +838,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
...
@@ -838,7 +838,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return
format
.
rotationDegrees
==
Format
.
NO_VALUE
?
0
:
format
.
rotationDegrees
;
return
format
.
rotationDegrees
==
Format
.
NO_VALUE
?
0
:
format
.
rotationDegrees
;
}
}
pr
ivate
static
final
class
CodecMaxValues
{
pr
otected
static
final
class
CodecMaxValues
{
public
final
int
width
;
public
final
int
width
;
public
final
int
height
;
public
final
int
height
;
...
...
library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
View file @
3ada4e17
...
@@ -410,12 +410,14 @@ public final class DashMediaSource implements MediaSource {
...
@@ -410,12 +410,14 @@ public final class DashMediaSource implements MediaSource {
private
void
resolveUtcTimingElement
(
UtcTimingElement
timingElement
)
{
private
void
resolveUtcTimingElement
(
UtcTimingElement
timingElement
)
{
String
scheme
=
timingElement
.
schemeIdUri
;
String
scheme
=
timingElement
.
schemeIdUri
;
if
(
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:direct:2012"
))
{
if
(
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:direct:2014"
)
||
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:direct:2012"
))
{
resolveUtcTimingElementDirect
(
timingElement
);
resolveUtcTimingElementDirect
(
timingElement
);
}
else
if
(
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:http-iso:2014"
))
{
}
else
if
(
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:http-iso:2014"
)
||
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:http-iso:2012"
))
{
resolveUtcTimingElementHttp
(
timingElement
,
new
Iso8601Parser
());
resolveUtcTimingElementHttp
(
timingElement
,
new
Iso8601Parser
());
}
else
if
(
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:http-xsdate:201
2
"
)
}
else
if
(
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:http-xsdate:201
4
"
)
||
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:http-xsdate:201
4
"
))
{
||
Util
.
areEqual
(
scheme
,
"urn:mpeg:dash:utc:http-xsdate:201
2
"
))
{
resolveUtcTimingElementHttp
(
timingElement
,
new
XsDateTimeParser
());
resolveUtcTimingElementHttp
(
timingElement
,
new
XsDateTimeParser
());
}
else
{
}
else
{
// Unsupported scheme.
// Unsupported scheme.
...
...
library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java
View file @
3ada4e17
...
@@ -51,6 +51,15 @@ public class HlsMasterPlaylistParserTest extends TestCase {
...
@@ -51,6 +51,15 @@ public class HlsMasterPlaylistParserTest extends TestCase {
+
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+
"http://example.com/audio-only.m3u8"
;
+
"http://example.com/audio-only.m3u8"
;
private
static
final
String
AVG_BANDWIDTH_MASTER_PLAYLIST
=
" #EXTM3U \n"
+
"\n"
+
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+
"http://example.com/low.m3u8\n"
+
"\n"
+
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000,"
+
"CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+
"http://example.com/spaces_in_codecs.m3u8\n"
;
private
static
final
String
PLAYLIST_WITH_INVALID_HEADER
=
"#EXTMU3\n"
private
static
final
String
PLAYLIST_WITH_INVALID_HEADER
=
"#EXTMU3\n"
+
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+
"http://example.com/low.m3u8\n"
;
+
"http://example.com/low.m3u8\n"
;
...
@@ -70,42 +79,48 @@ public class HlsMasterPlaylistParserTest extends TestCase {
...
@@ -70,42 +79,48 @@ public class HlsMasterPlaylistParserTest extends TestCase {
HlsMasterPlaylist
masterPlaylist
=
parseMasterPlaylist
(
PLAYLIST_URI
,
MASTER_PLAYLIST
);
HlsMasterPlaylist
masterPlaylist
=
parseMasterPlaylist
(
PLAYLIST_URI
,
MASTER_PLAYLIST
);
List
<
HlsMasterPlaylist
.
HlsUrl
>
variants
=
masterPlaylist
.
variants
;
List
<
HlsMasterPlaylist
.
HlsUrl
>
variants
=
masterPlaylist
.
variants
;
assertNotNull
(
variants
);
assertEquals
(
5
,
variants
.
size
());
assertEquals
(
5
,
variants
.
size
());
assertNull
(
masterPlaylist
.
muxedCaptionFormats
);
assertNull
(
masterPlaylist
.
muxedCaptionFormats
);
assertEquals
(
1280000
,
variants
.
get
(
0
).
format
.
bitrate
);
assertEquals
(
1280000
,
variants
.
get
(
0
).
format
.
bitrate
);
assertNotNull
(
variants
.
get
(
0
).
format
.
codecs
);
assertEquals
(
"mp4a.40.2,avc1.66.30"
,
variants
.
get
(
0
).
format
.
codecs
);
assertEquals
(
"mp4a.40.2,avc1.66.30"
,
variants
.
get
(
0
).
format
.
codecs
);
assertEquals
(
304
,
variants
.
get
(
0
).
format
.
width
);
assertEquals
(
304
,
variants
.
get
(
0
).
format
.
width
);
assertEquals
(
128
,
variants
.
get
(
0
).
format
.
height
);
assertEquals
(
128
,
variants
.
get
(
0
).
format
.
height
);
assertEquals
(
"http://example.com/low.m3u8"
,
variants
.
get
(
0
).
url
);
assertEquals
(
"http://example.com/low.m3u8"
,
variants
.
get
(
0
).
url
);
assertEquals
(
1280000
,
variants
.
get
(
1
).
format
.
bitrate
);
assertEquals
(
1280000
,
variants
.
get
(
1
).
format
.
bitrate
);
assertNotNull
(
variants
.
get
(
1
).
format
.
codecs
);
assertEquals
(
"mp4a.40.2 , avc1.66.30 "
,
variants
.
get
(
1
).
format
.
codecs
);
assertEquals
(
"mp4a.40.2 , avc1.66.30 "
,
variants
.
get
(
1
).
format
.
codecs
);
assertEquals
(
"http://example.com/spaces_in_codecs.m3u8"
,
variants
.
get
(
1
).
url
);
assertEquals
(
"http://example.com/spaces_in_codecs.m3u8"
,
variants
.
get
(
1
).
url
);
assertEquals
(
2560000
,
variants
.
get
(
2
).
format
.
bitrate
);
assertEquals
(
2560000
,
variants
.
get
(
2
).
format
.
bitrate
);
assert
Equals
(
null
,
variants
.
get
(
2
).
format
.
codecs
);
assert
Null
(
variants
.
get
(
2
).
format
.
codecs
);
assertEquals
(
384
,
variants
.
get
(
2
).
format
.
width
);
assertEquals
(
384
,
variants
.
get
(
2
).
format
.
width
);
assertEquals
(
160
,
variants
.
get
(
2
).
format
.
height
);
assertEquals
(
160
,
variants
.
get
(
2
).
format
.
height
);
assertEquals
(
"http://example.com/mid.m3u8"
,
variants
.
get
(
2
).
url
);
assertEquals
(
"http://example.com/mid.m3u8"
,
variants
.
get
(
2
).
url
);
assertEquals
(
7680000
,
variants
.
get
(
3
).
format
.
bitrate
);
assertEquals
(
7680000
,
variants
.
get
(
3
).
format
.
bitrate
);
assert
Equals
(
null
,
variants
.
get
(
3
).
format
.
codecs
);
assert
Null
(
variants
.
get
(
3
).
format
.
codecs
);
assertEquals
(
Format
.
NO_VALUE
,
variants
.
get
(
3
).
format
.
width
);
assertEquals
(
Format
.
NO_VALUE
,
variants
.
get
(
3
).
format
.
width
);
assertEquals
(
Format
.
NO_VALUE
,
variants
.
get
(
3
).
format
.
height
);
assertEquals
(
Format
.
NO_VALUE
,
variants
.
get
(
3
).
format
.
height
);
assertEquals
(
"http://example.com/hi.m3u8"
,
variants
.
get
(
3
).
url
);
assertEquals
(
"http://example.com/hi.m3u8"
,
variants
.
get
(
3
).
url
);
assertEquals
(
65000
,
variants
.
get
(
4
).
format
.
bitrate
);
assertEquals
(
65000
,
variants
.
get
(
4
).
format
.
bitrate
);
assertNotNull
(
variants
.
get
(
4
).
format
.
codecs
);
assertEquals
(
"mp4a.40.5"
,
variants
.
get
(
4
).
format
.
codecs
);
assertEquals
(
"mp4a.40.5"
,
variants
.
get
(
4
).
format
.
codecs
);
assertEquals
(
Format
.
NO_VALUE
,
variants
.
get
(
4
).
format
.
width
);
assertEquals
(
Format
.
NO_VALUE
,
variants
.
get
(
4
).
format
.
width
);
assertEquals
(
Format
.
NO_VALUE
,
variants
.
get
(
4
).
format
.
height
);
assertEquals
(
Format
.
NO_VALUE
,
variants
.
get
(
4
).
format
.
height
);
assertEquals
(
"http://example.com/audio-only.m3u8"
,
variants
.
get
(
4
).
url
);
assertEquals
(
"http://example.com/audio-only.m3u8"
,
variants
.
get
(
4
).
url
);
}
}
public
void
testMasterPlaylistWithBandwdithAverage
()
throws
IOException
{
HlsMasterPlaylist
masterPlaylist
=
parseMasterPlaylist
(
PLAYLIST_URI
,
AVG_BANDWIDTH_MASTER_PLAYLIST
);
List
<
HlsMasterPlaylist
.
HlsUrl
>
variants
=
masterPlaylist
.
variants
;
assertEquals
(
1280000
,
variants
.
get
(
0
).
format
.
bitrate
);
assertEquals
(
1270000
,
variants
.
get
(
1
).
format
.
bitrate
);
}
public
void
testPlaylistWithInvalidHeader
()
throws
IOException
{
public
void
testPlaylistWithInvalidHeader
()
throws
IOException
{
try
{
try
{
parseMasterPlaylist
(
PLAYLIST_URI
,
PLAYLIST_WITH_INVALID_HEADER
);
parseMasterPlaylist
(
PLAYLIST_URI
,
PLAYLIST_WITH_INVALID_HEADER
);
...
...
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
View file @
3ada4e17
...
@@ -73,7 +73,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
...
@@ -73,7 +73,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private
static
final
String
ATTR_CLOSED_CAPTIONS_NONE
=
"CLOSED-CAPTIONS=NONE"
;
private
static
final
String
ATTR_CLOSED_CAPTIONS_NONE
=
"CLOSED-CAPTIONS=NONE"
;
private
static
final
Pattern
REGEX_BANDWIDTH
=
Pattern
.
compile
(
"BANDWIDTH=(\\d+)\\b"
);
private
static
final
Pattern
REGEX_AVERAGE_BANDWIDTH
=
Pattern
.
compile
(
"AVERAGE-BANDWIDTH=(\\d+)\\b"
);
private
static
final
Pattern
REGEX_BANDWIDTH
=
Pattern
.
compile
(
"[^-]BANDWIDTH=(\\d+)\\b"
);
private
static
final
Pattern
REGEX_CODECS
=
Pattern
.
compile
(
"CODECS=\"(.+?)\""
);
private
static
final
Pattern
REGEX_CODECS
=
Pattern
.
compile
(
"CODECS=\"(.+?)\""
);
private
static
final
Pattern
REGEX_RESOLUTION
=
Pattern
.
compile
(
"RESOLUTION=(\\d+x\\d+)"
);
private
static
final
Pattern
REGEX_RESOLUTION
=
Pattern
.
compile
(
"RESOLUTION=(\\d+x\\d+)"
);
private
static
final
Pattern
REGEX_TARGET_DURATION
=
Pattern
.
compile
(
TAG_TARGET_DURATION
private
static
final
Pattern
REGEX_TARGET_DURATION
=
Pattern
.
compile
(
TAG_TARGET_DURATION
...
@@ -226,6 +228,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
...
@@ -226,6 +228,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
}
}
else
if
(
line
.
startsWith
(
TAG_STREAM_INF
))
{
}
else
if
(
line
.
startsWith
(
TAG_STREAM_INF
))
{
int
bitrate
=
parseIntAttr
(
line
,
REGEX_BANDWIDTH
);
int
bitrate
=
parseIntAttr
(
line
,
REGEX_BANDWIDTH
);
String
averageBandwidthString
=
parseOptionalStringAttr
(
line
,
REGEX_AVERAGE_BANDWIDTH
);
if
(
averageBandwidthString
!=
null
)
{
// If available, the average bandwidth attribute is used as the variant's bitrate.
bitrate
=
Integer
.
parseInt
(
averageBandwidthString
);
}
String
codecs
=
parseOptionalStringAttr
(
line
,
REGEX_CODECS
);
String
codecs
=
parseOptionalStringAttr
(
line
,
REGEX_CODECS
);
String
resolutionString
=
parseOptionalStringAttr
(
line
,
REGEX_RESOLUTION
);
String
resolutionString
=
parseOptionalStringAttr
(
line
,
REGEX_RESOLUTION
);
noClosedCaptions
|=
line
.
contains
(
ATTR_CLOSED_CAPTIONS_NONE
);
noClosedCaptions
|=
line
.
contains
(
ATTR_CLOSED_CAPTIONS_NONE
);
...
@@ -300,8 +307,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
...
@@ -300,8 +307,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
playlistType
=
HlsMediaPlaylist
.
PLAYLIST_TYPE_VOD
;
playlistType
=
HlsMediaPlaylist
.
PLAYLIST_TYPE_VOD
;
}
else
if
(
"EVENT"
.
equals
(
playlistTypeString
))
{
}
else
if
(
"EVENT"
.
equals
(
playlistTypeString
))
{
playlistType
=
HlsMediaPlaylist
.
PLAYLIST_TYPE_EVENT
;
playlistType
=
HlsMediaPlaylist
.
PLAYLIST_TYPE_EVENT
;
}
else
{
throw
new
ParserException
(
"Illegal playlist type: "
+
playlistTypeString
);
}
}
}
else
if
(
line
.
startsWith
(
TAG_START
))
{
}
else
if
(
line
.
startsWith
(
TAG_START
))
{
startOffsetUs
=
(
long
)
(
parseDoubleAttr
(
line
,
REGEX_TIME_OFFSET
)
*
C
.
MICROS_PER_SECOND
);
startOffsetUs
=
(
long
)
(
parseDoubleAttr
(
line
,
REGEX_TIME_OFFSET
)
*
C
.
MICROS_PER_SECOND
);
...
@@ -390,14 +395,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
...
@@ -390,14 +395,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
dateRanges
);
dateRanges
);
}
}
private
static
String
parseStringAttr
(
String
line
,
Pattern
pattern
)
throws
ParserException
{
Matcher
matcher
=
pattern
.
matcher
(
line
);
if
(
matcher
.
find
()
&&
matcher
.
groupCount
()
==
1
)
{
return
matcher
.
group
(
1
);
}
throw
new
ParserException
(
"Couldn't match "
+
pattern
.
pattern
()
+
" in "
+
line
);
}
private
static
int
parseIntAttr
(
String
line
,
Pattern
pattern
)
throws
ParserException
{
private
static
int
parseIntAttr
(
String
line
,
Pattern
pattern
)
throws
ParserException
{
return
Integer
.
parseInt
(
parseStringAttr
(
line
,
pattern
));
return
Integer
.
parseInt
(
parseStringAttr
(
line
,
pattern
));
}
}
...
@@ -408,10 +405,15 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
...
@@ -408,10 +405,15 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private
static
String
parseOptionalStringAttr
(
String
line
,
Pattern
pattern
)
{
private
static
String
parseOptionalStringAttr
(
String
line
,
Pattern
pattern
)
{
Matcher
matcher
=
pattern
.
matcher
(
line
);
Matcher
matcher
=
pattern
.
matcher
(
line
);
if
(
matcher
.
find
())
{
return
matcher
.
find
()
?
matcher
.
group
(
1
)
:
null
;
}
private
static
String
parseStringAttr
(
String
line
,
Pattern
pattern
)
throws
ParserException
{
Matcher
matcher
=
pattern
.
matcher
(
line
);
if
(
matcher
.
find
()
&&
matcher
.
groupCount
()
==
1
)
{
return
matcher
.
group
(
1
);
return
matcher
.
group
(
1
);
}
}
return
null
;
throw
new
ParserException
(
"Couldn't match "
+
pattern
.
pattern
()
+
" in "
+
line
)
;
}
}
private
static
boolean
parseBooleanAttribute
(
String
line
,
Pattern
pattern
,
boolean
defaultValue
)
{
private
static
boolean
parseBooleanAttribute
(
String
line
,
Pattern
pattern
,
boolean
defaultValue
)
{
...
...
library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
View file @
3ada4e17
...
@@ -287,39 +287,41 @@ public final class SsMediaSource implements MediaSource,
...
@@ -287,39 +287,41 @@ public final class SsMediaSource implements MediaSource,
for
(
int
i
=
0
;
i
<
mediaPeriods
.
size
();
i
++)
{
for
(
int
i
=
0
;
i
<
mediaPeriods
.
size
();
i
++)
{
mediaPeriods
.
get
(
i
).
updateManifest
(
manifest
);
mediaPeriods
.
get
(
i
).
updateManifest
(
manifest
);
}
}
long
startTimeUs
=
Long
.
MAX_VALUE
;
long
endTimeUs
=
Long
.
MIN_VALUE
;
for
(
StreamElement
element
:
manifest
.
streamElements
)
{
if
(
element
.
chunkCount
>
0
)
{
startTimeUs
=
Math
.
min
(
startTimeUs
,
element
.
getStartTimeUs
(
0
));
endTimeUs
=
Math
.
max
(
endTimeUs
,
element
.
getStartTimeUs
(
element
.
chunkCount
-
1
)
+
element
.
getChunkDurationUs
(
element
.
chunkCount
-
1
));
}
}
Timeline
timeline
;
Timeline
timeline
;
if
(
manifest
.
isLive
)
{
if
(
startTimeUs
==
Long
.
MAX_VALUE
)
{
long
startTimeUs
=
Long
.
MAX_VALUE
;
long
periodDurationUs
=
manifest
.
isLive
?
C
.
TIME_UNSET
:
0
;
long
endTimeUs
=
Long
.
MIN_VALUE
;
timeline
=
new
SinglePeriodTimeline
(
periodDurationUs
,
0
,
0
,
0
,
true
/* isSeekable */
,
for
(
int
i
=
0
;
i
<
manifest
.
streamElements
.
length
;
i
++)
{
manifest
.
isLive
/* isDynamic */
);
StreamElement
element
=
manifest
.
streamElements
[
i
];
}
else
if
(
manifest
.
isLive
)
{
if
(
element
.
chunkCount
>
0
)
{
if
(
manifest
.
dvrWindowLengthUs
!=
C
.
TIME_UNSET
&&
manifest
.
dvrWindowLengthUs
>
0
)
{
startTimeUs
=
Math
.
min
(
startTimeUs
,
element
.
getStartTimeUs
(
0
));
startTimeUs
=
Math
.
max
(
startTimeUs
,
endTimeUs
-
manifest
.
dvrWindowLengthUs
);
endTimeUs
=
Math
.
max
(
endTimeUs
,
element
.
getStartTimeUs
(
element
.
chunkCount
-
1
)
+
element
.
getChunkDurationUs
(
element
.
chunkCount
-
1
));
}
}
}
if
(
startTimeUs
==
Long
.
MAX_VALUE
)
{
long
durationUs
=
endTimeUs
-
startTimeUs
;
timeline
=
new
SinglePeriodTimeline
(
C
.
TIME_UNSET
,
false
);
long
defaultStartPositionUs
=
durationUs
-
C
.
msToUs
(
livePresentationDelayMs
);
}
else
{
if
(
defaultStartPositionUs
<
MIN_LIVE_DEFAULT_START_POSITION_US
)
{
if
(
manifest
.
dvrWindowLengthUs
!=
C
.
TIME_UNSET
// The default start position is too close to the start of the live window. Set it to the
&&
manifest
.
dvrWindowLengthUs
>
0
)
{
// minimum default start position provided the window is at least twice as big. Else set
startTimeUs
=
Math
.
max
(
startTimeUs
,
endTimeUs
-
manifest
.
dvrWindowLengthUs
);
// it to the middle of the window.
}
defaultStartPositionUs
=
Math
.
min
(
MIN_LIVE_DEFAULT_START_POSITION_US
,
durationUs
/
2
);
long
durationUs
=
endTimeUs
-
startTimeUs
;
long
defaultStartPositionUs
=
durationUs
-
C
.
msToUs
(
livePresentationDelayMs
);
if
(
defaultStartPositionUs
<
MIN_LIVE_DEFAULT_START_POSITION_US
)
{
// The default start position is too close to the start of the live window. Set it to the
// minimum default start position provided the window is at least twice as big. Else set
// it to the middle of the window.
defaultStartPositionUs
=
Math
.
min
(
MIN_LIVE_DEFAULT_START_POSITION_US
,
durationUs
/
2
);
}
timeline
=
new
SinglePeriodTimeline
(
C
.
TIME_UNSET
,
durationUs
,
startTimeUs
,
defaultStartPositionUs
,
true
/* isSeekable */
,
true
/* isDynamic */
);
}
}
timeline
=
new
SinglePeriodTimeline
(
C
.
TIME_UNSET
,
durationUs
,
startTimeUs
,
defaultStartPositionUs
,
true
/* isSeekable */
,
true
/* isDynamic */
);
}
else
{
}
else
{
boolean
isSeekable
=
manifest
.
durationUs
!=
C
.
TIME_UNSET
;
long
durationUs
=
manifest
.
durationUs
!=
C
.
TIME_UNSET
?
manifest
.
durationUs
timeline
=
new
SinglePeriodTimeline
(
manifest
.
durationUs
,
isSeekable
);
:
endTimeUs
-
startTimeUs
;
timeline
=
new
SinglePeriodTimeline
(
startTimeUs
+
durationUs
,
durationUs
,
startTimeUs
,
0
,
true
/* isSeekable */
,
false
/* isDynamic */
);
}
}
sourceListener
.
onSourceInfoRefreshed
(
timeline
,
manifest
);
sourceListener
.
onSourceInfoRefreshed
(
timeline
,
manifest
);
}
}
...
...
library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java
View file @
3ada4e17
...
@@ -220,11 +220,13 @@ public class DefaultTimeBar extends View implements TimeBar {
...
@@ -220,11 +220,13 @@ public class DefaultTimeBar extends View implements TimeBar {
public
void
setPosition
(
long
position
)
{
public
void
setPosition
(
long
position
)
{
this
.
position
=
position
;
this
.
position
=
position
;
setContentDescription
(
getProgressText
());
setContentDescription
(
getProgressText
());
update
();
}
}
@Override
@Override
public
void
setBufferedPosition
(
long
bufferedPosition
)
{
public
void
setBufferedPosition
(
long
bufferedPosition
)
{
this
.
bufferedPosition
=
bufferedPosition
;
this
.
bufferedPosition
=
bufferedPosition
;
update
();
}
}
@Override
@Override
...
@@ -235,6 +237,7 @@ public class DefaultTimeBar extends View implements TimeBar {
...
@@ -235,6 +237,7 @@ public class DefaultTimeBar extends View implements TimeBar {
}
else
{
}
else
{
updateScrubberState
();
updateScrubberState
();
}
}
update
();
}
}
@Override
@Override
...
@@ -242,6 +245,7 @@ public class DefaultTimeBar extends View implements TimeBar {
...
@@ -242,6 +245,7 @@ public class DefaultTimeBar extends View implements TimeBar {
Assertions
.
checkArgument
(
adBreakCount
==
0
||
adBreakTimesMs
!=
null
);
Assertions
.
checkArgument
(
adBreakCount
==
0
||
adBreakTimesMs
!=
null
);
this
.
adBreakCount
=
adBreakCount
;
this
.
adBreakCount
=
adBreakCount
;
this
.
adBreakTimesMs
=
adBreakTimesMs
;
this
.
adBreakTimesMs
=
adBreakTimesMs
;
update
();
}
}
@Override
@Override
...
@@ -438,7 +442,7 @@ public class DefaultTimeBar extends View implements TimeBar {
...
@@ -438,7 +442,7 @@ public class DefaultTimeBar extends View implements TimeBar {
parent
.
requestDisallowInterceptTouchEvent
(
true
);
parent
.
requestDisallowInterceptTouchEvent
(
true
);
}
}
if
(
listener
!=
null
)
{
if
(
listener
!=
null
)
{
listener
.
onScrubStart
(
this
);
listener
.
onScrubStart
(
this
,
getScrubberPosition
()
);
}
}
}
}
...
...
library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
View file @
3ada4e17
...
@@ -875,7 +875,7 @@ public class PlaybackControlView extends FrameLayout {
...
@@ -875,7 +875,7 @@ public class PlaybackControlView extends FrameLayout {
OnClickListener
{
OnClickListener
{
@Override
@Override
public
void
onScrubStart
(
TimeBar
timeBar
)
{
public
void
onScrubStart
(
TimeBar
timeBar
,
long
position
)
{
removeCallbacks
(
hideAction
);
removeCallbacks
(
hideAction
);
scrubbing
=
true
;
scrubbing
=
true
;
}
}
...
...
library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java
View file @
3ada4e17
...
@@ -95,8 +95,9 @@ public interface TimeBar {
...
@@ -95,8 +95,9 @@ public interface TimeBar {
* Called when the user starts moving the scrubber.
* Called when the user starts moving the scrubber.
*
*
* @param timeBar The time bar.
* @param timeBar The time bar.
* @param position The position of the scrubber, in milliseconds.
*/
*/
void
onScrubStart
(
TimeBar
timeBar
);
void
onScrubStart
(
TimeBar
timeBar
,
long
position
);
/**
/**
* Called when the user moves the scrubber.
* Called when the user moves the scrubber.
...
...
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