diff --git a/README.md b/README.md
index 3de86d21a3..d7bc23f700 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,11 @@ and extend, and can be updated through Play Store application updates.
## Using ExoPlayer ##
+ExoPlayer modules can be obtained via jCenter. It's also possible to clone the
+repository and depend on the modules locally.
+
+### Via jCenter ###
+
The easiest way to get started using ExoPlayer is to add it as a gradle
dependency. You need to make sure you have the jcenter repository included in
the `build.gradle` file in the root of your project:
@@ -64,6 +69,39 @@ latest versions, see the [Release notes][].
[Bintray]: https://bintray.com/google/exoplayer
[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
+### Locally ###
+
+Cloning the repository and depending on the modules locally is required when
+using some ExoPlayer extension modules. It's also a suitable approach if you
+want to make local changes to ExoPlayer, or if you want to use a development
+branch.
+
+First, clone the repository into a local directory and checkout the desired
+branch:
+
+```sh
+git clone https://github.com/google/ExoPlayer.git
+git checkout release-v2
+```
+
+Next, add the following to your project's `settings.gradle` file, replacing
+`path/to/exoplayer` with the path to your local copy:
+
+```gradle
+gradle.ext.exoplayerRoot = 'path/to/exoplayer'
+gradle.ext.exoplayerModulePrefix = 'exoplayer-'
+apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')
+```
+
+You should now see the ExoPlayer modules appear as part of your project. You can
+depend on them as you would on any other local module, for example:
+
+```gradle
+compile project(':exoplayer-library-core')
+compile project(':exoplayer-library-dash')
+compile project(':exoplayer-library-ui)
+```
+
## Developing ExoPlayer ##
#### Project branches ####
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index ff1bd42fde..4101caad47 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -1,5 +1,59 @@
# Release notes #
+### r2.5.0 ###
+
+* IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an
+ easy and seamless way of incorporating display ads into ExoPlayer playbacks.
+ You can read more about the IMA extension
+ [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea).
+* MediaSession extension: Provides an easy to to connect ExoPlayer with
+ MediaSessionCompat in the Android Support Library.
+* RTMP extension: An extension for playing streams over RTMP.
+* Build: Made it easier for application developers to depend on a local checkout
+ of ExoPlayer. You can learn how to do this
+ [here](https://medium.com/google-exoplayer/howto-2-depend-on-a-local-checkout-of-exoplayer-bcd7f8531720).
+* Core playback improvements:
+ * Eliminated re-buffering when changing audio and text track selections during
+ playback of progressive streams
+ ([#2926](https://github.com/google/ExoPlayer/issues/2926)).
+ * New DynamicConcatenatingMediaSource class to support playback of dynamic
+ playlists.
+ * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode
+ during playback. Use of setRepeatMode should be preferred to
+ LoopingMediaSource for most looping use cases. You can read more about
+ setRepeatMode
+ [here](https://medium.com/google-exoplayer/repeat-modes-in-exoplayer-19dd85f036d3).
+ * Eliminated jank when switching video playback from one Surface to another on
+ API level 23+ for unencrypted content, and on devices that support the
+ EGL_EXT_protected_content OpenGL extension for protected content
+ ([#677](https://github.com/google/ExoPlayer/issues/677)).
+ * Enabled ExoPlayer instantiation on background threads without Loopers.
+ Events from such players are delivered on the application's main thread.
+* HLS improvements:
+ * Optimized adaptive switches for playlists that specify the
+ EXT-X-INDEPENDENT-SEGMENTS tag.
+ * Optimized in-buffer seeking
+ ([#551](https://github.com/google/ExoPlayer/issues/551)).
+ * Eliminated re-buffering when changing audio and text track selections during
+ playback, provided the new selection does not require switching to different
+ renditions ([#2718](https://github.com/google/ExoPlayer/issues/2718)).
+ * Exposed all media playlist tags in ExoPlayer's MediaPlaylist object.
+* DASH: Support for seamless switching across streams in different AdaptationSet
+ elements ([#2431](https://github.com/google/ExoPlayer/issues/2431)).
+* DRM: Support for additional crypto schemes (cbc1, cbcs and cens) on
+ API level 24+ ([#1989](https://github.com/google/ExoPlayer/issues/1989)).
+* Captions: Initial support for SSA/ASS subtitles
+ ([#889](https://github.com/google/ExoPlayer/issues/889)).
+* AndroidTV: Fixed issue where tunneled video playback would not start on some
+ devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)).
+* MPEG-TS: Fixed segmentation issue when parsing H262
+ ([#2891](https://github.com/google/ExoPlayer/issues/2891)).
+* Cronet extension: Support for a user-defined fallback if Cronet library is not
+ present.
+* Fix buffer too small IllegalStateException issue affecting some composite
+ media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)).
+* Misc bugfixes.
+
### r2.4.4 ###
* HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance
diff --git a/build.gradle b/build.gradle
index a4ae1f175e..dbc8a41eb0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -16,8 +16,8 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.3.1'
- classpath 'com.novoda:bintray-release:0.4.0'
+ classpath 'com.android.tools.build:gradle:2.3.3'
+ classpath 'com.novoda:bintray-release:0.5.0'
}
// Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070
@@ -31,25 +31,12 @@ buildscript {
allprojects {
repositories {
jcenter()
+ maven {
+ url "https://maven.google.com"
+ }
}
project.ext {
- // Important: ExoPlayer specifies a minSdkVersion of 9 because various
- // components provided by the library may be of use on older devices.
- // However, please note that the core media playback functionality
- // provided by the library requires API level 16 or greater.
- minSdkVersion = 9
- compileSdkVersion = 25
- targetSdkVersion = 25
- buildToolsVersion = '25'
- testSupportLibraryVersion = '0.5'
- supportLibraryVersion = '25.3.1'
- dexmakerVersion = '1.2'
- mockitoVersion = '1.9.5'
- releaseRepoName = getBintrayRepo()
- releaseUserOrg = 'google'
- releaseGroupId = 'com.google.android.exoplayer'
- releaseVersion = 'r2.4.4'
- releaseWebsite = 'https://github.com/google/ExoPlayer'
+ exoplayerPublishEnabled = true
}
if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) {
@@ -59,10 +46,4 @@ allprojects {
}
}
-def getBintrayRepo() {
- boolean publicRepo = hasProperty('publicRepo') &&
- property('publicRepo').toBoolean()
- return publicRepo ? 'exoplayer' : 'exoplayer-test'
-}
-
apply from: 'javadoc_combined.gradle'
diff --git a/constants.gradle b/constants.gradle
new file mode 100644
index 0000000000..7d126ccd89
--- /dev/null
+++ b/constants.gradle
@@ -0,0 +1,32 @@
+// Copyright (C) 2017 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.
+project.ext {
+ // Important: ExoPlayer specifies a minSdkVersion of 9 because various
+ // components provided by the library may be of use on older devices.
+ // However, please note that the core media playback functionality provided
+ // by the library requires API level 16 or greater.
+ minSdkVersion = 9
+ compileSdkVersion = 25
+ targetSdkVersion = 25
+ buildToolsVersion = '25'
+ testSupportLibraryVersion = '0.5'
+ supportLibraryVersion = '25.4.0'
+ dexmakerVersion = '1.2'
+ mockitoVersion = '1.9.5'
+ releaseVersion = 'r2.5.0'
+ modulePrefix = ':'
+ if (gradle.ext.has('exoplayerModulePrefix')) {
+ modulePrefix += gradle.ext.exoplayerModulePrefix
+ }
+}
diff --git a/core_settings.gradle b/core_settings.gradle
new file mode 100644
index 0000000000..20e7b235a2
--- /dev/null
+++ b/core_settings.gradle
@@ -0,0 +1,58 @@
+// Copyright (C) 2017 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.
+def rootDir = gradle.ext.exoplayerRoot
+def modulePrefix = ':'
+if (gradle.ext.has('exoplayerModulePrefix')) {
+ modulePrefix += gradle.ext.exoplayerModulePrefix
+}
+
+include modulePrefix + 'library'
+include modulePrefix + 'library-core'
+include modulePrefix + 'library-dash'
+include modulePrefix + 'library-hls'
+include modulePrefix + 'library-smoothstreaming'
+include modulePrefix + 'library-ui'
+include modulePrefix + 'testutils'
+include modulePrefix + 'extension-ffmpeg'
+include modulePrefix + 'extension-flac'
+include modulePrefix + 'extension-gvr'
+include modulePrefix + 'extension-ima'
+include modulePrefix + 'extension-mediasession'
+include modulePrefix + 'extension-okhttp'
+include modulePrefix + 'extension-opus'
+include modulePrefix + 'extension-vp9'
+include modulePrefix + 'extension-rtmp'
+
+project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
+project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
+project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')
+project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
+project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
+project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
+project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
+project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
+project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
+project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
+project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
+project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
+project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
+project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
+project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
+project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
+
+if (gradle.ext.has('exoplayerIncludeCronetExtension')
+ && gradle.ext.exoplayerIncludeCronetExtension) {
+ include modulePrefix + 'extension-cronet'
+ project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
+}
diff --git a/demo/build.gradle b/demo/build.gradle
index be5e52a25c..7eea25478f 100644
--- a/demo/build.gradle
+++ b/demo/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../constants.gradle'
apply plugin: 'com.android.application'
android {
@@ -45,13 +46,14 @@ android {
}
dependencies {
- compile project(':library-core')
- compile project(':library-dash')
- compile project(':library-hls')
- compile project(':library-smoothstreaming')
- compile project(':library-ui')
- withExtensionsCompile project(path: ':extension-ffmpeg')
- withExtensionsCompile project(path: ':extension-flac')
- withExtensionsCompile project(path: ':extension-opus')
- withExtensionsCompile project(path: ':extension-vp9')
+ compile project(modulePrefix + 'library-core')
+ compile project(modulePrefix + 'library-dash')
+ compile project(modulePrefix + 'library-hls')
+ compile project(modulePrefix + 'library-smoothstreaming')
+ compile project(modulePrefix + 'library-ui')
+ withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg')
+ withExtensionsCompile project(path: modulePrefix + 'extension-flac')
+ withExtensionsCompile project(path: modulePrefix + 'extension-ima')
+ withExtensionsCompile project(path: modulePrefix + 'extension-opus')
+ withExtensionsCompile project(path: modulePrefix + 'extension-vp9')
}
diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml
index afcddccac9..0e04d9a435 100644
--- a/demo/src/main/AndroidManifest.xml
+++ b/demo/src/main/AndroidManifest.xml
@@ -16,14 +16,14 @@
+ android:versionCode="2500"
+ android:versionName="2.5.0">
-
+ Dogs",
"playlist": [
{
- "uri": "http://html5demos.com/assets/dizzy.mp4"
+ "uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
- "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
}
]
},
@@ -422,7 +458,7 @@
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
},
{
- "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
@@ -435,13 +471,13 @@
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
"playlist": [
{
- "uri": "http://html5demos.com/assets/dizzy.mp4"
+ "uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
},
{
- "uri": "http://html5demos.com/assets/dizzy.mp4"
+ "uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
@@ -452,5 +488,85 @@
]
}
]
+ },
+ {
+ "name": "IMA sample ad tags",
+ "samples": [
+ {
+ "name": "Single inline linear",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
+ },
+ {
+ "name": "Single skippable inline",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator="
+ },
+ {
+ "name": "Single redirect linear",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator="
+ },
+ {
+ "name": "Single redirect error",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator="
+ },
+ {
+ "name": "Single redirect broken (fallback)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator="
+ },
+ {
+ "name": "VMAP pre-roll",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll + bumper",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP post-roll",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP post-roll + bumper",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-, mid- and post-rolls, single ads",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
+ }
+ ]
}
]
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java
index 953021fe6f..30dfb5140a 100644
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java
+++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java
@@ -20,9 +20,9 @@ import android.util.Log;
import android.view.Surface;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
@@ -55,8 +55,8 @@ import java.util.Locale;
/**
* Logs player events using {@link Log}.
*/
-/* package */ final class EventLogger implements ExoPlayer.EventListener,
- AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
+/* package */ final class EventLogger implements Player.EventListener, AudioRendererEventListener,
+ VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
MetadataRenderer.Output {
@@ -82,7 +82,7 @@ import java.util.Locale;
startTimeMs = SystemClock.elapsedRealtime();
}
- // ExoPlayer.EventListener
+ // Player.EventListener
@Override
public void onLoadingChanged(boolean isLoading) {
@@ -95,6 +95,11 @@ import java.util.Locale;
+ getStateString(state) + "]");
}
+ @Override
+ public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
+ Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]");
+ }
+
@Override
public void onPositionDiscontinuity() {
Log.d(TAG, "positionDiscontinuity");
@@ -276,7 +281,7 @@ import java.util.Locale;
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
- // Do nothing.
+ Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]");
}
@Override
@@ -407,13 +412,13 @@ import java.util.Locale;
private static String getStateString(int state) {
switch (state) {
- case ExoPlayer.STATE_BUFFERING:
+ case Player.STATE_BUFFERING:
return "B";
- case ExoPlayer.STATE_ENDED:
+ case Player.STATE_ENDED:
return "E";
- case ExoPlayer.STATE_IDLE:
+ case Player.STATE_IDLE:
return "I";
- case ExoPlayer.STATE_READY:
+ case Player.STATE_READY:
return "R";
default:
return "?";
@@ -461,4 +466,16 @@ import java.util.Locale;
return enabled ? "[X]" : "[ ]";
}
+ private static String getRepeatModeString(@Player.RepeatMode int repeatMode) {
+ switch (repeatMode) {
+ case Player.REPEAT_MODE_OFF:
+ return "OFF";
+ case Player.REPEAT_MODE_ONE:
+ return "ONE";
+ case Player.REPEAT_MODE_ALL:
+ return "ALL";
+ default:
+ return "?";
+ }
+ }
}
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
index d0703f3496..9e53dff857 100644
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
+++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.demo;
import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@@ -26,16 +27,19 @@ import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
+import android.view.ViewGroup;
import android.widget.Button;
+import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
@@ -69,6 +73,8 @@ import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
@@ -77,7 +83,7 @@ import java.util.UUID;
/**
* An activity that plays media using {@link SimpleExoPlayer}.
*/
-public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener,
+public class PlayerActivity extends Activity implements OnClickListener, EventListener,
PlaybackControlView.VisibilityListener {
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
@@ -92,6 +98,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
"com.google.android.exoplayer.demo.action.VIEW_LIST";
public static final String URI_LIST_EXTRA = "uri_list";
public static final String EXTENSION_LIST_EXTRA = "extension_list";
+ public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final CookieManager DEFAULT_COOKIE_MANAGER;
@@ -112,13 +119,19 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private DefaultTrackSelector trackSelector;
private TrackSelectionHelper trackSelectionHelper;
private DebugTextViewHelper debugViewHelper;
- private boolean needRetrySource;
+ private boolean inErrorState;
private TrackGroupArray lastSeenTrackGroupArray;
private boolean shouldAutoPlay;
private int resumeWindow;
private long resumePosition;
+ // Fields used only for ad playback. The ads loader is loaded via reflection.
+
+ private Object imaAdsLoader; // com.google.android.exoplayer2.ext.ima.ImaAdsLoader
+ private Uri loadedAdTagUri;
+ private ViewGroup adOverlayViewGroup;
+
// Activity lifecycle
@Override
@@ -185,6 +198,12 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
}
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ releaseAdsLoader();
+ }
+
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
@@ -200,10 +219,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
- // Show the controls on any key event.
- simpleExoPlayerView.showController();
- // If the event was not handled then see if the player view can handle it as a media key event.
- return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event);
+ // If the event was not handled then see if the player view can handle it.
+ return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchKeyEvent(event);
}
// OnClickListener methods
@@ -247,13 +264,19 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (drmSchemeUuid != null) {
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
- try {
- drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl,
- keyRequestPropertiesArray);
- } catch (UnsupportedDrmException e) {
- int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
- : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
- ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
+ int errorStringId = R.string.error_drm_unknown;
+ if (Util.SDK_INT < 18) {
+ errorStringId = R.string.error_drm_not_supported;
+ } else {
+ try {
+ drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl,
+ keyRequestPropertiesArray);
+ } catch (UnsupportedDrmException e) {
+ errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
+ ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
+ }
+ }
+ if (drmSessionManager == null) {
showToast(errorStringId);
return;
}
@@ -280,45 +303,58 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
}
- if (needNewPlayer || needRetrySource) {
- String action = intent.getAction();
- Uri[] uris;
- String[] extensions;
- if (ACTION_VIEW.equals(action)) {
- uris = new Uri[] {intent.getData()};
- extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
- } else if (ACTION_VIEW_LIST.equals(action)) {
- String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
- uris = new Uri[uriStrings.length];
- for (int i = 0; i < uriStrings.length; i++) {
- uris[i] = Uri.parse(uriStrings[i]);
- }
- extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
- if (extensions == null) {
- extensions = new String[uriStrings.length];
- }
- } else {
- showToast(getString(R.string.unexpected_intent_action, action));
- return;
+ String action = intent.getAction();
+ Uri[] uris;
+ String[] extensions;
+ if (ACTION_VIEW.equals(action)) {
+ uris = new Uri[]{intent.getData()};
+ extensions = new String[]{intent.getStringExtra(EXTENSION_EXTRA)};
+ } else if (ACTION_VIEW_LIST.equals(action)) {
+ String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
+ uris = new Uri[uriStrings.length];
+ for (int i = 0; i < uriStrings.length; i++) {
+ uris[i] = Uri.parse(uriStrings[i]);
}
- if (Util.maybeRequestReadExternalStoragePermission(this, uris)) {
- // The player will be reinitialized if the permission is granted.
- return;
+ extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
+ if (extensions == null) {
+ extensions = new String[uriStrings.length];
}
- MediaSource[] mediaSources = new MediaSource[uris.length];
- for (int i = 0; i < uris.length; i++) {
- mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
- }
- MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
- : new ConcatenatingMediaSource(mediaSources);
- boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
- if (haveResumePosition) {
- player.seekTo(resumeWindow, resumePosition);
- }
- player.prepare(mediaSource, !haveResumePosition, false);
- needRetrySource = false;
- updateButtonVisibilities();
+ } else {
+ showToast(getString(R.string.unexpected_intent_action, action));
+ return;
}
+ if (Util.maybeRequestReadExternalStoragePermission(this, uris)) {
+ // The player will be reinitialized if the permission is granted.
+ return;
+ }
+ MediaSource[] mediaSources = new MediaSource[uris.length];
+ for (int i = 0; i < uris.length; i++) {
+ mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
+ }
+ MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
+ : new ConcatenatingMediaSource(mediaSources);
+ String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
+ if (adTagUriString != null) {
+ Uri adTagUri = Uri.parse(adTagUriString);
+ if (!adTagUri.equals(loadedAdTagUri)) {
+ releaseAdsLoader();
+ loadedAdTagUri = adTagUri;
+ }
+ try {
+ mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
+ } catch (Exception e) {
+ showToast(R.string.ima_not_loaded);
+ }
+ } else {
+ releaseAdsLoader();
+ }
+ boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
+ if (haveResumePosition) {
+ player.seekTo(resumeWindow, resumePosition);
+ }
+ player.prepare(mediaSource, !haveResumePosition, false);
+ inErrorState = false;
+ updateButtonVisibilities();
}
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
@@ -342,11 +378,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
}
- private DrmSessionManager buildDrmSessionManager(UUID uuid,
+ private DrmSessionManager buildDrmSessionManagerV18(UUID uuid,
String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
- if (Util.SDK_INT < 18) {
- return null;
- }
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false));
if (keyRequestPropertiesArray != null) {
@@ -355,8 +388,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
keyRequestPropertiesArray[i + 1]);
}
}
- return new DefaultDrmSessionManager<>(uuid,
- FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
+ return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback,
+ null, mainHandler, eventLogger);
}
private void releasePlayer() {
@@ -375,8 +408,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private void updateResumePosition() {
resumeWindow = player.getCurrentWindowIndex();
- resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition())
- : C.TIME_UNSET;
+ resumePosition = Math.max(0, player.getContentPosition());
}
private void clearResumePosition() {
@@ -408,7 +440,48 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
.buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
- // ExoPlayer.EventListener implementation
+ /**
+ * Returns an ads media source, reusing the ads loader if one exists.
+ *
+ * @throws Exception Thrown if it was not possible to create an ads media source, for example, due
+ * to a missing dependency.
+ */
+ private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) throws Exception {
+ // Load the extension source using reflection so the demo app doesn't have to depend on it.
+ // The ads loader is reused for multiple playbacks, so that ad playback can resume.
+ Class> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
+ if (imaAdsLoader == null) {
+ imaAdsLoader = loaderClass.getConstructor(Context.class, Uri.class)
+ .newInstance(this, adTagUri);
+ adOverlayViewGroup = new FrameLayout(this);
+ // The demo app has a non-null overlay frame layout.
+ simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup);
+ }
+ Class> sourceClass =
+ Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource");
+ Constructor> constructor = sourceClass.getConstructor(MediaSource.class,
+ DataSource.Factory.class, loaderClass, ViewGroup.class);
+ return (MediaSource) constructor.newInstance(mediaSource, mediaDataSourceFactory, imaAdsLoader,
+ adOverlayViewGroup);
+ }
+
+ private void releaseAdsLoader() {
+ if (imaAdsLoader != null) {
+ try {
+ Class> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
+ Method releaseMethod = loaderClass.getMethod("release");
+ releaseMethod.invoke(imaAdsLoader);
+ } catch (Exception e) {
+ // Should never happen.
+ throw new IllegalStateException(e);
+ }
+ imaAdsLoader = null;
+ loadedAdTagUri = null;
+ simpleExoPlayerView.getOverlayFrameLayout().removeAllViews();
+ }
+ }
+
+ // Player.EventListener implementation
@Override
public void onLoadingChanged(boolean isLoading) {
@@ -417,15 +490,20 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
- if (playbackState == ExoPlayer.STATE_ENDED) {
+ if (playbackState == Player.STATE_ENDED) {
showControls();
}
updateButtonVisibilities();
}
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
@Override
public void onPositionDiscontinuity() {
- if (needRetrySource) {
+ if (inErrorState) {
// This will only occur if the user has performed a seek whilst in the error state. Update the
// resume position so that if the user then retries, playback will resume from the position to
// which they seeked.
@@ -471,7 +549,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (errorString != null) {
showToast(errorString);
}
- needRetrySource = true;
+ inErrorState = true;
if (isBehindLiveWindow(e)) {
clearResumePosition();
initializePlayer();
@@ -507,7 +585,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private void updateButtonVisibilities() {
debugRootView.removeAllViews();
- retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE);
+ retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE);
debugRootView.addView(retryButton);
if (player == null) {
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
index 081ad190b5..87b8e92e83 100644
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
+++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
@@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity {
String[] drmKeyRequestProperties = null;
boolean preferExtensionDecoders = false;
ArrayList playlistSamples = null;
+ String adTagUri = null;
reader.beginObject();
while (reader.hasNext()) {
@@ -233,6 +234,9 @@ public class SampleChooserActivity extends Activity {
}
reader.endArray();
break;
+ case "ad_tag_uri":
+ adTagUri = reader.nextString();
+ break;
default:
throw new ParserException("Unsupported attribute name: " + name);
}
@@ -246,7 +250,7 @@ public class SampleChooserActivity extends Activity {
preferExtensionDecoders, playlistSamplesArray);
} else {
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
- preferExtensionDecoders, uri, extension);
+ preferExtensionDecoders, uri, extension, adTagUri);
}
}
@@ -402,13 +406,15 @@ public class SampleChooserActivity extends Activity {
public final String uri;
public final String extension;
+ public final String adTagUri;
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
- String extension) {
+ String extension, String adTagUri) {
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
this.uri = uri;
this.extension = extension;
+ this.adTagUri = adTagUri;
}
@Override
@@ -416,6 +422,7 @@ public class SampleChooserActivity extends Activity {
return super.buildIntent(context)
.setData(Uri.parse(uri))
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
+ .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
.setAction(PlayerActivity.ACTION_VIEW);
}
diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml
index 4eb2b89324..cc6357c574 100644
--- a/demo/src/main/res/values/strings.xml
+++ b/demo/src/main/res/values/strings.xml
@@ -56,4 +56,6 @@
One or more sample lists failed to load
+ Playing sample without ads, as the IMA extension was not loaded
+
diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md
index a570385a52..2287c4c19b 100644
--- a/extensions/cronet/README.md
+++ b/extensions/cronet/README.md
@@ -1,23 +1,18 @@
-# ExoPlayer Cronet Extension #
+# ExoPlayer Cronet extension #
## Description ##
-[Cronet][] is Chromium's Networking stack packaged as a library.
-
-The Cronet Extension is an [HttpDataSource][] implementation using [Cronet][].
+The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
-## Build Instructions ##
+## Build instructions ##
-* Checkout ExoPlayer along with Extensions:
-
-```
-git clone https://github.com/google/ExoPlayer.git
-```
-
-* Get the Cronet libraries:
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][]. In addition, it's necessary to get the Cronet libraries
+and enable the extension:
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
directory
@@ -27,6 +22,37 @@ git clone https://github.com/google/ExoPlayer.git
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
directory of this extension
-* In ExoPlayer's `settings.gradle` file, uncomment the Cronet extension
+* In your `settings.gradle` file, add the following line before the line that
+ applies `core_settings.gradle`:
+```gradle
+gradle.ext.exoplayerIncludeCronetExtension = true;
+```
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
+
+## Using the extension ##
+
+ExoPlayer requests data through `DataSource` instances. These instances are
+either instantiated and injected from application code, or obtained from
+instances of `DataSource.Factory` that are instantiated and injected from
+application code.
+
+If your application only needs to play http(s) content, using the Cronet
+extension is as simple as updating any `DataSource`s and `DataSource.Factory`
+instantiations in your application code to use `CronetDataSource` and
+`CronetDataSourceFactory` respectively. If your application also needs to play
+non-http(s) content such as local files, use
+```
+new DefaultDataSource(
+ ...
+ new CronetDataSource(...) /* baseDataSource argument */);
+```
+and
+```
+new DefaultDataSourceFactory(
+ ...
+ new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */);
+```
+respectively.
diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle
index 5611817b2e..930a53c7c5 100644
--- a/extensions/cronet/build.gradle
+++ b/extensions/cronet/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -29,11 +30,11 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
compile files('libs/cronet_api.jar')
compile files('libs/cronet_impl_common_java.jar')
compile files('libs/cronet_impl_native_java.jar')
- androidTestCompile project(':library')
+ androidTestCompile project(modulePrefix + 'library')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/androidTest/AndroidManifest.xml
index 2f45a1a2e5..1f371a1864 100644
--- a/extensions/cronet/src/androidTest/AndroidManifest.xml
+++ b/extensions/cronet/src/androidTest/AndroidManifest.xml
@@ -28,7 +28,6 @@
+ android:targetPackage="com.google.android.exoplayer.ext.cronet"/>
diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
index 246e23e172..06a356487e 100644
--- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
+++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
@@ -124,6 +124,7 @@ public final class CronetDataSourceTest {
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
.thenReturn(mockUrlRequestBuilder);
+ when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder);
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
mockStatusResponse();
@@ -683,6 +684,15 @@ public final class CronetDataSourceTest {
}
}
+ @Test
+ public void testAllowDirectExecutor() throws HttpDataSourceException {
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
+ mockResponseStartSuccess();
+
+ dataSourceUnderTest.open(testDataSpec);
+ verify(mockUrlRequestBuilder).allowDirectExecutor();
+ }
+
// Helper methods.
private void mockStatusResponse() {
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
index 4f15a6eabc..204a2756bb 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
@@ -20,6 +20,7 @@ import android.os.ConditionVariable;
import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@@ -27,7 +28,6 @@ import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Predicate;
-import com.google.android.exoplayer2.util.SystemClock;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
@@ -74,6 +74,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
}
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.cronet");
+ }
+
/**
* The default connection timeout, in milliseconds.
*/
@@ -127,7 +131,11 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
/**
* @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses.
+ * This may be a direct executor (i.e. executes tasks on the calling thread) in order
+ * to avoid a thread hop from Cronet's internal network thread to the response handling
+ * thread. However, to avoid slowing down overall network performance, care must be taken
+ * to make sure response handling is a fast operation when using a direct executor.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}.
@@ -141,7 +149,11 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
/**
* @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses.
+ * This may be a direct executor (i.e. executes tasks on the calling thread) in order
+ * to avoid a thread hop from Cronet's internal network thread to the response handling
+ * thread. However, to avoid slowing down overall network performance, care must be taken
+ * to make sure response handling is a fast operation when using a direct executor.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}.
@@ -156,7 +168,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
RequestProperties defaultRequestProperties) {
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
- readTimeoutMs, resetTimeoutOnRedirects, new SystemClock(), defaultRequestProperties);
+ readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties);
}
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
@@ -416,8 +428,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
// Internal methods.
private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException {
- UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(),
- this, executor);
+ UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(
+ dataSpec.uri.toString(), this, executor).allowDirectExecutor();
// Set the headers.
boolean isContentTypeHeaderSet = false;
if (defaultRequestProperties != null) {
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
index 2ad6da6a54..d6237fc988 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
@@ -16,9 +16,11 @@
package com.google.android.exoplayer2.ext.cronet;
import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Predicate;
import java.util.concurrent.Executor;
@@ -34,43 +36,143 @@ public final class CronetDataSourceFactory extends BaseFactory {
*/
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS =
CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
+
/**
* The default read timeout, in milliseconds.
*/
public static final int DEFAULT_READ_TIMEOUT_MILLIS =
CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
- private final CronetEngine cronetEngine;
+ private final CronetEngineWrapper cronetEngineWrapper;
private final Executor executor;
private final Predicate contentTypePredicate;
private final TransferListener super DataSource> transferListener;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
+ private final HttpDataSource.Factory fallbackFactory;
- public CronetDataSourceFactory(CronetEngine cronetEngine,
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
+ * fallback {@link HttpDataSource.Factory} will be used instead.
+ *
+ * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
+ * no suitable CronetEngine can be build.
+ */
+ public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
Executor executor, Predicate contentTypePredicate,
- TransferListener super DataSource> transferListener) {
- this(cronetEngine, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false);
+ TransferListener super DataSource> transferListener,
+ HttpDataSource.Factory fallbackFactory) {
+ this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
}
- public CronetDataSourceFactory(CronetEngine cronetEngine,
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
+ * {@link DefaultHttpDataSourceFactory} will be used instead.
+ *
+ * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
+ */
+ public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
+ Executor executor, Predicate contentTypePredicate,
+ TransferListener super DataSource> transferListener, String userAgent) {
+ this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
+ new DefaultHttpDataSourceFactory(userAgent, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
+ }
+
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
+ * {@link DefaultHttpDataSourceFactory} will be used instead.
+ *
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
+ */
+ public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
Executor executor, Predicate contentTypePredicate,
TransferListener super DataSource> transferListener, int connectTimeoutMs,
- int readTimeoutMs, boolean resetTimeoutOnRedirects) {
- this.cronetEngine = cronetEngine;
+ int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) {
+ this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
+ new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
+ readTimeoutMs, resetTimeoutOnRedirects));
+ }
+
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
+ * fallback {@link HttpDataSource.Factory} will be used instead.
+ *
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
+ * no suitable CronetEngine can be build.
+ */
+ public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
+ Executor executor, Predicate contentTypePredicate,
+ TransferListener super DataSource> transferListener, int connectTimeoutMs,
+ int readTimeoutMs, boolean resetTimeoutOnRedirects,
+ HttpDataSource.Factory fallbackFactory) {
+ this.cronetEngineWrapper = cronetEngineWrapper;
this.executor = executor;
this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
+ this.fallbackFactory = fallbackFactory;
}
@Override
- protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties
+ protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties
defaultRequestProperties) {
+ CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();
+ if (cronetEngine == null) {
+ return fallbackFactory.createDataSource();
+ }
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
}
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
new file mode 100644
index 0000000000..efe30d6525
--- /dev/null
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.cronet;
+
+import android.content.Context;
+import android.support.annotation.IntDef;
+import android.util.Log;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import org.chromium.net.CronetEngine;
+import org.chromium.net.CronetProvider;
+
+/**
+ * A wrapper class for a {@link CronetEngine}.
+ */
+public final class CronetEngineWrapper {
+
+ private static final String TAG = "CronetEngineWrapper";
+
+ private final CronetEngine cronetEngine;
+ private final @CronetEngineSource int cronetEngineSource;
+
+ /**
+ * Source of {@link CronetEngine}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})
+ public @interface CronetEngineSource {}
+ /**
+ * Natively bundled Cronet implementation.
+ */
+ public static final int SOURCE_NATIVE = 0;
+ /**
+ * Cronet implementation from GMSCore.
+ */
+ public static final int SOURCE_GMS = 1;
+ /**
+ * Other (unknown) Cronet implementation.
+ */
+ public static final int SOURCE_UNKNOWN = 2;
+ /**
+ * User-provided Cronet engine.
+ */
+ public static final int SOURCE_USER_PROVIDED = 3;
+ /**
+ * No Cronet implementation available. Fallback Http provider is used if possible.
+ */
+ public static final int SOURCE_UNAVAILABLE = 4;
+
+ /**
+ * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
+ * {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet
+ * if both are available.
+ *
+ * @param context A context.
+ */
+ public CronetEngineWrapper(Context context) {
+ this(context, false);
+ }
+
+ /**
+ * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
+ * {@link CronetProvider} based on user preference.
+ *
+ * @param context A context.
+ * @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively
+ * bundled Cronet if both are available.
+ */
+ public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
+ CronetEngine cronetEngine = null;
+ @CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
+ List cronetProviders = CronetProvider.getAllProviders(context);
+ // Remove disabled and fallback Cronet providers from list
+ for (int i = cronetProviders.size() - 1; i >= 0; i--) {
+ if (!cronetProviders.get(i).isEnabled()
+ || CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) {
+ cronetProviders.remove(i);
+ }
+ }
+ // Sort remaining providers by type and version.
+ CronetProviderComparator providerComparator = new CronetProviderComparator(preferGMSCoreCronet);
+ Collections.sort(cronetProviders, providerComparator);
+ for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) {
+ String providerName = cronetProviders.get(i).getName();
+ try {
+ cronetEngine = cronetProviders.get(i).createBuilder().build();
+ if (providerComparator.isNativeProvider(providerName)) {
+ cronetEngineSource = SOURCE_NATIVE;
+ } else if (providerComparator.isGMSCoreProvider(providerName)) {
+ cronetEngineSource = SOURCE_GMS;
+ } else {
+ cronetEngineSource = SOURCE_UNKNOWN;
+ }
+ Log.d(TAG, "CronetEngine built using " + providerName);
+ } catch (SecurityException e) {
+ Log.w(TAG, "Failed to build CronetEngine. Please check if current process has "
+ + "android.permission.ACCESS_NETWORK_STATE.");
+ } catch (UnsatisfiedLinkError e) {
+ Log.w(TAG, "Failed to link Cronet binaries. Please check if native Cronet binaries are "
+ + "bundled into your app.");
+ }
+ }
+ if (cronetEngine == null) {
+ Log.w(TAG, "Cronet not available. Using fallback provider.");
+ }
+ this.cronetEngine = cronetEngine;
+ this.cronetEngineSource = cronetEngineSource;
+ }
+
+ /**
+ * Creates a wrapper for an existing CronetEngine.
+ *
+ * @param cronetEngine An existing CronetEngine.
+ */
+ public CronetEngineWrapper(CronetEngine cronetEngine) {
+ this.cronetEngine = cronetEngine;
+ this.cronetEngineSource = SOURCE_USER_PROVIDED;
+ }
+
+ /**
+ * Returns the source of the wrapped {@link CronetEngine}.
+ *
+ * @return A {@link CronetEngineSource} value.
+ */
+ public @CronetEngineSource int getCronetEngineSource() {
+ return cronetEngineSource;
+ }
+
+ /**
+ * Returns the wrapped {@link CronetEngine}.
+ *
+ * @return The CronetEngine, or null if no CronetEngine is available.
+ */
+ /* package */ CronetEngine getCronetEngine() {
+ return cronetEngine;
+ }
+
+ private static class CronetProviderComparator implements Comparator {
+
+ private final String gmsCoreCronetName;
+ private final boolean preferGMSCoreCronet;
+
+ public CronetProviderComparator(boolean preferGMSCoreCronet) {
+ // GMSCore CronetProvider classes are only available in some configurations.
+ // Thus, we use reflection to copy static name.
+ String gmsCoreVersionString = null;
+ try {
+ Class> cronetProviderInstallerClass =
+ Class.forName("com.google.android.gms.net.CronetProviderInstaller");
+ Field providerNameField = cronetProviderInstallerClass.getDeclaredField("PROVIDER_NAME");
+ gmsCoreVersionString = (String) providerNameField.get(null);
+ } catch (ClassNotFoundException e) {
+ // GMSCore CronetProvider not available.
+ } catch (NoSuchFieldException e) {
+ // GMSCore CronetProvider not available.
+ } catch (IllegalAccessException e) {
+ // GMSCore CronetProvider not available.
+ }
+ gmsCoreCronetName = gmsCoreVersionString;
+ this.preferGMSCoreCronet = preferGMSCoreCronet;
+ }
+
+ @Override
+ public int compare(CronetProvider providerLeft, CronetProvider providerRight) {
+ int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName());
+ int typePreferenceRight = evaluateCronetProviderType(providerRight.getName());
+ if (typePreferenceLeft != typePreferenceRight) {
+ return typePreferenceLeft - typePreferenceRight;
+ }
+ return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion());
+ }
+
+ public boolean isNativeProvider(String providerName) {
+ return CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName);
+ }
+
+ public boolean isGMSCoreProvider(String providerName) {
+ return gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName);
+ }
+
+ /**
+ * Convert Cronet provider name into a sortable preference value.
+ * Smaller values are preferred.
+ */
+ private int evaluateCronetProviderType(String providerName) {
+ if (isNativeProvider(providerName)) {
+ return 1;
+ }
+ if (isGMSCoreProvider(providerName)) {
+ return preferGMSCoreCronet ? 0 : 2;
+ }
+ // Unknown provider type.
+ return -1;
+ }
+
+ /**
+ * Compares version strings of format "12.123.35.23".
+ */
+ private static int compareVersionStrings(String versionLeft, String versionRight) {
+ if (versionLeft == null || versionRight == null) {
+ return 0;
+ }
+ String[] versionStringsLeft = versionLeft.split("\\.");
+ String[] versionStringsRight = versionRight.split("\\.");
+ int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);
+ for (int i = 0; i < minLength; i++) {
+ if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
+ try {
+ int versionIntLeft = Integer.parseInt(versionStringsLeft[i]);
+ int versionIntRight = Integer.parseInt(versionStringsRight[i]);
+ return versionIntLeft - versionIntRight;
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+ }
+ return 0;
+ }
+ }
+
+}
diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md
index 4ce9173ec9..b4514effbc 100644
--- a/extensions/ffmpeg/README.md
+++ b/extensions/ffmpeg/README.md
@@ -1,4 +1,4 @@
-# FfmpegAudioRenderer #
+# ExoPlayer FFmpeg extension #
## Description ##
@@ -9,11 +9,10 @@ audio.
## Build instructions ##
-* Checkout ExoPlayer along with Extensions
-
-```
-git clone https://github.com/google/ExoPlayer.git
-```
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][]. In addition, it's necessary to build the extension's
+native components as follows:
* Set the following environment variables:
@@ -25,8 +24,6 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
* Download the [Android NDK][] and set its location in an environment variable:
-[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-
```
NDK_PATH=""
```
@@ -106,20 +103,5 @@ cd "${FFMPEG_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
```
-* In your project, you can add a dependency on the extension by using a rule
- like this:
-
-```
-// in settings.gradle
-include ':..:ExoPlayer:library'
-include ':..:ExoPlayer:extension-ffmpeg'
-
-// in build.gradle
-dependencies {
- compile project(':..:ExoPlayer:library')
- compile project(':..:ExoPlayer:extension-ffmpeg')
-}
-```
-
-* Now, when you build your app, the extension will be built and the native
- libraries will be packaged along with the APK.
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle
index 0eddd017a4..9820818f3e 100644
--- a/extensions/ffmpeg/build.gradle
+++ b/extensions/ffmpeg/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -30,7 +31,7 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
}
ext {
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
index 8d75ca3dbb..453a18476e 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
@@ -30,7 +30,14 @@ import com.google.android.exoplayer2.util.MimeTypes;
*/
public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
+ /**
+ * The number of input and output buffers.
+ */
private static final int NUM_BUFFERS = 16;
+ /**
+ * The initial input buffer size. Input buffers are reallocated dynamically if this value is
+ * insufficient.
+ */
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
private FfmpegDecoder decoder;
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
index 4992bcbb3e..9b3bbbb6ab 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -23,6 +24,10 @@ import com.google.android.exoplayer2.util.MimeTypes;
*/
public final class FfmpegLibrary {
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
+ }
+
private static final LibraryLoader LOADER =
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
@@ -32,6 +37,8 @@ public final class FfmpegLibrary {
* Override the names of the FFmpeg native libraries. If an application wishes to call this
* method, it must do so before calling any other method defined by this class, and before
* instantiating a {@link FfmpegAudioRenderer} instance.
+ *
+ * @param libraries The names of the FFmpeg native libraries.
*/
public static void setLibraries(String... libraries) {
LOADER.setLibraries(libraries);
@@ -53,6 +60,8 @@ public final class FfmpegLibrary {
/**
* Returns whether the underlying library supports the specified MIME type.
+ *
+ * @param mimeType The MIME type to check.
*/
public static boolean supportsFormat(String mimeType) {
if (!isAvailable()) {
diff --git a/extensions/flac/README.md b/extensions/flac/README.md
index 2f3b067d6f..9db2e5727d 100644
--- a/extensions/flac/README.md
+++ b/extensions/flac/README.md
@@ -1,20 +1,19 @@
-# ExoPlayer Flac Extension #
+# ExoPlayer Flac extension #
## Description ##
-The Flac Extension is a [Renderer][] implementation that helps you bundle
+The Flac extension is a [Renderer][] implementation that helps you bundle
libFLAC (the Flac decoding library) into your app and use it along with
ExoPlayer to play Flac audio on Android devices.
[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html
-## Build Instructions ##
+## Build instructions ##
-* Checkout ExoPlayer along with Extensions:
-
-```
-git clone https://github.com/google/ExoPlayer.git
-```
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][]. In addition, it's necessary to build the extension's
+native components as follows:
* Set the following environment variables:
@@ -26,8 +25,6 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
* Download the [Android NDK][] and set its location in an environment variable:
-[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-
```
NDK_PATH=""
```
@@ -47,20 +44,5 @@ cd "${FLAC_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4
```
-* In your project, you can add a dependency to the Flac Extension by using a
- rule like this:
-
-```
-// in settings.gradle
-include ':..:ExoPlayer:library'
-include ':..:ExoPlayer:extension-flac'
-
-// in build.gradle
-dependencies {
- compile project(':..:ExoPlayer:library')
- compile project(':..:ExoPlayer:extension-flac')
-}
-```
-
-* Now, when you build your app, the Flac extension will be built and the native
- libraries will be packaged along with the APK.
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle
index 4a6b8e0e5a..4d840d34ac 100644
--- a/extensions/flac/build.gradle
+++ b/extensions/flac/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -30,8 +31,8 @@ android {
}
dependencies {
- compile project(':library-core')
- androidTestCompile project(':testutils')
+ compile project(modulePrefix + 'library-core')
+ androidTestCompile project(modulePrefix + 'testutils')
}
ext {
diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml
index 0a62db3bb5..73032ab50c 100644
--- a/extensions/flac/src/androidTest/AndroidManifest.xml
+++ b/extensions/flac/src/androidTest/AndroidManifest.xml
@@ -28,7 +28,6 @@
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
index 4196f1ea63..7b193997c3 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.flac;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link FlacExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public class FlacExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new FlacExtractor();
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
index 21f01f0cca..1fa30bed9d 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
@@ -57,7 +58,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
}
}
- private static class TestPlaybackThread extends Thread implements ExoPlayer.EventListener {
+ private static class TestPlaybackThread extends Thread implements Player.EventListener {
private final Context context;
private final Uri uri;
@@ -120,12 +121,17 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
- if (playbackState == ExoPlayer.STATE_ENDED
- || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) {
+ if (playbackState == Player.STATE_ENDED
+ || (playbackState == Player.STATE_IDLE && playbackException != null)) {
releasePlayerAndQuitLooper();
}
}
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
private void releasePlayerAndQuitLooper() {
player.release();
Looper.myLooper().quit();
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
index d13194793e..7b71b5c743 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
@@ -159,13 +159,17 @@ public final class FlacExtractor implements Extractor {
if (position == 0) {
metadataParsed = false;
}
- decoderJni.reset(position);
+ if (decoderJni != null) {
+ decoderJni.reset(position);
+ }
}
@Override
public void release() {
- decoderJni.release();
- decoderJni = null;
+ if (decoderJni != null) {
+ decoderJni.release();
+ decoderJni = null;
+ }
}
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java
index ca18051207..d8b9b808a6 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
/**
@@ -22,6 +23,10 @@ import com.google.android.exoplayer2.util.LibraryLoader;
*/
public final class FlacLibrary {
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.flac");
+ }
+
private static final LibraryLoader LOADER = new LibraryLoader("flacJNI");
private FlacLibrary() {}
@@ -30,6 +35,8 @@ public final class FlacLibrary {
* Override the names of the Flac native libraries. If an application wishes to call this method,
* it must do so before calling any other method defined by this class, and before instantiating
* any {@link LibflacAudioRenderer} and {@link FlacExtractor} instances.
+ *
+ * @param libraries The names of the Flac native libraries.
*/
public static void setLibraries(String... libraries) {
LOADER.setLibraries(libraries);
diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md
index bae5de4812..7e072d070c 100644
--- a/extensions/gvr/README.md
+++ b/extensions/gvr/README.md
@@ -1,4 +1,4 @@
-# ExoPlayer GVR Extension #
+# ExoPlayer GVR extension #
## Description ##
@@ -6,7 +6,10 @@ The GVR extension wraps the [Google VR SDK for Android][]. It provides a
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
of surround sound and ambisonic soundfields.
-## Using the extension ##
+[Google VR SDK for Android]: https://developers.google.com/vr/android/
+[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
+
+## Getting the extension ##
The easiest way to use the extension is to add it as a gradle dependency. You
need to make sure you have the jcenter repository included in the `build.gradle`
@@ -27,12 +30,15 @@ compile 'com.google.android.exoplayer:extension-gvr:rX.X.X'
where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used.
-## Using GvrAudioProcessor ##
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+## Using the extension ##
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
return a GvrAudioProcessor.
* If constructing renderers directly, pass a GvrAudioProcessor to
MediaCodecAudioRenderer's constructor.
-[Google VR SDK for Android]: https://developers.google.com/vr/android/
-[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index e15c8b1ad8..66665576bb 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -24,7 +25,7 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
compile 'com.google.vr:sdk-audio:1.60.1'
}
diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
index a56bc7f0a9..5750f5f04d 100644
--- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
+++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.gvr;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.vr.sdk.audio.GvrAudioSurround;
@@ -28,6 +29,10 @@ import java.nio.ByteOrder;
*/
public final class GvrAudioProcessor implements AudioProcessor {
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.gvr");
+ }
+
private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;
private static final int OUTPUT_CHANNEL_COUNT = 2;
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
@@ -56,6 +61,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
/**
* Updates the listener head orientation. May be called on any thread. See
* {@code GvrAudioSurround.updateNativeOrientation}.
+ *
+ * @param w The w component of the quaternion.
+ * @param x The x component of the quaternion.
+ * @param y The y component of the quaternion.
+ * @param z The z component of the quaternion.
*/
public synchronized void updateOrientation(float w, float x, float y, float z) {
this.w = w;
diff --git a/extensions/ima/README.md b/extensions/ima/README.md
new file mode 100644
index 0000000000..f328bb44cb
--- /dev/null
+++ b/extensions/ima/README.md
@@ -0,0 +1,57 @@
+# ExoPlayer IMA extension #
+
+## Description ##
+
+The IMA extension is a [MediaSource][] implementation wrapping the
+[Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads
+alongside content.
+
+[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/
+[MediaSource]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/MediaSource.html
+
+## Getting the extension ##
+
+The easiest way to use the extension is to add it as a gradle dependency:
+
+```gradle
+compile 'com.google.android.exoplayer:extension-ima:rX.X.X'
+```
+
+where `rX.X.X` is the version, which must match the version of the ExoPlayer
+library being used.
+
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+
+## Using the extension ##
+
+To play ads alongside a single-window content `MediaSource`, prepare the player
+with an `ImaAdsMediaSource` constructed using an `ImaAdsLoader`, the content
+`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag
+URI from your ad campaign when creating the `ImaAdsLoader`. The IMA
+documentation includes some [sample ad tags][] for testing.
+
+Resuming the player after entering the background requires some special handling
+when playing ads. The player and its media source are released on entering the
+background, and are recreated when the player returns to the foreground. When
+playing ads it is necessary to persist ad playback state while in the background
+by keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of
+the same content/ads by passing it in when constructing the new
+`ImaAdsMediaSource`. It is also important to persist the player position when
+entering the background by storing the value of `player.getContentPosition()`.
+On returning to the foreground, seek to that position before preparing the new
+player instance. Finally, it is important to call `ImaAdsLoader.release()` when
+playback of the content/ads has finished and will not be resumed.
+
+You can try the IMA extension in the ExoPlayer demo app. To do this you must
+select and build one of the `withExtensions` build variants of the demo app in
+Android Studio. You can find IMA test content in the "IMA sample ad tags"
+section of the app. The demo app's `PlayerActivity` also shows how to persist
+the `ImaAdsLoader` instance and the player position when backgrounded during ad
+playback.
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags
diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle
new file mode 100644
index 0000000000..a4ead9e01f
--- /dev/null
+++ b/extensions/ima/build.gradle
@@ -0,0 +1,42 @@
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+ buildToolsVersion project.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion project.ext.targetSdkVersion
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+}
+
+dependencies {
+ compile project(modulePrefix + 'library-core')
+ // This dependency is necessary to force the supportLibraryVersion of
+ // com.android.support:support-v4 to be used. Else an older version (25.2.0) is included via:
+ // com.google.android.gms:play-services-ads:11.0.2
+ // |-- com.google.android.gms:play-services-ads-lite:[11.0.2] -> 11.0.2
+ // |-- com.google.android.gms:play-services-basement:[11.0.2] -> 11.0.2
+ // |-- com.android.support:support-v4:25.2.0
+ compile 'com.android.support:support-v4:' + supportLibraryVersion
+ compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
+ compile 'com.google.android.gms:play-services-ads:11.0.2'
+ androidTestCompile project(modulePrefix + 'library')
+ androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
+ androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
+ androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
+ androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion
+}
+
+ext {
+ javadocTitle = 'IMA extension'
+}
+apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'extension-ima'
+ releaseDescription = 'Interactive Media Ads extension for ExoPlayer.'
+}
+apply from: '../../publish.gradle'
diff --git a/extensions/ima/src/main/AndroidManifest.xml b/extensions/ima/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..22fb518c58
--- /dev/null
+++ b/extensions/ima/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java
new file mode 100644
index 0000000000..0edd7d6558
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.util.Arrays;
+
+/**
+ * Represents the structure of ads to play and the state of loaded/played ads.
+ */
+/* package */ final class AdPlaybackState {
+
+ /**
+ * The number of ad groups.
+ */
+ public final int adGroupCount;
+ /**
+ * The times of ad groups, in microseconds. A final element with the value
+ * {@link C#TIME_END_OF_SOURCE} indicates a postroll ad.
+ */
+ public final long[] adGroupTimesUs;
+ /**
+ * The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET} if the number of
+ * ads is not yet known.
+ */
+ public final int[] adCounts;
+ /**
+ * The number of ads loaded so far in each ad group.
+ */
+ public final int[] adsLoadedCounts;
+ /**
+ * The number of ads played so far in each ad group.
+ */
+ public final int[] adsPlayedCounts;
+ /**
+ * The URI of each ad in each ad group.
+ */
+ public final Uri[][] adUris;
+
+ /**
+ * The position offset in the first unplayed ad at which to begin playback, in microseconds.
+ */
+ public long adResumePositionUs;
+
+ /**
+ * Creates a new ad playback state with the specified ad group times.
+ *
+ * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value
+ * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad.
+ */
+ public AdPlaybackState(long[] adGroupTimesUs) {
+ this.adGroupTimesUs = adGroupTimesUs;
+ adGroupCount = adGroupTimesUs.length;
+ adsPlayedCounts = new int[adGroupCount];
+ adCounts = new int[adGroupCount];
+ Arrays.fill(adCounts, C.LENGTH_UNSET);
+ adUris = new Uri[adGroupCount][];
+ Arrays.fill(adUris, new Uri[0]);
+ adsLoadedCounts = new int[adGroupTimesUs.length];
+ }
+
+ private AdPlaybackState(long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts,
+ int[] adsPlayedCounts, Uri[][] adUris, long adResumePositionUs) {
+ this.adGroupTimesUs = adGroupTimesUs;
+ this.adCounts = adCounts;
+ this.adsLoadedCounts = adsLoadedCounts;
+ this.adsPlayedCounts = adsPlayedCounts;
+ this.adUris = adUris;
+ this.adResumePositionUs = adResumePositionUs;
+ adGroupCount = adGroupTimesUs.length;
+ }
+
+ /**
+ * Returns a deep copy of this instance.
+ */
+ public AdPlaybackState copy() {
+ Uri[][] adUris = new Uri[adGroupTimesUs.length][];
+ for (int i = 0; i < this.adUris.length; i++) {
+ adUris[i] = Arrays.copyOf(this.adUris[i], this.adUris[i].length);
+ }
+ return new AdPlaybackState(Arrays.copyOf(adGroupTimesUs, adGroupCount),
+ Arrays.copyOf(adCounts, adGroupCount), Arrays.copyOf(adsLoadedCounts, adGroupCount),
+ Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, adResumePositionUs);
+ }
+
+ /**
+ * Sets the number of ads in the specified ad group.
+ */
+ public void setAdCount(int adGroupIndex, int adCount) {
+ adCounts[adGroupIndex] = adCount;
+ }
+
+ /**
+ * Adds an ad to the specified ad group.
+ */
+ public void addAdUri(int adGroupIndex, Uri uri) {
+ int adIndexInAdGroup = adUris[adGroupIndex].length;
+ adUris[adGroupIndex] = Arrays.copyOf(adUris[adGroupIndex], adIndexInAdGroup + 1);
+ adUris[adGroupIndex][adIndexInAdGroup] = uri;
+ adsLoadedCounts[adGroupIndex]++;
+ }
+
+ /**
+ * Marks the last ad in the specified ad group as played.
+ */
+ public void playedAd(int adGroupIndex) {
+ adResumePositionUs = 0;
+ adsPlayedCounts[adGroupIndex]++;
+ }
+
+ /**
+ * Marks all ads in the specified ad group as played.
+ */
+ public void playedAdGroup(int adGroupIndex) {
+ adResumePositionUs = 0;
+ if (adCounts[adGroupIndex] == C.LENGTH_UNSET) {
+ adCounts[adGroupIndex] = 0;
+ }
+ adsPlayedCounts[adGroupIndex] = adCounts[adGroupIndex];
+ }
+
+ /**
+ * Sets the position offset in the first unplayed ad at which to begin playback, in microseconds.
+ */
+ public void setAdResumePositionUs(long adResumePositionUs) {
+ this.adResumePositionUs = adResumePositionUs;
+ }
+
+}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
new file mode 100644
index 0000000000..8c4fb4c51c
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.ViewGroup;
+import com.google.ads.interactivemedia.v3.api.Ad;
+import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
+import com.google.ads.interactivemedia.v3.api.AdEvent;
+import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;
+import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;
+import com.google.ads.interactivemedia.v3.api.AdPodInfo;
+import com.google.ads.interactivemedia.v3.api.AdsLoader;
+import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener;
+import com.google.ads.interactivemedia.v3.api.AdsManager;
+import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
+import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
+import com.google.ads.interactivemedia.v3.api.AdsRequest;
+import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
+import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
+import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
+import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
+import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Loads ads using the IMA SDK. All methods are called on the main thread.
+ */
+public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
+ ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener {
+
+ /**
+ * Listener for ad loader events. All methods are called on the main thread.
+ */
+ /* package */ interface EventListener {
+
+ /**
+ * Called when the ad playback state has been updated.
+ *
+ * @param adPlaybackState The new ad playback state.
+ */
+ void onAdPlaybackState(AdPlaybackState adPlaybackState);
+
+ /**
+ * Called when there was an error loading ads.
+ *
+ * @param error The error.
+ */
+ void onLoadError(IOException error);
+
+ /**
+ * Called when the user clicks through an ad (for example, following a 'learn more' link).
+ */
+ void onAdClicked();
+
+ /**
+ * Called when the user taps a non-clickthrough part of an ad.
+ */
+ void onAdTapped();
+
+ }
+
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
+ }
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ImaAdsLoader";
+
+ /**
+ * Whether to enable preloading of ads in {@link AdsRenderingSettings}.
+ */
+ private static final boolean ENABLE_PRELOADING = true;
+
+ private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
+ private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;
+
+ /**
+ * Threshold before the end of content at which IMA is notified that content is complete if the
+ * player buffers, in milliseconds.
+ */
+ private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000;
+
+ private final Uri adTagUri;
+ private final Timeline.Period period;
+ private final List adCallbacks;
+ private final ImaSdkFactory imaSdkFactory;
+ private final AdDisplayContainer adDisplayContainer;
+ private final AdsLoader adsLoader;
+
+ private EventListener eventListener;
+ private Player player;
+ private VideoProgressUpdate lastContentProgress;
+ private VideoProgressUpdate lastAdProgress;
+
+ private AdsManager adsManager;
+ private Timeline timeline;
+ private long contentDurationMs;
+ private AdPlaybackState adPlaybackState;
+
+ // Fields tracking IMA's state.
+
+ /**
+ * The index of the current ad group that IMA is loading.
+ */
+ private int adGroupIndex;
+ /**
+ * Whether IMA has sent an ad event to pause content since the last resume content event.
+ */
+ private boolean imaPausedContent;
+ /**
+ * If {@link #playingAd} is set, stores whether IMA has called {@link #playAd()} and not
+ * {@link #stopAd()}.
+ */
+ private boolean imaPlayingAd;
+ /**
+ * If {@link #playingAd} is set, stores whether IMA has called {@link #pauseAd()} since a
+ * preceding call to {@link #playAd()} for the current ad.
+ */
+ private boolean imaPausedInAd;
+ /**
+ * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback.
+ */
+ private boolean sentContentComplete;
+
+ // Fields tracking the player/loader state.
+
+ /**
+ * Whether the player's play when ready flag has temporarily been set to true for playing ads.
+ */
+ private boolean playWhenReadyOverriddenForAds;
+ /**
+ * Whether the player is playing an ad.
+ */
+ private boolean playingAd;
+ /**
+ * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}
+ * otherwise.
+ */
+ private int playingAdIndexInAdGroup;
+ /**
+ * If a content period has finished but IMA has not yet sent an ad event with
+ * {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of
+ * {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to
+ * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
+ */
+ private long fakeContentProgressElapsedRealtimeMs;
+ /**
+ * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the
+ * content progress should increase. {@link C#TIME_UNSET} otherwise.
+ */
+ private long fakeContentProgressOffsetMs;
+ /**
+ * Stores the pending content position when a seek operation was intercepted to play an ad.
+ */
+ private long pendingContentPositionMs;
+ /**
+ * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
+ */
+ private boolean sentPendingContentPositionMs;
+ /**
+ * Whether {@link #release()} has been called.
+ */
+ private boolean released;
+
+ /**
+ * Creates a new IMA ads loader.
+ *
+ * @param context The context.
+ * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
+ * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
+ * more information.
+ */
+ public ImaAdsLoader(Context context, Uri adTagUri) {
+ this(context, adTagUri, null);
+ }
+
+ /**
+ * Creates a new IMA ads loader.
+ *
+ * @param context The context.
+ * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
+ * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
+ * more information.
+ * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to
+ * use the default settings. If set, the player type and version fields may be overwritten.
+ */
+ public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
+ this.adTagUri = adTagUri;
+ period = new Timeline.Period();
+ adCallbacks = new ArrayList<>(1);
+ imaSdkFactory = ImaSdkFactory.getInstance();
+ adDisplayContainer = imaSdkFactory.createAdDisplayContainer();
+ adDisplayContainer.setPlayer(this);
+ if (imaSdkSettings == null) {
+ imaSdkSettings = imaSdkFactory.createImaSdkSettings();
+ }
+ imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
+ imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
+ adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings);
+ adsLoader.addAdErrorListener(this);
+ adsLoader.addAdsLoadedListener(this);
+ fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
+ fakeContentProgressOffsetMs = C.TIME_UNSET;
+ pendingContentPositionMs = C.TIME_UNSET;
+ adGroupIndex = C.INDEX_UNSET;
+ contentDurationMs = C.TIME_UNSET;
+ }
+
+ /**
+ * Attaches a player that will play ads loaded using this instance.
+ *
+ * @param player The player instance that will play the loaded ads.
+ * @param eventListener Listener for ads loader events.
+ * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
+ */
+ /* package */ void attachPlayer(ExoPlayer player, EventListener eventListener,
+ ViewGroup adUiViewGroup) {
+ this.player = player;
+ this.eventListener = eventListener;
+ lastAdProgress = null;
+ lastContentProgress = null;
+ adDisplayContainer.setAdContainer(adUiViewGroup);
+ player.addListener(this);
+ if (adPlaybackState != null) {
+ eventListener.onAdPlaybackState(adPlaybackState);
+ if (imaPausedContent) {
+ adsManager.resume();
+ }
+ } else {
+ requestAds();
+ }
+ }
+
+ /**
+ * Detaches the attached player and event listener. To attach a new player, call
+ * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Call {@link #release()} to release
+ * all resources associated with this instance.
+ */
+ /* package */ void detachPlayer() {
+ if (adsManager != null && imaPausedContent) {
+ adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition()));
+ adsManager.pause();
+ }
+ lastAdProgress = getAdProgress();
+ lastContentProgress = getContentProgress();
+ player.removeListener(this);
+ player = null;
+ eventListener = null;
+ }
+
+ /**
+ * Releases the loader. Must be called when the instance is no longer needed.
+ */
+ public void release() {
+ released = true;
+ if (adsManager != null) {
+ adsManager.destroy();
+ adsManager = null;
+ }
+ }
+
+ // AdsLoader.AdsLoadedListener implementation.
+
+ @Override
+ public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
+ AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();
+ if (released) {
+ adsManager.destroy();
+ return;
+ }
+ this.adsManager = adsManager;
+ adsManager.addAdErrorListener(this);
+ adsManager.addAdEventListener(this);
+ if (ENABLE_PRELOADING) {
+ ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
+ AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
+ adsRenderingSettings.setEnablePreloading(true);
+ adsManager.init(adsRenderingSettings);
+ if (DEBUG) {
+ Log.d(TAG, "Initialized with preloading");
+ }
+ } else {
+ adsManager.init();
+ if (DEBUG) {
+ Log.d(TAG, "Initialized without preloading");
+ }
+ }
+ long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
+ adPlaybackState = new AdPlaybackState(adGroupTimesUs);
+ updateAdPlaybackState();
+ }
+
+ // AdEvent.AdEventListener implementation.
+
+ @Override
+ public void onAdEvent(AdEvent adEvent) {
+ AdEventType adEventType = adEvent.getType();
+ boolean isLogAdEvent = adEventType == AdEventType.LOG;
+ if (DEBUG || isLogAdEvent) {
+ Log.w(TAG, "onAdEvent: " + adEventType);
+ if (isLogAdEvent) {
+ for (Map.Entry entry : adEvent.getAdData().entrySet()) {
+ Log.w(TAG, " " + entry.getKey() + ": " + entry.getValue());
+ }
+ }
+ }
+ if (adsManager == null) {
+ Log.w(TAG, "Dropping ad event after release: " + adEvent);
+ return;
+ }
+ Ad ad = adEvent.getAd();
+ switch (adEvent.getType()) {
+ case LOADED:
+ // The ad position is not always accurate when using preloading. See [Internal: b/62613240].
+ AdPodInfo adPodInfo = ad.getAdPodInfo();
+ int podIndex = adPodInfo.getPodIndex();
+ adGroupIndex = podIndex == -1 ? adPlaybackState.adGroupCount - 1 : podIndex;
+ int adPosition = adPodInfo.getAdPosition();
+ int adCountInAdGroup = adPodInfo.getTotalAds();
+ adsManager.start();
+ if (DEBUG) {
+ Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group "
+ + adGroupIndex);
+ }
+ adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup);
+ updateAdPlaybackState();
+ break;
+ case CONTENT_PAUSE_REQUESTED:
+ // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
+ // before sending CONTENT_RESUME_REQUESTED.
+ imaPausedContent = true;
+ pauseContentInternal();
+ break;
+ case TAPPED:
+ if (eventListener != null) {
+ eventListener.onAdTapped();
+ }
+ break;
+ case CLICKED:
+ if (eventListener != null) {
+ eventListener.onAdClicked();
+ }
+ break;
+ case CONTENT_RESUME_REQUESTED:
+ imaPausedContent = false;
+ resumeContentInternal();
+ break;
+ case ALL_ADS_COMPLETED:
+ // Do nothing. The ads manager will be released when the source is released.
+ default:
+ break;
+ }
+ }
+
+ // AdErrorEvent.AdErrorListener implementation.
+
+ @Override
+ public void onAdError(AdErrorEvent adErrorEvent) {
+ if (DEBUG) {
+ Log.d(TAG, "onAdError " + adErrorEvent);
+ }
+ if (adsManager == null) {
+ adPlaybackState = new AdPlaybackState(new long[0]);
+ updateAdPlaybackState();
+ }
+ if (eventListener != null) {
+ IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError());
+ eventListener.onLoadError(exception);
+ }
+ }
+
+ // ContentProgressProvider implementation.
+
+ @Override
+ public VideoProgressUpdate getContentProgress() {
+ if (player == null) {
+ return lastContentProgress;
+ } else if (pendingContentPositionMs != C.TIME_UNSET) {
+ sentPendingContentPositionMs = true;
+ return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs);
+ } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
+ long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
+ long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
+ return new VideoProgressUpdate(fakePositionMs, contentDurationMs);
+ } else if (playingAd || contentDurationMs == C.TIME_UNSET) {
+ return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ } else {
+ return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
+ }
+ }
+
+ // VideoAdPlayer implementation.
+
+ @Override
+ public VideoProgressUpdate getAdProgress() {
+ if (player == null) {
+ return lastAdProgress;
+ } else if (!playingAd) {
+ return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ } else {
+ return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration());
+ }
+ }
+
+ @Override
+ public void loadAd(String adUriString) {
+ if (DEBUG) {
+ Log.d(TAG, "loadAd in ad group " + adGroupIndex);
+ }
+ adPlaybackState.addAdUri(adGroupIndex, Uri.parse(adUriString));
+ updateAdPlaybackState();
+ }
+
+ @Override
+ public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
+ adCallbacks.add(videoAdPlayerCallback);
+ }
+
+ @Override
+ public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
+ adCallbacks.remove(videoAdPlayerCallback);
+ }
+
+ @Override
+ public void playAd() {
+ if (DEBUG) {
+ Log.d(TAG, "playAd");
+ }
+ if (player == null) {
+ // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
+ Log.w(TAG, "Unexpected playAd while detached");
+ } else if (!player.getPlayWhenReady()) {
+ playWhenReadyOverriddenForAds = true;
+ player.setPlayWhenReady(true);
+ }
+ if (imaPlayingAd && !imaPausedInAd) {
+ // Work around an issue where IMA does not always call stopAd before resuming content.
+ // See [Internal: b/38354028, b/63320878].
+ Log.w(TAG, "Unexpected playAd without stopAd");
+ }
+ if (!imaPlayingAd) {
+ imaPlayingAd = true;
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onPlay();
+ }
+ } else if (imaPausedInAd) {
+ imaPausedInAd = false;
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onResume();
+ }
+ }
+ }
+
+ @Override
+ public void stopAd() {
+ if (DEBUG) {
+ Log.d(TAG, "stopAd");
+ }
+ if (player == null) {
+ // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
+ Log.w(TAG, "Unexpected stopAd while detached");
+ }
+ if (!imaPlayingAd) {
+ Log.w(TAG, "Unexpected stopAd");
+ return;
+ }
+ stopAdInternal();
+ }
+
+ @Override
+ public void pauseAd() {
+ if (DEBUG) {
+ Log.d(TAG, "pauseAd");
+ }
+ if (!imaPlayingAd) {
+ // This method is called after content is resumed.
+ return;
+ }
+ imaPausedInAd = true;
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onPause();
+ }
+ }
+
+ @Override
+ public void resumeAd() {
+ // This method is never called. See [Internal: b/18931719].
+ throw new IllegalStateException();
+ }
+
+ // Player.EventListener implementation.
+
+ @Override
+ public void onTimelineChanged(Timeline timeline, Object manifest) {
+ if (timeline.isEmpty()) {
+ // The player is being re-prepared and this source will be released.
+ return;
+ }
+ Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ this.timeline = timeline;
+ contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs);
+ updateImaStateForPlayerState();
+ }
+
+ @Override
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onLoadingChanged(boolean isLoading) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ if (adsManager == null) {
+ return;
+ }
+
+ if (!imaPlayingAd && playbackState == Player.STATE_BUFFERING && playWhenReady) {
+ checkForContentComplete();
+ } else if (imaPlayingAd && playbackState == Player.STATE_ENDED) {
+ // IMA is waiting for the ad playback to finish so invoke the callback now.
+ // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onEnded();
+ }
+ }
+ }
+
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException error) {
+ if (playingAd) {
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onError();
+ }
+ }
+ }
+
+ @Override
+ public void onPositionDiscontinuity() {
+ if (adsManager == null) {
+ return;
+ }
+ if (!playingAd && !player.isPlayingAd()) {
+ checkForContentComplete();
+ if (sentContentComplete) {
+ for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
+ if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) {
+ adPlaybackState.playedAdGroup(i);
+ }
+ }
+ updateAdPlaybackState();
+ } else {
+ long positionMs = player.getCurrentPosition();
+ timeline.getPeriod(0, period);
+ if (period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)) != C.INDEX_UNSET) {
+ sentPendingContentPositionMs = false;
+ pendingContentPositionMs = positionMs;
+ }
+ }
+ } else {
+ updateImaStateForPlayerState();
+ }
+ }
+
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
+ // Internal methods.
+
+ private void requestAds() {
+ AdsRequest request = imaSdkFactory.createAdsRequest();
+ request.setAdTagUrl(adTagUri.toString());
+ request.setAdDisplayContainer(adDisplayContainer);
+ request.setContentProgressProvider(this);
+ adsLoader.requestAds(request);
+ }
+
+ private void updateImaStateForPlayerState() {
+ boolean wasPlayingAd = playingAd;
+ playingAd = player.isPlayingAd();
+ if (!playingAd && playWhenReadyOverriddenForAds) {
+ playWhenReadyOverriddenForAds = false;
+ player.setPlayWhenReady(false);
+ }
+ if (!sentContentComplete) {
+ boolean adFinished = (wasPlayingAd && !playingAd)
+ || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup();
+ if (adFinished) {
+ // IMA is waiting for the ad playback to finish so invoke the callback now.
+ // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onEnded();
+ }
+ }
+ if (!wasPlayingAd && playingAd) {
+ int adGroupIndex = player.getCurrentAdGroupIndex();
+ // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
+ Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET);
+ fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
+ fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
+ if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
+ fakeContentProgressOffsetMs = contentDurationMs;
+ }
+ }
+ }
+ playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
+ }
+
+ private void resumeContentInternal() {
+ if (imaPlayingAd) {
+ if (DEBUG) {
+ Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
+ }
+ }
+ if (playingAd && adGroupIndex != C.INDEX_UNSET) {
+ adPlaybackState.playedAdGroup(adGroupIndex);
+ adGroupIndex = C.INDEX_UNSET;
+ updateAdPlaybackState();
+ }
+ clearFlags();
+ }
+
+ private void pauseContentInternal() {
+ if (sentPendingContentPositionMs) {
+ pendingContentPositionMs = C.TIME_UNSET;
+ sentPendingContentPositionMs = false;
+ }
+ // IMA is requesting to pause content, so stop faking the content position.
+ fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
+ fakeContentProgressOffsetMs = C.TIME_UNSET;
+ clearFlags();
+ }
+
+ private void stopAdInternal() {
+ Assertions.checkState(imaPlayingAd);
+ adPlaybackState.playedAd(adGroupIndex);
+ updateAdPlaybackState();
+ if (!playingAd) {
+ adGroupIndex = C.INDEX_UNSET;
+ }
+ clearFlags();
+ }
+
+ private void clearFlags() {
+ // If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until
+ // the content is resumed.
+ imaPlayingAd = false;
+ imaPausedInAd = false;
+ }
+
+ private void checkForContentComplete() {
+ if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET
+ && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
+ && !sentContentComplete) {
+ adsLoader.contentComplete();
+ if (DEBUG) {
+ Log.d(TAG, "adsLoader.contentComplete");
+ }
+ sentContentComplete = true;
+ }
+ }
+
+ private void updateAdPlaybackState() {
+ // Ignore updates while detached. When a player is attached it will receive the latest state.
+ if (eventListener != null) {
+ eventListener.onAdPlaybackState(adPlaybackState.copy());
+ }
+ }
+
+ private static long[] getAdGroupTimesUs(List cuePoints) {
+ if (cuePoints.isEmpty()) {
+ // If no cue points are specified, there is a preroll ad.
+ return new long[] {0};
+ }
+
+ int count = cuePoints.size();
+ long[] adGroupTimesUs = new long[count];
+ for (int i = 0; i < count; i++) {
+ double cuePoint = cuePoints.get(i);
+ adGroupTimesUs[i] =
+ cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint);
+ }
+ return adGroupTimesUs;
+ }
+
+}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
new file mode 100644
index 0000000000..d56a3ad41f
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.view.ViewGroup;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link MediaSource} that inserts ads linearly with a provided content media source using the
+ * Interactive Media Ads SDK for ad loading and tracking.
+ */
+public final class ImaAdsMediaSource implements MediaSource {
+
+ /**
+ * Listener for events relating to ad loading.
+ */
+ public interface AdsListener {
+
+ /**
+ * Called if there was an error loading ads. The media source will load the content without ads
+ * if ads can't be loaded, so listen for this event if you need to implement additional handling
+ * (for example, stopping the player).
+ *
+ * @param error The error.
+ */
+ void onAdLoadError(IOException error);
+
+ /**
+ * Called when the user clicks through an ad (for example, following a 'learn more' link).
+ */
+ void onAdClicked();
+
+ /**
+ * Called when the user taps a non-clickthrough part of an ad.
+ */
+ void onAdTapped();
+
+ }
+
+ private static final String TAG = "ImaAdsMediaSource";
+
+ private final MediaSource contentMediaSource;
+ private final DataSource.Factory dataSourceFactory;
+ private final ImaAdsLoader imaAdsLoader;
+ private final ViewGroup adUiViewGroup;
+ private final Handler mainHandler;
+ private final AdsLoaderListener adsLoaderListener;
+ private final Map adMediaSourceByMediaPeriod;
+ private final Timeline.Period period;
+ @Nullable
+ private final Handler eventHandler;
+ @Nullable
+ private final AdsListener eventListener;
+
+ private Handler playerHandler;
+ private ExoPlayer player;
+ private volatile boolean released;
+
+ // Accessed on the player thread.
+ private Timeline contentTimeline;
+ private Object contentManifest;
+ private AdPlaybackState adPlaybackState;
+ private MediaSource[][] adGroupMediaSources;
+ private long[][] adDurationsUs;
+ private MediaSource.Listener listener;
+
+ /**
+ * Constructs a new source that inserts ads linearly with the content specified by
+ * {@code contentMediaSource}.
+ *
+ * @param contentMediaSource The {@link MediaSource} providing the content to play.
+ * @param dataSourceFactory Factory for data sources used to load ad media.
+ * @param imaAdsLoader The loader for ads.
+ * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
+ */
+ public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
+ ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup) {
+ this(contentMediaSource, dataSourceFactory, imaAdsLoader, adUiViewGroup, null, null);
+ }
+
+ /**
+ * Constructs a new source that inserts ads linearly with the content specified by
+ * {@code contentMediaSource}.
+ *
+ * @param contentMediaSource The {@link MediaSource} providing the content to play.
+ * @param dataSourceFactory Factory for data sources used to load ad media.
+ * @param imaAdsLoader The loader for ads.
+ * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
+ ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler,
+ @Nullable AdsListener eventListener) {
+ this.contentMediaSource = contentMediaSource;
+ this.dataSourceFactory = dataSourceFactory;
+ this.imaAdsLoader = imaAdsLoader;
+ this.adUiViewGroup = adUiViewGroup;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ mainHandler = new Handler(Looper.getMainLooper());
+ adsLoaderListener = new AdsLoaderListener();
+ adMediaSourceByMediaPeriod = new HashMap<>();
+ period = new Timeline.Period();
+ adGroupMediaSources = new MediaSource[0][];
+ adDurationsUs = new long[0][];
+ }
+
+ @Override
+ public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ Assertions.checkArgument(isTopLevelSource);
+ this.listener = listener;
+ this.player = player;
+ playerHandler = new Handler();
+ contentMediaSource.prepareSource(player, false, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ ImaAdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest);
+ }
+ });
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ imaAdsLoader.attachPlayer(player, adsLoaderListener, adUiViewGroup);
+ }
+ });
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ contentMediaSource.maybeThrowSourceInfoRefreshError();
+ for (MediaSource[] mediaSources : adGroupMediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ if (mediaSource != null) {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ }
+ }
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ if (adPlaybackState.adGroupCount > 0 && id.isAd()) {
+ final int adGroupIndex = id.adGroupIndex;
+ final int adIndexInAdGroup = id.adIndexInAdGroup;
+ if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {
+ MediaSource adMediaSource = new ExtractorMediaSource(
+ adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory,
+ new DefaultExtractorsFactory(), mainHandler, adsLoaderListener);
+ int oldAdCount = adGroupMediaSources[id.adGroupIndex].length;
+ if (adIndexInAdGroup >= oldAdCount) {
+ int adCount = adIndexInAdGroup + 1;
+ adGroupMediaSources[adGroupIndex] =
+ Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);
+ adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount);
+ Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET);
+ }
+ adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
+ adMediaSource.prepareSource(player, false, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline);
+ }
+ });
+ }
+ MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];
+ MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator);
+ adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource);
+ return mediaPeriod;
+ } else {
+ return contentMediaSource.createPeriod(id, allocator);
+ }
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) {
+ adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
+ } else {
+ contentMediaSource.releasePeriod(mediaPeriod);
+ }
+ }
+
+ @Override
+ public void releaseSource() {
+ released = true;
+ contentMediaSource.releaseSource();
+ for (MediaSource[] mediaSources : adGroupMediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ if (mediaSource != null) {
+ mediaSource.releaseSource();
+ }
+ }
+ }
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ imaAdsLoader.detachPlayer();
+ }
+ });
+ }
+
+ // Internal methods.
+
+ private void onAdPlaybackState(AdPlaybackState adPlaybackState) {
+ if (this.adPlaybackState == null) {
+ adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][];
+ Arrays.fill(adGroupMediaSources, new MediaSource[0]);
+ adDurationsUs = new long[adPlaybackState.adGroupCount][];
+ Arrays.fill(adDurationsUs, new long[0]);
+ }
+ this.adPlaybackState = adPlaybackState;
+ maybeUpdateSourceInfo();
+ }
+
+ private void onLoadError(final IOException error) {
+ Log.w(TAG, "Ad load error", error);
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (!released) {
+ eventListener.onAdLoadError(error);
+ }
+ }
+ });
+ }
+ }
+
+ private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ contentTimeline = timeline;
+ contentManifest = manifest;
+ maybeUpdateSourceInfo();
+ }
+
+ private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) {
+ Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs();
+ maybeUpdateSourceInfo();
+ }
+
+ private void maybeUpdateSourceInfo() {
+ if (adPlaybackState != null && contentTimeline != null) {
+ Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline
+ : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs,
+ adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts,
+ adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs);
+ listener.onSourceInfoRefreshed(timeline, contentManifest);
+ }
+ }
+
+ /**
+ * Listener for ad loading events. All methods are called on the main thread.
+ */
+ private final class AdsLoaderListener implements ImaAdsLoader.EventListener,
+ ExtractorMediaSource.EventListener {
+
+ @Override
+ public void onAdPlaybackState(final AdPlaybackState adPlaybackState) {
+ if (released) {
+ return;
+ }
+ playerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (released) {
+ return;
+ }
+ ImaAdsMediaSource.this.onAdPlaybackState(adPlaybackState);
+ }
+ });
+ }
+
+ @Override
+ public void onLoadError(final IOException error) {
+ if (released) {
+ return;
+ }
+ playerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (released) {
+ return;
+ }
+ ImaAdsMediaSource.this.onLoadError(error);
+ }
+ });
+ }
+
+ @Override
+ public void onAdClicked() {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (!released) {
+ eventListener.onAdClicked();
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onAdTapped() {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (!released) {
+ eventListener.onAdTapped();
+ }
+ }
+ });
+ }
+ }
+
+ }
+
+}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java
new file mode 100644
index 0000000000..1d73234286
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * A {@link Timeline} for sources that have ads.
+ */
+/* package */ final class SinglePeriodAdTimeline extends Timeline {
+
+ private final Timeline contentTimeline;
+ private final long[] adGroupTimesUs;
+ private final int[] adCounts;
+ private final int[] adsLoadedCounts;
+ private final int[] adsPlayedCounts;
+ private final long[][] adDurationsUs;
+ private final long adResumePositionUs;
+
+ /**
+ * Creates a new timeline with a single period containing the specified ads.
+ *
+ * @param contentTimeline The timeline of the content alongside which ads will be played. It must
+ * have one window and one period.
+ * @param adGroupTimesUs The times of ad groups relative to the start of the period, in
+ * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
+ * the period has a postroll ad.
+ * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET}
+ * if the number of ads is not yet known.
+ * @param adsLoadedCounts The number of ads loaded so far in each ad group.
+ * @param adsPlayedCounts The number of ads played so far in each ad group.
+ * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element
+ * may be {@link C#TIME_UNSET} if the duration is not yet known.
+ * @param adResumePositionUs The position offset in the earliest unplayed ad at which to begin
+ * playback, in microseconds.
+ */
+ public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts,
+ int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs,
+ long adResumePositionUs) {
+ Assertions.checkState(contentTimeline.getPeriodCount() == 1);
+ Assertions.checkState(contentTimeline.getWindowCount() == 1);
+ this.contentTimeline = contentTimeline;
+ this.adGroupTimesUs = adGroupTimesUs;
+ this.adCounts = adCounts;
+ this.adsLoadedCounts = adsLoadedCounts;
+ this.adsPlayedCounts = adsPlayedCounts;
+ this.adDurationsUs = adDurationsUs;
+ this.adResumePositionUs = adResumePositionUs;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return 1;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ contentTimeline.getPeriod(periodIndex, period, setIds);
+ period.set(period.id, period.uid, period.windowIndex, period.durationUs,
+ period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts,
+ adDurationsUs, adResumePositionUs);
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return contentTimeline.getIndexOfPeriod(uid);
+ }
+
+}
diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md
new file mode 100644
index 0000000000..3acf8e4c79
--- /dev/null
+++ b/extensions/mediasession/README.md
@@ -0,0 +1,27 @@
+# ExoPlayer MediaSession extension #
+
+## Description ##
+
+The MediaSession extension mediates between an ExoPlayer instance and a
+[MediaSession][]. It automatically retrieves and implements playback actions
+and syncs the player state with the state of the media session. The behaviour
+can be extended to support other playback and custom actions.
+
+[MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html
+
+## Getting the extension ##
+
+The easiest way to use the extension is to add it as a gradle dependency:
+
+```gradle
+compile 'com.google.android.exoplayer:extension-mediasession:rX.X.X'
+```
+
+where `rX.X.X` is the version, which must match the version of the ExoPlayer
+library being used.
+
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle
new file mode 100644
index 0000000000..85a8ac46e2
--- /dev/null
+++ b/extensions/mediasession/build.gradle
@@ -0,0 +1,42 @@
+// Copyright (C) 2017 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.
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+ buildToolsVersion project.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.targetSdkVersion
+ }
+}
+
+dependencies {
+ compile project(modulePrefix + 'library-core')
+ compile 'com.android.support:support-media-compat:' + supportLibraryVersion
+ compile 'com.android.support:appcompat-v7:' + supportLibraryVersion
+}
+
+ext {
+ javadocTitle = 'Media session extension'
+}
+apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'extension-mediasession'
+ releaseDescription = 'Media session extension for ExoPlayer.'
+}
+apply from: '../../publish.gradle'
diff --git a/extensions/mediasession/src/main/AndroidManifest.xml b/extensions/mediasession/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..8ed6ef2011
--- /dev/null
+++ b/extensions/mediasession/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java
new file mode 100644
index 0000000000..c3586b29e6
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.mediasession;
+
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+
+/**
+ * A default implementation of {@link MediaSessionConnector.PlaybackController}.
+ *
+ * Methods can be safely overridden by subclasses to intercept calls for given actions.
+ */
+public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController {
+
+ /**
+ * The default fast forward increment, in milliseconds.
+ */
+ public static final int DEFAULT_FAST_FORWARD_MS = 15000;
+ /**
+ * The default rewind increment, in milliseconds.
+ */
+ public static final int DEFAULT_REWIND_MS = 5000;
+
+ private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_STOP;
+
+ protected final long rewindIncrementMs;
+ protected final long fastForwardIncrementMs;
+
+ /**
+ * Creates a new instance.
+ *
+ * Equivalent to {@code DefaultPlaybackController(
+ * DefaultPlaybackController.DEFAULT_REWIND_MS,
+ * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS)}.
+ */
+ public DefaultPlaybackController() {
+ this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS);
+ }
+
+ /**
+ * Creates a new instance with the given fast forward and rewind increments.
+ *
+ * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will
+ * cause the rewind action to be disabled.
+ * @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative
+ * value will cause the fast forward action to be removed.
+ */
+ public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs) {
+ this.rewindIncrementMs = rewindIncrementMs;
+ this.fastForwardIncrementMs = fastForwardIncrementMs;
+ }
+
+ @Override
+ public long getSupportedPlaybackActions(Player player) {
+ if (player == null || player.getCurrentTimeline().isEmpty()) {
+ return 0;
+ }
+ long actions = BASE_ACTIONS;
+ if (player.isCurrentWindowSeekable()) {
+ actions |= PlaybackStateCompat.ACTION_SEEK_TO;
+ }
+ if (fastForwardIncrementMs > 0) {
+ actions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
+ }
+ if (rewindIncrementMs > 0) {
+ actions |= PlaybackStateCompat.ACTION_REWIND;
+ }
+ return actions;
+ }
+
+ @Override
+ public void onPlay(Player player) {
+ player.setPlayWhenReady(true);
+ }
+
+ @Override
+ public void onPause(Player player) {
+ player.setPlayWhenReady(false);
+ }
+
+ @Override
+ public void onSeekTo(Player player, long position) {
+ long duration = player.getDuration();
+ if (duration != C.TIME_UNSET) {
+ position = Math.min(position, duration);
+ }
+ player.seekTo(Math.max(position, 0));
+ }
+
+ @Override
+ public void onFastForward(Player player) {
+ if (fastForwardIncrementMs <= 0) {
+ return;
+ }
+ onSeekTo(player, player.getCurrentPosition() + fastForwardIncrementMs);
+ }
+
+ @Override
+ public void onRewind(Player player) {
+ if (rewindIncrementMs <= 0) {
+ return;
+ }
+ onSeekTo(player, player.getCurrentPosition() - rewindIncrementMs);
+ }
+
+ @Override
+ public void onStop(Player player) {
+ player.stop();
+ }
+
+}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
new file mode 100644
index 0000000000..0e839b8083
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -0,0 +1,836 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.mediasession;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Connects a {@link MediaSessionCompat} to a {@link Player}.
+ *
+ * The connector listens for actions sent by the media session's controller and implements these
+ * actions by calling appropriate ExoPlayer methods. The playback state of the media session is
+ * automatically synced with the player. The connector can also be optionally extended by providing
+ * various collaborators:
+ *
+ *
Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and
+ * {@code PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
+ * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
+ * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar way.
+ *
+ *
To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
+ * calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator} is
+ * recommended for most use cases.
+ *
To enable editing of the media queue, you can set a {@link QueueEditor} by calling
+ * {@link #setQueueEditor(QueueEditor)}.
+ *
An {@link ErrorMessageProvider} for providing human readable error messages and
+ * corresponding error codes can be set by calling
+ * {@link #setErrorMessageProvider(ErrorMessageProvider)}.
+ *
+ */
+public final class MediaSessionConnector {
+
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession");
+ }
+
+ public static final String EXTRAS_PITCH = "EXO_PITCH";
+
+ /**
+ * Interface to which playback preparation actions are delegated.
+ */
+ public interface PlaybackPreparer {
+
+ long ACTIONS = PlaybackStateCompat.ACTION_PREPARE
+ | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
+ | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
+ | PlaybackStateCompat.ACTION_PREPARE_FROM_URI
+ | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
+ | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
+ | PlaybackStateCompat.ACTION_PLAY_FROM_URI;
+
+ /**
+ * Returns the actions which are supported by the preparer. The supported actions must be a
+ * bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE},
+ * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID},
+ * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH},
+ * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI},
+ * {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID},
+ * {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and
+ * {@link PlaybackStateCompat#ACTION_PLAY_FROM_URI}.
+ *
+ * @return The bitmask of the supported media actions.
+ */
+ long getSupportedPrepareActions();
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepare()}.
+ */
+ void onPrepare();
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
+ */
+ void onPrepareFromMediaId(String mediaId, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
+ */
+ void onPrepareFromSearch(String query, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
+ */
+ void onPrepareFromUri(Uri uri, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
+ */
+ void onCommand(String command, Bundle extras, ResultReceiver cb);
+ }
+
+ /**
+ * Interface to which playback actions are delegated.
+ */
+ public interface PlaybackController {
+
+ long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO
+ | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_STOP;
+
+ /**
+ * Returns the actions which are supported by the controller. The supported actions must be a
+ * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE},
+ * {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE},
+ * {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD},
+ * {@link PlaybackStateCompat#ACTION_REWIND} and {@link PlaybackStateCompat#ACTION_STOP}.
+ *
+ * @param player The player.
+ * @return The bitmask of the supported media actions.
+ */
+ long getSupportedPlaybackActions(@Nullable Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPlay()}.
+ */
+ void onPlay(Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPause()}.
+ */
+ void onPause(Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSeekTo(long)}.
+ */
+ void onSeekTo(Player player, long position);
+ /**
+ * See {@link MediaSessionCompat.Callback#onFastForward()}.
+ */
+ void onFastForward(Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onRewind()}.
+ */
+ void onRewind(Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onStop()}.
+ */
+ void onStop(Player player);
+ }
+
+ /**
+ * Handles queue navigation actions, and updates the media session queue by calling
+ * {@code MediaSessionCompat.setQueue()}.
+ */
+ public interface QueueNavigator {
+
+ long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED;
+
+ /**
+ * Returns the actions which are supported by the navigator. The supported actions must be a
+ * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM},
+ * {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT},
+ * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS},
+ * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}.
+ *
+ * @param player The {@link Player}.
+ * @return The bitmask of the supported media actions.
+ */
+ long getSupportedQueueNavigatorActions(@Nullable Player player);
+ /**
+ * Called when the timeline of the player has changed.
+ *
+ * @param player The player of which the timeline has changed.
+ */
+ void onTimelineChanged(Player player);
+ /**
+ * Called when the current window index changed.
+ *
+ * @param player The player of which the current window index of the timeline has changed.
+ */
+ void onCurrentWindowIndexChanged(Player player);
+ /**
+ * Gets the id of the currently active queue item, or
+ * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown.
+ *
+ * To let the connector publish metadata for the active queue item, the queue item with the
+ * returned id must be available in the list of items returned by
+ * {@link MediaControllerCompat#getQueue()}.
+ *
+ * @param player The player connected to the media session.
+ * @return The id of the active queue item.
+ */
+ long getActiveQueueItemId(@Nullable Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}.
+ */
+ void onSkipToPrevious(Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
+ */
+ void onSkipToQueueItem(Player player, long id);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToNext()}.
+ */
+ void onSkipToNext(Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSetShuffleModeEnabled(boolean)}.
+ */
+ void onSetShuffleModeEnabled(Player player, boolean enabled);
+ }
+
+ /**
+ * Handles media session queue edits.
+ */
+ public interface QueueEditor {
+
+ long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING;
+
+ /**
+ * Returns {@link PlaybackStateCompat#ACTION_SET_RATING} or {@code 0}. The Media API does
+ * not declare action constants for adding and removing queue items.
+ *
+ * @param player The {@link Player}.
+ */
+ long getSupportedQueueEditorActions(@Nullable Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}.
+ */
+ void onAddQueueItem(Player player, MediaDescriptionCompat description);
+ /**
+ * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description,
+ * int index)}.
+ */
+ void onAddQueueItem(Player player, MediaDescriptionCompat description, int index);
+ /**
+ * See {@link MediaSessionCompat.Callback#onRemoveQueueItem(MediaDescriptionCompat
+ * description)}.
+ */
+ void onRemoveQueueItem(Player player, MediaDescriptionCompat description);
+ /**
+ * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}.
+ */
+ void onRemoveQueueItemAt(Player player, int index);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}.
+ */
+ void onSetRating(Player player, RatingCompat rating);
+ }
+
+ /**
+ * Provides a {@link PlaybackStateCompat.CustomAction} to be published and handles the action when
+ * sent by a media controller.
+ */
+ public interface CustomActionProvider {
+ /**
+ * Called when a custom action provided by this provider is sent to the media session.
+ *
+ * @param action The name of the action which was sent by a media controller.
+ * @param extras Optional extras sent by a media controller.
+ */
+ void onCustomAction(String action, Bundle extras);
+
+ /**
+ * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the
+ * media session by the connector or {@code null} if this action should not be published at the
+ * given player state.
+ *
+ * @return The custom action to be included in the session playback state or {@code null}.
+ */
+ PlaybackStateCompat.CustomAction getCustomAction();
+ }
+
+ /**
+ * Converts an exception into an error code and a user readable error message.
+ */
+ public interface ErrorMessageProvider {
+ /**
+ * Returns a pair consisting of an error code and a user readable error message for a given
+ * exception.
+ */
+ Pair getErrorMessage(ExoPlaybackException playbackException);
+ }
+
+ /**
+ * The wrapped {@link MediaSessionCompat}.
+ */
+ public final MediaSessionCompat mediaSession;
+
+ private final MediaControllerCompat mediaController;
+ private final Handler handler;
+ private final boolean doMaintainMetadata;
+ private final ExoPlayerEventListener exoPlayerEventListener;
+ private final MediaSessionCallback mediaSessionCallback;
+ private final PlaybackController playbackController;
+
+ private Player player;
+ private CustomActionProvider[] customActionProviders;
+ private int currentWindowIndex;
+ private Map customActionMap;
+ private ErrorMessageProvider errorMessageProvider;
+ private PlaybackPreparer playbackPreparer;
+ private QueueNavigator queueNavigator;
+ private QueueEditor queueEditor;
+ private ExoPlaybackException playbackException;
+
+ /**
+ * Creates an instance. Must be called on the same thread that is used to construct the player
+ * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
+ *
+ * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
+ *
+ * @param mediaSession The {@link MediaSessionCompat} to connect to.
+ */
+ public MediaSessionConnector(MediaSessionCompat mediaSession) {
+ this(mediaSession, new DefaultPlaybackController());
+ }
+
+ /**
+ * Creates an instance. Must be called on the same thread that is used to construct the player
+ * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
+ *
+ * Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}.
+ *
+ * @param mediaSession The {@link MediaSessionCompat} to connect to.
+ * @param playbackController A {@link PlaybackController} for handling playback actions.
+ */
+ public MediaSessionConnector(MediaSessionCompat mediaSession,
+ PlaybackController playbackController) {
+ this(mediaSession, playbackController, true);
+ }
+
+ /**
+ * Creates an instance. Must be called on the same thread that is used to construct the player
+ * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
+ *
+ * @param mediaSession The {@link MediaSessionCompat} to connect to.
+ * @param playbackController A {@link PlaybackController} for handling playback actions.
+ * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If
+ * {@code false}, you need to maintain the metadata of the media session yourself (provide at
+ * least the duration to allow clients to show a progress bar).
+ */
+ public MediaSessionConnector(MediaSessionCompat mediaSession,
+ PlaybackController playbackController, boolean doMaintainMetadata) {
+ this.mediaSession = mediaSession;
+ this.playbackController = playbackController;
+ this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
+ : Looper.getMainLooper());
+ this.doMaintainMetadata = doMaintainMetadata;
+ mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
+ | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mediaController = mediaSession.getController();
+ mediaSessionCallback = new MediaSessionCallback();
+ exoPlayerEventListener = new ExoPlayerEventListener();
+ customActionMap = Collections.emptyMap();
+ }
+
+ /**
+ * Sets the player to be connected to the media session.
+ *
+ * The order in which any {@link CustomActionProvider}s are passed determines the order of the
+ * actions published with the playback state of the session.
+ *
+ * @param player The player to be connected to the {@code MediaSession}.
+ * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player.
+ * @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle
+ * custom actions.
+ */
+ public void setPlayer(Player player, PlaybackPreparer playbackPreparer,
+ CustomActionProvider... customActionProviders) {
+ if (this.player != null) {
+ this.player.removeListener(exoPlayerEventListener);
+ mediaSession.setCallback(null);
+ }
+ this.playbackPreparer = playbackPreparer;
+ this.player = player;
+ this.customActionProviders = (player != null && customActionProviders != null)
+ ? customActionProviders : new CustomActionProvider[0];
+ if (player != null) {
+ mediaSession.setCallback(mediaSessionCallback, handler);
+ player.addListener(exoPlayerEventListener);
+ }
+ updateMediaSessionPlaybackState();
+ updateMediaSessionMetadata();
+ }
+
+ /**
+ * Sets the {@link ErrorMessageProvider}.
+ *
+ * @param errorMessageProvider The error message provider.
+ */
+ public void setErrorMessageProvider(ErrorMessageProvider errorMessageProvider) {
+ this.errorMessageProvider = errorMessageProvider;
+ }
+
+ /**
+ * Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT},
+ * {@code ACTION_SKIP_TO_PREVIOUS}, {@code ACTION_SKIP_TO_QUEUE_ITEM} and
+ * {@code ACTION_SET_SHUFFLE_MODE_ENABLED}.
+ *
+ * @param queueNavigator The queue navigator.
+ */
+ public void setQueueNavigator(QueueNavigator queueNavigator) {
+ this.queueNavigator = queueNavigator;
+ }
+
+ /**
+ * Sets the {@link QueueEditor} to handle queue edits sent by the media controller.
+ *
+ * @param queueEditor The queue editor.
+ */
+ public void setQueueEditor(QueueEditor queueEditor) {
+ this.queueEditor = queueEditor;
+ }
+
+ private void updateMediaSessionPlaybackState() {
+ PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+ if (player == null) {
+ builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
+ mediaSession.setPlaybackState(builder.build());
+ return;
+ }
+
+ Map currentActions = new HashMap<>();
+ for (CustomActionProvider customActionProvider : customActionProviders) {
+ PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction();
+ if (customAction != null) {
+ currentActions.put(customAction.getAction(), customActionProvider);
+ builder.addCustomAction(customAction);
+ }
+ }
+ customActionMap = Collections.unmodifiableMap(currentActions);
+
+ int sessionPlaybackState = playbackException != null ? PlaybackStateCompat.STATE_ERROR
+ : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
+ if (playbackException != null) {
+ if (errorMessageProvider != null) {
+ Pair message = errorMessageProvider.getErrorMessage(playbackException);
+ builder.setErrorMessage(message.first, message.second);
+ }
+ if (player.getPlaybackState() != Player.STATE_IDLE) {
+ playbackException = null;
+ }
+ }
+ long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
+ : MediaSessionCompat.QueueItem.UNKNOWN_ID;
+ Bundle extras = new Bundle();
+ extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
+ builder.setActions(buildPlaybackActions())
+ .setActiveQueueItemId(activeQueueItemId)
+ .setBufferedPosition(player.getBufferedPosition())
+ .setState(sessionPlaybackState, player.getCurrentPosition(),
+ player.getPlaybackParameters().speed, SystemClock.elapsedRealtime())
+ .setExtras(extras);
+ mediaSession.setPlaybackState(builder.build());
+ }
+
+ private long buildPlaybackActions() {
+ long actions = 0;
+ if (playbackController != null) {
+ actions |= (PlaybackController.ACTIONS & playbackController
+ .getSupportedPlaybackActions(player));
+ }
+ if (playbackPreparer != null) {
+ actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
+ }
+ if (queueNavigator != null) {
+ actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(
+ player));
+ }
+ if (queueEditor != null) {
+ actions |= (QueueEditor.ACTIONS & queueEditor.getSupportedQueueEditorActions(player));
+ }
+ return actions;
+ }
+
+ private void updateMediaSessionMetadata() {
+ if (doMaintainMetadata) {
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ if (player != null && player.isPlayingAd()) {
+ builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1);
+ }
+ builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player == null ? 0
+ : player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration());
+
+ if (queueNavigator != null) {
+ long activeQueueItemId = queueNavigator.getActiveQueueItemId(player);
+ List queue = mediaController.getQueue();
+ for (int i = 0; queue != null && i < queue.size(); i++) {
+ MediaSessionCompat.QueueItem queueItem = queue.get(i);
+ if (queueItem.getQueueId() == activeQueueItemId) {
+ MediaDescriptionCompat description = queueItem.getDescription();
+ if (description.getTitle() != null) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
+ String.valueOf(description.getTitle()));
+ }
+ if (description.getSubtitle() != null) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
+ String.valueOf(description.getSubtitle()));
+ }
+ if (description.getDescription() != null) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
+ String.valueOf(description.getDescription()));
+ }
+ if (description.getIconBitmap() != null) {
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON,
+ description.getIconBitmap());
+ }
+ if (description.getIconUri() != null) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
+ String.valueOf(description.getIconUri()));
+ }
+ if (description.getMediaId() != null) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
+ String.valueOf(description.getMediaId()));
+ }
+ if (description.getMediaUri() != null) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
+ String.valueOf(description.getMediaUri()));
+ }
+ break;
+ }
+ }
+ }
+ mediaSession.setMetadata(builder.build());
+ }
+ }
+
+ private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) {
+ switch (exoPlayerPlaybackState) {
+ case Player.STATE_BUFFERING:
+ return PlaybackStateCompat.STATE_BUFFERING;
+ case Player.STATE_READY:
+ return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
+ case Player.STATE_ENDED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ default:
+ return PlaybackStateCompat.STATE_NONE;
+ }
+ }
+
+ private boolean canDispatchToPlaybackPreparer(long action) {
+ return playbackPreparer != null && (playbackPreparer.getSupportedPrepareActions()
+ & PlaybackPreparer.ACTIONS & action) != 0;
+ }
+
+ private boolean canDispatchToPlaybackController(long action) {
+ return playbackController != null && (playbackController.getSupportedPlaybackActions(player)
+ & PlaybackController.ACTIONS & action) != 0;
+ }
+
+ private boolean canDispatchToQueueNavigator(long action) {
+ return queueNavigator != null && (queueNavigator.getSupportedQueueNavigatorActions(player)
+ & QueueNavigator.ACTIONS & action) != 0;
+ }
+
+ private boolean canDispatchToQueueEditor(long action) {
+ return queueEditor != null && (queueEditor.getSupportedQueueEditorActions(player)
+ & QueueEditor.ACTIONS & action) != 0;
+ }
+
+ private class ExoPlayerEventListener implements Player.EventListener {
+
+ @Override
+ public void onTimelineChanged(Timeline timeline, Object manifest) {
+ if (queueNavigator != null) {
+ queueNavigator.onTimelineChanged(player);
+ }
+ currentWindowIndex = player.getCurrentWindowIndex();
+ updateMediaSessionMetadata();
+ }
+
+ @Override
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onLoadingChanged(boolean isLoading) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ updateMediaSessionPlaybackState();
+ }
+
+ @Override
+ public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
+ mediaSession.setRepeatMode(repeatMode == Player.REPEAT_MODE_ONE
+ ? PlaybackStateCompat.REPEAT_MODE_ONE : repeatMode == Player.REPEAT_MODE_ALL
+ ? PlaybackStateCompat.REPEAT_MODE_ALL : PlaybackStateCompat.REPEAT_MODE_NONE);
+ updateMediaSessionPlaybackState();
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException error) {
+ playbackException = error;
+ updateMediaSessionPlaybackState();
+ }
+
+ @Override
+ public void onPositionDiscontinuity() {
+ if (currentWindowIndex != player.getCurrentWindowIndex()) {
+ if (queueNavigator != null) {
+ queueNavigator.onCurrentWindowIndexChanged(player);
+ }
+ updateMediaSessionMetadata();
+ currentWindowIndex = player.getCurrentWindowIndex();
+ }
+ updateMediaSessionPlaybackState();
+ }
+
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ updateMediaSessionPlaybackState();
+ }
+
+ }
+
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+
+ @Override
+ public void onPlay() {
+ if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PLAY)) {
+ playbackController.onPlay(player);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PAUSE)) {
+ playbackController.onPause(player);
+ }
+ }
+
+ @Override
+ public void onSeekTo(long position) {
+ if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SEEK_TO)) {
+ playbackController.onSeekTo(player, position);
+ }
+ }
+
+ @Override
+ public void onFastForward() {
+ if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
+ playbackController.onFastForward(player);
+ }
+ }
+
+ @Override
+ public void onRewind() {
+ if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_REWIND)) {
+ playbackController.onRewind(player);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_STOP)) {
+ playbackController.onStop(player);
+ }
+ }
+
+ @Override
+ public void onSkipToNext() {
+ if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
+ queueNavigator.onSkipToNext(player);
+ }
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
+ queueNavigator.onSkipToPrevious(player);
+ }
+ }
+
+ @Override
+ public void onSkipToQueueItem(long id) {
+ if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) {
+ queueNavigator.onSkipToQueueItem(player, id);
+ }
+ }
+
+ @Override
+ public void onSetRepeatMode(int repeatMode) {
+ // implemented as custom action
+ }
+
+ @Override
+ public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
+ Map actionMap = customActionMap;
+ if (actionMap.containsKey(action)) {
+ actionMap.get(action).onCustomAction(action, extras);
+ updateMediaSessionPlaybackState();
+ }
+ }
+
+ @Override
+ public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+ if (playbackPreparer != null) {
+ playbackPreparer.onCommand(command, extras, cb);
+ }
+ }
+
+ @Override
+ public void onPrepare() {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
+ player.stop();
+ player.setPlayWhenReady(false);
+ playbackPreparer.onPrepare();
+ }
+ }
+
+ @Override
+ public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
+ player.stop();
+ player.setPlayWhenReady(false);
+ playbackPreparer.onPrepareFromMediaId(mediaId, extras);
+ }
+ }
+
+ @Override
+ public void onPrepareFromSearch(String query, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
+ player.stop();
+ player.setPlayWhenReady(false);
+ playbackPreparer.onPrepareFromSearch(query, extras);
+ }
+ }
+
+ @Override
+ public void onPrepareFromUri(Uri uri, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
+ player.stop();
+ player.setPlayWhenReady(false);
+ playbackPreparer.onPrepareFromUri(uri, extras);
+ }
+ }
+
+ @Override
+ public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
+ player.stop();
+ player.setPlayWhenReady(true);
+ playbackPreparer.onPrepareFromMediaId(mediaId, extras);
+ }
+ }
+
+ @Override
+ public void onPlayFromSearch(String query, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
+ player.stop();
+ player.setPlayWhenReady(true);
+ playbackPreparer.onPrepareFromSearch(query, extras);
+ }
+ }
+
+ @Override
+ public void onPlayFromUri(Uri uri, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
+ player.stop();
+ player.setPlayWhenReady(true);
+ playbackPreparer.onPrepareFromUri(uri, extras);
+ }
+ }
+
+ @Override
+ public void onSetShuffleModeEnabled(boolean enabled) {
+ if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) {
+ queueNavigator.onSetShuffleModeEnabled(player, enabled);
+ }
+ }
+
+ @Override
+ public void onAddQueueItem(MediaDescriptionCompat description) {
+ if (queueEditor != null) {
+ queueEditor.onAddQueueItem(player, description);
+ }
+ }
+
+ @Override
+ public void onAddQueueItem(MediaDescriptionCompat description, int index) {
+ if (queueEditor != null) {
+ queueEditor.onAddQueueItem(player, description, index);
+ }
+ }
+
+ @Override
+ public void onRemoveQueueItem(MediaDescriptionCompat description) {
+ if (queueEditor != null) {
+ queueEditor.onRemoveQueueItem(player, description);
+ }
+ }
+
+ @Override
+ public void onRemoveQueueItemAt(int index) {
+ if (queueEditor != null) {
+ queueEditor.onRemoveQueueItemAt(player, index);
+ }
+ }
+
+ @Override
+ public void onSetRating(RatingCompat rating) {
+ if (canDispatchToQueueEditor(PlaybackStateCompat.ACTION_SET_RATING)) {
+ queueEditor.onSetRating(player, rating);
+ }
+ }
+
+ }
+
+}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
new file mode 100644
index 0000000000..abefe533ce
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
@@ -0,0 +1,106 @@
+package com.google.android.exoplayer2.ext.mediasession;
+/*
+ * Copyright (c) 2017 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.
+ */
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.media.session.PlaybackStateCompat;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.util.RepeatModeUtil;
+
+/**
+ * Provides a custom action for toggling repeat modes.
+ */
+public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
+
+ /**
+ * The default repeat toggle modes.
+ */
+ public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
+ RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
+
+ private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE";
+
+ private final Player player;
+ @RepeatModeUtil.RepeatToggleModes
+ private final int repeatToggleModes;
+ private final CharSequence repeatAllDescription;
+ private final CharSequence repeatOneDescription;
+ private final CharSequence repeatOffDescription;
+
+ /**
+ * Creates a new instance.
+ *
+ * Equivalent to {@code RepeatModeActionProvider(context, player,
+ * RepeatModeActionProvider.DEFAULT_REPEAT_TOGGLE_MODES)}.
+ *
+ * @param context The context.
+ * @param player The player on which to toggle the repeat mode.
+ */
+ public RepeatModeActionProvider(Context context, Player player) {
+ this(context, player, DEFAULT_REPEAT_TOGGLE_MODES);
+ }
+
+ /**
+ * Creates a new instance enabling the given repeat toggle modes.
+ *
+ * @param context The context.
+ * @param player The player on which to toggle the repeat mode.
+ * @param repeatToggleModes The toggle modes to enable.
+ */
+ public RepeatModeActionProvider(Context context, Player player,
+ @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
+ this.player = player;
+ this.repeatToggleModes = repeatToggleModes;
+ repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);
+ repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);
+ repeatOffDescription = context.getString(R.string.exo_media_action_repeat_off_description);
+ }
+
+ @Override
+ public void onCustomAction(String action, Bundle extras) {
+ int mode = player.getRepeatMode();
+ int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
+ if (mode != proposedMode) {
+ player.setRepeatMode(proposedMode);
+ }
+ }
+
+ @Override
+ public PlaybackStateCompat.CustomAction getCustomAction() {
+ CharSequence actionLabel;
+ int iconResourceId;
+ switch (player.getRepeatMode()) {
+ case Player.REPEAT_MODE_ONE:
+ actionLabel = repeatOneDescription;
+ iconResourceId = R.drawable.exo_media_action_repeat_one;
+ break;
+ case Player.REPEAT_MODE_ALL:
+ actionLabel = repeatAllDescription;
+ iconResourceId = R.drawable.exo_media_action_repeat_all;
+ break;
+ case Player.REPEAT_MODE_OFF:
+ default:
+ actionLabel = repeatOffDescription;
+ iconResourceId = R.drawable.exo_media_action_repeat_off;
+ break;
+ }
+ PlaybackStateCompat.CustomAction.Builder repeatBuilder = new PlaybackStateCompat.CustomAction
+ .Builder(ACTION_REPEAT_MODE, actionLabel, iconResourceId);
+ return repeatBuilder.build();
+ }
+
+}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
new file mode 100644
index 0000000000..521b4cd6e3
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.mediasession;
+
+import android.support.annotation.Nullable;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.util.Util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that maps the
+ * windows of a {@link Player}'s {@link Timeline} to the media session queue.
+ */
+public abstract class TimelineQueueNavigator implements MediaSessionConnector.QueueNavigator {
+
+ public static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;
+ public static final int DEFAULT_MAX_QUEUE_SIZE = 10;
+
+ private final MediaSessionCompat mediaSession;
+ protected final int maxQueueSize;
+
+ private long activeQueueItemId;
+
+ /**
+ * Creates an instance for a given {@link MediaSessionCompat}.
+ *
+ * Equivalent to {@code TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}.
+ *
+ * @param mediaSession The {@link MediaSessionCompat}.
+ */
+ public TimelineQueueNavigator(MediaSessionCompat mediaSession) {
+ this(mediaSession, DEFAULT_MAX_QUEUE_SIZE);
+ }
+
+ /**
+ * Creates an instance for a given {@link MediaSessionCompat} and maximum queue size.
+ *
+ * If the number of windows in the {@link Player}'s {@link Timeline} exceeds {@code maxQueueSize},
+ * the media session queue will correspond to {@code maxQueueSize} windows centered on the one
+ * currently being played.
+ *
+ * @param mediaSession The {@link MediaSessionCompat}.
+ * @param maxQueueSize The maximum queue size.
+ */
+ public TimelineQueueNavigator(MediaSessionCompat mediaSession, int maxQueueSize) {
+ this.mediaSession = mediaSession;
+ this.maxQueueSize = maxQueueSize;
+ activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
+ }
+
+ /**
+ * Gets the {@link MediaDescriptionCompat} for a given timeline window index.
+ *
+ * @param windowIndex The timeline window index for which to provide a description.
+ * @return A {@link MediaDescriptionCompat}.
+ */
+ public abstract MediaDescriptionCompat getMediaDescription(int windowIndex);
+
+ @Override
+ public long getSupportedQueueNavigatorActions(Player player) {
+ if (player == null || player.getCurrentTimeline().getWindowCount() < 2) {
+ return 0;
+ }
+ if (player.getRepeatMode() != Player.REPEAT_MODE_OFF) {
+ return PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
+ }
+
+ int currentWindowIndex = player.getCurrentWindowIndex();
+ long actions;
+ if (currentWindowIndex == 0) {
+ actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
+ } else if (currentWindowIndex == player.getCurrentTimeline().getWindowCount() - 1) {
+ actions = PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+ } else {
+ actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+ }
+ return actions | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
+ }
+
+ @Override
+ public final void onTimelineChanged(Player player) {
+ publishFloatingQueueWindow(player);
+ }
+
+ @Override
+ public final void onCurrentWindowIndexChanged(Player player) {
+ if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
+ || player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
+ publishFloatingQueueWindow(player);
+ } else if (!player.getCurrentTimeline().isEmpty()) {
+ activeQueueItemId = player.getCurrentWindowIndex();
+ }
+ }
+
+ @Override
+ public final long getActiveQueueItemId(@Nullable Player player) {
+ return activeQueueItemId;
+ }
+
+ @Override
+ public void onSkipToPrevious(Player player) {
+ Timeline timeline = player.getCurrentTimeline();
+ if (timeline.isEmpty()) {
+ return;
+ }
+ int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(),
+ player.getRepeatMode());
+ if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS
+ || previousWindowIndex == C.INDEX_UNSET) {
+ player.seekTo(0);
+ } else {
+ player.seekTo(previousWindowIndex, C.TIME_UNSET);
+ }
+ }
+
+ @Override
+ public void onSkipToQueueItem(Player player, long id) {
+ Timeline timeline = player.getCurrentTimeline();
+ if (timeline.isEmpty()) {
+ return;
+ }
+ int windowIndex = (int) id;
+ if (0 <= windowIndex && windowIndex < timeline.getWindowCount()) {
+ player.seekTo(windowIndex, C.TIME_UNSET);
+ }
+ }
+
+ @Override
+ public void onSkipToNext(Player player) {
+ Timeline timeline = player.getCurrentTimeline();
+ if (timeline.isEmpty()) {
+ return;
+ }
+ int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(),
+ player.getRepeatMode());
+ if (nextWindowIndex != C.INDEX_UNSET) {
+ player.seekTo(nextWindowIndex, C.TIME_UNSET);
+ }
+ }
+
+ @Override
+ public void onSetShuffleModeEnabled(Player player, boolean enabled) {
+ // TODO: Implement this.
+ }
+
+ private void publishFloatingQueueWindow(Player player) {
+ if (player.getCurrentTimeline().isEmpty()) {
+ mediaSession.setQueue(Collections.emptyList());
+ activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
+ return;
+ }
+ int windowCount = player.getCurrentTimeline().getWindowCount();
+ int currentWindowIndex = player.getCurrentWindowIndex();
+ int queueSize = Math.min(maxQueueSize, windowCount);
+ int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
+ windowCount - queueSize);
+ List queue = new ArrayList<>();
+ for (int i = startIndex; i < startIndex + queueSize; i++) {
+ queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(i), i));
+ }
+ mediaSession.setQueue(queue);
+ activeQueueItemId = currentWindowIndex;
+ }
+
+}
diff --git a/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml
new file mode 100644
index 0000000000..dad37fa1f0
--- /dev/null
+++ b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml
new file mode 100644
index 0000000000..132eae0d76
--- /dev/null
+++ b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml
new file mode 100644
index 0000000000..d51010566a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..2824e7847c
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..0b92f583da
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..232aa2b1cd
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..5c91a47519
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..a94abd864f
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..a59a985239
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..97f7e1cc75
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..6a02321702
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..59bac33705
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..2baaedecbf
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..2468f92f9f
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..4e1d53db77
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..d7207ebc0d
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..4d6253ead6
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..d577f4ebcd
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/values-af/strings.xml b/extensions/mediasession/src/main/res/values-af/strings.xml
new file mode 100644
index 0000000000..4ef78cd84f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-af/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Herhaal alles"
+ "Herhaal niks"
+ "Herhaal een"
+
diff --git a/extensions/mediasession/src/main/res/values-am/strings.xml b/extensions/mediasession/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..531f605584
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-am/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ሁሉንም ድገም"
+ "ምንም አትድገም"
+ "አንዱን ድገም"
+
diff --git a/extensions/mediasession/src/main/res/values-ar/strings.xml b/extensions/mediasession/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..0101a746e0
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ar/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "تكرار الكل"
+ "عدم التكرار"
+ "تكرار مقطع واحد"
+
diff --git a/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml b/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml
new file mode 100644
index 0000000000..34408143fa
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Bütün təkrarlayın"
+ "Təkrar bir"
+ "Heç bir təkrar"
+
diff --git a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000000..67a51cf85e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ponovi sve"
+ "Ne ponavljaj nijednu"
+ "Ponovi jednu"
+
diff --git a/extensions/mediasession/src/main/res/values-be-rBY/strings.xml b/extensions/mediasession/src/main/res/values-be-rBY/strings.xml
new file mode 100644
index 0000000000..2f05607235
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-be-rBY/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Паўтарыць усё"
+ "Паўтараць ні"
+ "Паўтарыць адзін"
+
diff --git a/extensions/mediasession/src/main/res/values-bg/strings.xml b/extensions/mediasession/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..16910d640a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-bg/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Повтаряне на всички"
+ "Без повтаряне"
+ "Повтаряне на един елемент"
+
diff --git a/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml b/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000000..8872b464c6
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "সবগুলির পুনরাবৃত্তি করুন"
+ "একটিরও পুনরাবৃত্তি করবেন না"
+ "একটির পুনরাবৃত্তি করুন"
+
diff --git a/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml b/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml
new file mode 100644
index 0000000000..d0bf068573
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ponovite sve"
+ "Ne ponavljaju"
+ "Ponovite jedan"
+
diff --git a/extensions/mediasession/src/main/res/values-ca/strings.xml b/extensions/mediasession/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..89414d736e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ca/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repeteix-ho tot"
+ "No en repeteixis cap"
+ "Repeteix-ne un"
+
diff --git a/extensions/mediasession/src/main/res/values-cs/strings.xml b/extensions/mediasession/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..784d872570
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-cs/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Opakovat vše"
+ "Neopakovat"
+ "Opakovat jednu položku"
+
diff --git a/extensions/mediasession/src/main/res/values-da/strings.xml b/extensions/mediasession/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..2c9784d122
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-da/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Gentag alle"
+ "Gentag ingen"
+ "Gentag en"
+
diff --git a/extensions/mediasession/src/main/res/values-de/strings.xml b/extensions/mediasession/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..c11e449665
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-de/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Alle wiederholen"
+ "Keinen Titel wiederholen"
+ "Einen Titel wiederholen"
+
diff --git a/extensions/mediasession/src/main/res/values-el/strings.xml b/extensions/mediasession/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..6279af5d64
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-el/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Επανάληψη όλων"
+ "Καμία επανάληψη"
+ "Επανάληψη ενός στοιχείου"
+
diff --git a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000000..a3fccf8b52
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repeat all"
+ "Repeat none"
+ "Repeat one"
+
diff --git a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..a3fccf8b52
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repeat all"
+ "Repeat none"
+ "Repeat one"
+
diff --git a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000000..a3fccf8b52
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repeat all"
+ "Repeat none"
+ "Repeat one"
+
diff --git a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000000..0fe29d3d5a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir todo"
+ "No repetir"
+ "Repetir uno"
+
diff --git a/extensions/mediasession/src/main/res/values-es/strings.xml b/extensions/mediasession/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..0fe29d3d5a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-es/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir todo"
+ "No repetir"
+ "Repetir uno"
+
diff --git a/extensions/mediasession/src/main/res/values-et-rEE/strings.xml b/extensions/mediasession/src/main/res/values-et-rEE/strings.xml
new file mode 100644
index 0000000000..1bc3b59706
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-et-rEE/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Korda kõike"
+ "Ära korda midagi"
+ "Korda ühte"
+
diff --git a/extensions/mediasession/src/main/res/values-eu-rES/strings.xml b/extensions/mediasession/src/main/res/values-eu-rES/strings.xml
new file mode 100644
index 0000000000..f15f03160f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-eu-rES/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Errepikatu guztiak"
+ "Ez errepikatu"
+ "Errepikatu bat"
+
diff --git a/extensions/mediasession/src/main/res/values-fa/strings.xml b/extensions/mediasession/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..e37a08de64
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-fa/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "تکرار همه"
+ "تکرار هیچکدام"
+ "یکبار تکرار"
+
diff --git a/extensions/mediasession/src/main/res/values-fi/strings.xml b/extensions/mediasession/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..c920827976
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-fi/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Toista kaikki"
+ "Toista ei mitään"
+ "Toista yksi"
+
diff --git a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000000..c5191e74a9
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Tout lire en boucle"
+ "Aucune répétition"
+ "Répéter un élément"
+
diff --git a/extensions/mediasession/src/main/res/values-fr/strings.xml b/extensions/mediasession/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..1d76358d1f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-fr/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Tout lire en boucle"
+ "Ne rien lire en boucle"
+ "Lire en boucle un élément"
+
diff --git a/extensions/mediasession/src/main/res/values-gl-rES/strings.xml b/extensions/mediasession/src/main/res/values-gl-rES/strings.xml
new file mode 100644
index 0000000000..6b65b3e843
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-gl-rES/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir todo"
+ "Non repetir"
+ "Repetir un"
+
diff --git a/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml b/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..0eb9cab37e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "બધા પુનરાવર્તન કરો"
+ "કંઈ પુનરાવર્તન કરો"
+ "એક પુનરાવર્તન કરો"
+
diff --git a/extensions/mediasession/src/main/res/values-hi/strings.xml b/extensions/mediasession/src/main/res/values-hi/strings.xml
new file mode 100644
index 0000000000..8ce336d5e5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-hi/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "सभी को दोहराएं"
+ "कुछ भी न दोहराएं"
+ "एक दोहराएं"
+
diff --git a/extensions/mediasession/src/main/res/values-hr/strings.xml b/extensions/mediasession/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..9f995ec15b
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-hr/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ponovi sve"
+ "Bez ponavljanja"
+ "Ponovi jedno"
+
diff --git a/extensions/mediasession/src/main/res/values-hu/strings.xml b/extensions/mediasession/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..2335ade72e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-hu/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Összes ismétlése"
+ "Nincs ismétlés"
+ "Egy ismétlése"
+
diff --git a/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml b/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..19a89e6c87
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "կրկնել այն ամենը"
+ "Չկրկնել"
+ "Կրկնել մեկը"
+
diff --git a/extensions/mediasession/src/main/res/values-in/strings.xml b/extensions/mediasession/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..093a7f8576
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-in/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ulangi Semua"
+ "Jangan Ulangi"
+ "Ulangi Satu"
+
diff --git a/extensions/mediasession/src/main/res/values-is-rIS/strings.xml b/extensions/mediasession/src/main/res/values-is-rIS/strings.xml
new file mode 100644
index 0000000000..b200abbdb2
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-is-rIS/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Endurtaka allt"
+ "Endurtaka ekkert"
+ "Endurtaka eitt"
+
diff --git a/extensions/mediasession/src/main/res/values-it/strings.xml b/extensions/mediasession/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..c0682519f9
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-it/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ripeti tutti"
+ "Non ripetere nessuno"
+ "Ripeti uno"
+
diff --git a/extensions/mediasession/src/main/res/values-iw/strings.xml b/extensions/mediasession/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..5cf23d5a4c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-iw/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "חזור על הכל"
+ "אל תחזור על כלום"
+ "חזור על פריט אחד"
+
diff --git a/extensions/mediasession/src/main/res/values-ja/strings.xml b/extensions/mediasession/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..6f543fbdee
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ja/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "全曲を繰り返し"
+ "繰り返しなし"
+ "1曲を繰り返し"
+
diff --git a/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml b/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml
new file mode 100644
index 0000000000..96656612a7
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "გამეორება ყველა"
+ "გაიმეორეთ არცერთი"
+ "გაიმეორეთ ერთი"
+
diff --git a/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml b/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml
new file mode 100644
index 0000000000..be4140120d
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Барлығын қайталау"
+ "Ешқайсысын қайталамау"
+ "Біреуін қайталау"
+
diff --git a/extensions/mediasession/src/main/res/values-km-rKH/strings.xml b/extensions/mediasession/src/main/res/values-km-rKH/strings.xml
new file mode 100644
index 0000000000..dd4b734e30
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-km-rKH/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ធ្វើម្ដងទៀតទាំងអស់"
+ "មិនធ្វើឡើងវិញ"
+ "ធ្វើឡើងវិញម្ដង"
+
diff --git a/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml b/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml
new file mode 100644
index 0000000000..3d79aca9e2
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ"
+ "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ"
+ "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ"
+
diff --git a/extensions/mediasession/src/main/res/values-ko/strings.xml b/extensions/mediasession/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..d269937771
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ko/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "전체 반복"
+ "반복 안함"
+ "한 항목 반복"
+
diff --git a/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml b/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml
new file mode 100644
index 0000000000..a8978ecc61
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Баарын кайталоо"
+ "Эч бирин кайталабоо"
+ "Бирөөнү кайталоо"
+
diff --git a/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml b/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml
new file mode 100644
index 0000000000..950a9ba097
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ຫຼິ້ນຊ້ຳທັງໝົດ"
+ "ບໍ່ຫຼິ້ນຊ້ຳ"
+ "ຫຼິ້ນຊ້ຳ"
+
diff --git a/extensions/mediasession/src/main/res/values-lt/strings.xml b/extensions/mediasession/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..ae8f1cf8c3
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-lt/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Kartoti viską"
+ "Nekartoti nieko"
+ "Kartoti vieną"
+
diff --git a/extensions/mediasession/src/main/res/values-lv/strings.xml b/extensions/mediasession/src/main/res/values-lv/strings.xml
new file mode 100644
index 0000000000..a69f6a0ad5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-lv/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Atkārtot visu"
+ "Neatkārtot nevienu"
+ "Atkārtot vienu"
+
diff --git a/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml b/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml
new file mode 100644
index 0000000000..ddf2a60c20
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Повтори ги сите"
+ "Не повторувај ниту една"
+ "Повтори една"
+
diff --git a/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml
new file mode 100644
index 0000000000..6f869e2931
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "എല്ലാം ആവർത്തിക്കുക"
+ "ഒന്നും ആവർത്തിക്കരുത്"
+ "ഒന്ന് ആവർത്തിക്കുക"
+
diff --git a/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml b/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml
new file mode 100644
index 0000000000..8d3074b91a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Бүгдийг давтах"
+ "Алийг нь ч давтахгүй"
+ "Нэгийг давтах"
+
diff --git a/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml b/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml
new file mode 100644
index 0000000000..6e4bfccc16
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "सर्व पुनरावृत्ती करा"
+ "काहीही पुनरावृत्ती करू नका"
+ "एक पुनरावृत्ती करा"
+
diff --git a/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml b/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml
new file mode 100644
index 0000000000..829542b668
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ulang semua"
+ "Tiada ulangan"
+ "Ulangan"
+
diff --git a/extensions/mediasession/src/main/res/values-my-rMM/strings.xml b/extensions/mediasession/src/main/res/values-my-rMM/strings.xml
new file mode 100644
index 0000000000..aeb1375ebf
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-my-rMM/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "အားလုံး ထပ်တလဲလဲဖွင့်ရန်"
+ "ထပ်တလဲလဲမဖွင့်ရန်"
+ "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်"
+
diff --git a/extensions/mediasession/src/main/res/values-nb/strings.xml b/extensions/mediasession/src/main/res/values-nb/strings.xml
new file mode 100644
index 0000000000..10f334b226
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-nb/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Gjenta alle"
+ "Ikke gjenta noen"
+ "Gjenta én"
+
diff --git a/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml b/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..6d81ce5684
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "सबै दोहोर्याउनुहोस्"
+ "कुनै पनि नदोहोर्याउनुहोस्"
+ "एउटा दोहोर्याउनुहोस्"
+
diff --git a/extensions/mediasession/src/main/res/values-nl/strings.xml b/extensions/mediasession/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..55997be098
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-nl/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Alles herhalen"
+ "Niet herhalen"
+ "Eén herhalen"
+
diff --git a/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml b/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..8eee0bee16
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ਸਭ ਨੂੰ ਦੁਹਰਾਓ"
+ "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ"
+ "ਇੱਕ ਦੁਹਰਾਓ"
+
diff --git a/extensions/mediasession/src/main/res/values-pl/strings.xml b/extensions/mediasession/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..6a52d58b63
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pl/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Powtórz wszystkie"
+ "Nie powtarzaj"
+ "Powtórz jeden"
+
diff --git a/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml b/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..efb8fc433f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir tudo"
+ "Não repetir"
+ "Repetir um"
+
diff --git a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..efb8fc433f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir tudo"
+ "Não repetir"
+ "Repetir um"
+
diff --git a/extensions/mediasession/src/main/res/values-pt/strings.xml b/extensions/mediasession/src/main/res/values-pt/strings.xml
new file mode 100644
index 0000000000..aadebbb3b0
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pt/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir tudo"
+ "Não repetir"
+ "Repetir uma"
+
diff --git a/extensions/mediasession/src/main/res/values-ro/strings.xml b/extensions/mediasession/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..f6aee447e5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ro/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetați toate"
+ "Repetați niciuna"
+ "Repetați unul"
+
diff --git a/extensions/mediasession/src/main/res/values-ru/strings.xml b/extensions/mediasession/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..575ad9f930
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ru/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Повторять все"
+ "Не повторять"
+ "Повторять один элемент"
+
diff --git a/extensions/mediasession/src/main/res/values-si-rLK/strings.xml b/extensions/mediasession/src/main/res/values-si-rLK/strings.xml
new file mode 100644
index 0000000000..8e172ac268
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-si-rLK/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "සියලු නැවත"
+ "කිසිවක් නැවත"
+ "නැවත නැවත එක්"
+
diff --git a/extensions/mediasession/src/main/res/values-sk/strings.xml b/extensions/mediasession/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..5d092003e5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sk/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Opakovať všetko"
+ "Neopakovať"
+ "Opakovať jednu položku"
+
diff --git a/extensions/mediasession/src/main/res/values-sl/strings.xml b/extensions/mediasession/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..ecac3800c8
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sl/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ponovi vse"
+ "Ne ponovi"
+ "Ponovi eno"
+
diff --git a/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml b/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml
new file mode 100644
index 0000000000..6da24cc4c7
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Përsërit të gjithë"
+ "Përsëritni asnjë"
+ "Përsëritni një"
+
diff --git a/extensions/mediasession/src/main/res/values-sr/strings.xml b/extensions/mediasession/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..881cb2703b
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sr/strings.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/res/values-sv/strings.xml b/extensions/mediasession/src/main/res/values-sv/strings.xml
new file mode 100644
index 0000000000..3a7bb630aa
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sv/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Upprepa alla"
+ "Upprepa inga"
+ "Upprepa en"
+
diff --git a/extensions/mediasession/src/main/res/values-sw/strings.xml b/extensions/mediasession/src/main/res/values-sw/strings.xml
new file mode 100644
index 0000000000..726012ab88
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sw/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Rudia zote"
+ "Usirudie Yoyote"
+ "Rudia Moja"
+
diff --git a/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml
new file mode 100644
index 0000000000..9364bc0be2
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "அனைத்தையும் மீண்டும் இயக்கு"
+ "எதையும் மீண்டும் இயக்காதே"
+ "ஒன்றை மட்டும் மீண்டும் இயக்கு"
+
diff --git a/extensions/mediasession/src/main/res/values-te-rIN/strings.xml b/extensions/mediasession/src/main/res/values-te-rIN/strings.xml
new file mode 100644
index 0000000000..b7ee7345d5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-te-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "అన్నీ పునరావృతం చేయి"
+ "ఏదీ పునరావృతం చేయవద్దు"
+ "ఒకదాన్ని పునరావృతం చేయి"
+
diff --git a/extensions/mediasession/src/main/res/values-th/strings.xml b/extensions/mediasession/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..af502b3a4c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-th/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "เล่นซ้ำทั้งหมด"
+ "ไม่เล่นซ้ำ"
+ "เล่นซ้ำรายการเดียว"
+
diff --git a/extensions/mediasession/src/main/res/values-tl/strings.xml b/extensions/mediasession/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..239972a4c7
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-tl/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ulitin Lahat"
+ "Walang Uulitin"
+ "Ulitin ang Isa"
+
diff --git a/extensions/mediasession/src/main/res/values-tr/strings.xml b/extensions/mediasession/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..89a98b1ed9
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-tr/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Tümünü Tekrarla"
+ "Hiçbirini Tekrarlama"
+ "Birini Tekrarla"
+
diff --git a/extensions/mediasession/src/main/res/values-uk/strings.xml b/extensions/mediasession/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..4e1d25eb8a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-uk/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Повторити все"
+ "Не повторювати"
+ "Повторити один елемент"
+
diff --git a/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml b/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml
new file mode 100644
index 0000000000..ab2631a4ec
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "سبھی کو دہرائیں"
+ "کسی کو نہ دہرائیں"
+ "ایک کو دہرائیں"
+
diff --git a/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml b/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml
new file mode 100644
index 0000000000..c32d00af8e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Barchasini takrorlash"
+ "Takrorlamaslik"
+ "Bir marta takrorlash"
+
diff --git a/extensions/mediasession/src/main/res/values-vi/strings.xml b/extensions/mediasession/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..dabc9e05d5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-vi/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Lặp lại tất cả"
+ "Không lặp lại"
+ "Lặp lại một mục"
+
diff --git a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..beb3403cb9
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "重复播放全部"
+ "不重复播放"
+ "重复播放单个视频"
+
diff --git a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000000..775cd6441c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "重複播放所有媒體項目"
+ "不重複播放任何媒體項目"
+ "重複播放一個媒體項目"
+
diff --git a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..d3789f4145
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "重複播放所有媒體項目"
+ "不重複播放"
+ "重複播放單一媒體項目"
+
diff --git a/extensions/mediasession/src/main/res/values-zu/strings.xml b/extensions/mediasession/src/main/res/values-zu/strings.xml
new file mode 100644
index 0000000000..789b6fecb4
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-zu/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Phinda konke"
+ "Ungaphindi lutho"
+ "Phida okukodwa"
+
diff --git a/extensions/mediasession/src/main/res/values/strings.xml b/extensions/mediasession/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..72a67ff01c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Repeat none
+ Repeat one
+ Repeat all
+
diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md
index d84dcb44ec..b10c4ba629 100644
--- a/extensions/okhttp/README.md
+++ b/extensions/okhttp/README.md
@@ -1,23 +1,16 @@
-# ExoPlayer OkHttp Extension #
+# ExoPlayer OkHttp extension #
## Description ##
-The OkHttp Extension is an [HttpDataSource][] implementation using Square's
+The OkHttp extension is an [HttpDataSource][] implementation using Square's
[OkHttp][].
-## Using the extension ##
+[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
+[OkHttp]: https://square.github.io/okhttp/
-The easiest way to use the extension is to add it as a gradle dependency. You
-need to make sure you have the jcenter repository included in the `build.gradle`
-file in the root of your project:
+## Getting the extension ##
-```gradle
-repositories {
- jcenter()
-}
-```
-
-Next, include the following in your module's `build.gradle` file:
+The easiest way to use the extension is to add it as a gradle dependency:
```gradle
compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
@@ -26,5 +19,33 @@ compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used.
-[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
-[OkHttp]: https://square.github.io/okhttp/
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+
+## Using the extension ##
+
+ExoPlayer requests data through `DataSource` instances. These instances are
+either instantiated and injected from application code, or obtained from
+instances of `DataSource.Factory` that are instantiated and injected from
+application code.
+
+If your application only needs to play http(s) content, using the OkHttp
+extension is as simple as updating any `DataSource`s and `DataSource.Factory`
+instantiations in your application code to use `OkHttpDataSource` and
+`OkHttpDataSourceFactory` respectively. If your application also needs to play
+non-http(s) content such as local files, use
+```
+new DefaultDataSource(
+ ...
+ new OkHttpDataSource(...) /* baseDataSource argument */);
+```
+and
+```
+new DefaultDataSourceFactory(
+ ...
+ new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */);
+```
+respectively.
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
index f47f1a8556..bc9e0eba3e 100644
--- a/extensions/okhttp/build.gradle
+++ b/extensions/okhttp/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -29,8 +30,8 @@ android {
}
dependencies {
- compile project(':library-core')
- compile('com.squareup.okhttp3:okhttp:3.6.0') {
+ compile project(modulePrefix + 'library-core')
+ compile('com.squareup.okhttp3:okhttp:3.8.1') {
exclude group: 'org.json'
}
}
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
index 167fc68e86..0519673e50 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
@@ -19,6 +19,7 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@@ -45,6 +46,10 @@ import okhttp3.Response;
*/
public class OkHttpDataSource implements HttpDataSource {
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.okhttp");
+ }
+
private static final AtomicReference skipBufferReference = new AtomicReference<>();
@NonNull private final Call.Factory callFactory;
diff --git a/extensions/opus/README.md b/extensions/opus/README.md
index 36ca2b7261..e5f5bcb168 100644
--- a/extensions/opus/README.md
+++ b/extensions/opus/README.md
@@ -1,20 +1,19 @@
-# ExoPlayer Opus Extension #
+# ExoPlayer Opus extension #
## Description ##
-The Opus Extension is a [Renderer][] implementation that helps you bundle
+The Opus extension is a [Renderer][] implementation that helps you bundle
libopus (the Opus decoding library) into your app and use it along with
ExoPlayer to play Opus audio on Android devices.
[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html
-## Build Instructions ##
+## Build instructions ##
-* Checkout ExoPlayer along with Extensions:
-
-```
-git clone https://github.com/google/ExoPlayer.git
-```
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][]. In addition, it's necessary to build the extension's
+native components as follows:
* Set the following environment variables:
@@ -26,8 +25,6 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main"
* Download the [Android NDK][] and set its location in an environment variable:
-[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-
```
NDK_PATH=""
```
@@ -52,23 +49,8 @@ cd "${OPUS_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4
```
-* In your project, you can add a dependency to the Opus Extension by using a
-rule like this:
-
-```
-// in settings.gradle
-include ':..:ExoPlayer:library'
-include ':..:ExoPlayer:extension-opus'
-
-// in build.gradle
-dependencies {
- compile project(':..:ExoPlayer:library')
- compile project(':..:ExoPlayer:extension-opus')
-}
-```
-
-* Now, when you build your app, the Opus extension will be built and the native
- libraries will be packaged along with the APK.
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
## Notes ##
diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle
index 31d5450fdd..41b428070f 100644
--- a/extensions/opus/build.gradle
+++ b/extensions/opus/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -30,7 +31,7 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
}
ext {
diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml
index c819529692..e77590dc65 100644
--- a/extensions/opus/src/androidTest/AndroidManifest.xml
+++ b/extensions/opus/src/androidTest/AndroidManifest.xml
@@ -28,7 +28,6 @@
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
index 263934d982..4c576b2cc0 100644
--- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
+++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
@@ -57,7 +58,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
}
}
- private static class TestPlaybackThread extends Thread implements ExoPlayer.EventListener {
+ private static class TestPlaybackThread extends Thread implements Player.EventListener {
private final Context context;
private final Uri uri;
@@ -120,12 +121,17 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
- if (playbackState == ExoPlayer.STATE_ENDED
- || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) {
+ if (playbackState == Player.STATE_ENDED
+ || (playbackState == Player.STATE_IDLE && playbackException != null)) {
releasePlayerAndQuitLooper();
}
}
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
private void releasePlayerAndQuitLooper() {
player.release();
Looper.myLooper().quit();
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
index 564a41fc77..730473ddad 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.opus;
import android.os.Handler;
+import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
@@ -32,6 +33,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
private static final int NUM_BUFFERS = 16;
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
+ private OpusDecoder decoder;
+
public LibopusAudioRenderer() {
this(null, null);
}
@@ -51,6 +54,13 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param drmSessionManager For use with encrypted media. May be null if support for encrypted
+ * media is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisition. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
@@ -69,8 +79,16 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws OpusDecoderException {
- return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
+ decoder = new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
format.initializationData, mediaCrypto);
+ return decoder;
+ }
+
+ @Override
+ protected Format getOutputFormat() {
+ return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
+ Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT,
+ null, null, 0, null);
}
}
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
index 95c38c34bb..b4a4622346 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
@@ -197,6 +197,20 @@ import java.util.List;
opusClose(nativeDecoderContext);
}
+ /**
+ * Returns the channel count of output audio.
+ */
+ public int getChannelCount() {
+ return channelCount;
+ }
+
+ /**
+ * Returns the sample rate of output audio.
+ */
+ public int getSampleRate() {
+ return SAMPLE_RATE;
+ }
+
private static int nsToSamples(long ns) {
return (int) (ns * SAMPLE_RATE / 1000000000);
}
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
index 41a28b9fd7..22985ea497 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.opus;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
/**
@@ -22,6 +23,10 @@ import com.google.android.exoplayer2.util.LibraryLoader;
*/
public final class OpusLibrary {
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.opus");
+ }
+
private static final LibraryLoader LOADER = new LibraryLoader("opus", "opusJNI");
private OpusLibrary() {}
@@ -30,6 +35,8 @@ public final class OpusLibrary {
* Override the names of the Opus native libraries. If an application wishes to call this method,
* it must do so before calling any other method defined by this class, and before instantiating a
* {@link LibopusAudioRenderer} instance.
+ *
+ * @param libraries The names of the Opus native libraries.
*/
public static void setLibraries(String... libraries) {
LOADER.setLibraries(libraries);
diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md
new file mode 100644
index 0000000000..042d7078dc
--- /dev/null
+++ b/extensions/rtmp/README.md
@@ -0,0 +1,27 @@
+# ExoPlayer RTMP extension #
+
+## Description ##
+
+The RTMP extension is a [DataSource][] implementation for playing [RTMP][]
+streams using [LibRtmp Client for Android][].
+
+[DataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html
+[RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
+[LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android
+
+## Using the extension ##
+
+The easiest way to use the extension is to add it as a gradle dependency:
+
+```gradle
+compile 'com.google.android.exoplayer:extension-rtmp:rX.X.X'
+```
+
+where `rX.X.X` is the version, which must match the version of the ExoPlayer
+library being used.
+
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle
new file mode 100644
index 0000000000..c832cb82e9
--- /dev/null
+++ b/extensions/rtmp/build.gradle
@@ -0,0 +1,41 @@
+// Copyright (C) 2016 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.
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+ buildToolsVersion project.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion 15
+ targetSdkVersion project.ext.targetSdkVersion
+ }
+}
+
+dependencies {
+ compile project(modulePrefix + 'library-core')
+ compile 'net.butterflytv.utils:rtmp-client:0.2.8'
+}
+
+ext {
+ javadocTitle = 'RTMP extension'
+}
+apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'extension-rtmp'
+ releaseDescription = 'RTMP extension for ExoPlayer.'
+}
+apply from: '../../publish.gradle'
diff --git a/extensions/rtmp/src/main/AndroidManifest.xml b/extensions/rtmp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..7c5e92c198
--- /dev/null
+++ b/extensions/rtmp/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
new file mode 100644
index 0000000000..0601af4a2f
--- /dev/null
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.rtmp;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.TransferListener;
+import java.io.IOException;
+import net.butterflytv.rtmp_client.RtmpClient;
+import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException;
+
+/**
+ * A Real-Time Messaging Protocol (RTMP) {@link DataSource}.
+ */
+public final class RtmpDataSource implements DataSource {
+
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp");
+ }
+
+ @Nullable private final TransferListener super RtmpDataSource> listener;
+
+ private RtmpClient rtmpClient;
+ private Uri uri;
+
+ public RtmpDataSource() {
+ this(null);
+ }
+
+ /**
+ * @param listener An optional listener.
+ */
+ public RtmpDataSource(@Nullable TransferListener super RtmpDataSource> listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws RtmpIOException {
+ rtmpClient = new RtmpClient();
+ rtmpClient.open(dataSpec.uri.toString(), false);
+
+ this.uri = dataSpec.uri;
+ if (listener != null) {
+ listener.onTransferStart(this, dataSpec);
+ }
+ return C.LENGTH_UNSET;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ int bytesRead = rtmpClient.read(buffer, offset, readLength);
+ if (bytesRead == -1) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (listener != null) {
+ listener.onBytesTransferred(this, bytesRead);
+ }
+ return bytesRead;
+ }
+
+ @Override
+ public void close() {
+ if (uri != null) {
+ uri = null;
+ if (listener != null) {
+ listener.onTransferEnd(this);
+ }
+ }
+ if (rtmpClient != null) {
+ rtmpClient.close();
+ rtmpClient = null;
+ }
+ }
+
+ @Override
+ public Uri getUri() {
+ return uri;
+ }
+
+}
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
new file mode 100644
index 0000000000..0510e9c7da
--- /dev/null
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.rtmp;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+import com.google.android.exoplayer2.upstream.TransferListener;
+
+/**
+ * A {@link Factory} that produces {@link RtmpDataSource}.
+ */
+public final class RtmpDataSourceFactory implements DataSource.Factory {
+
+ @Nullable
+ private final TransferListener super RtmpDataSource> listener;
+
+ public RtmpDataSourceFactory() {
+ this(null);
+ }
+
+ /**
+ * @param listener An optional listener.
+ */
+ public RtmpDataSourceFactory(@Nullable TransferListener super RtmpDataSource> listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public DataSource createDataSource() {
+ return new RtmpDataSource(listener);
+ }
+
+}
diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md
index 53ef4b0bfd..87c5c8d54f 100644
--- a/extensions/vp9/README.md
+++ b/extensions/vp9/README.md
@@ -1,20 +1,19 @@
-# ExoPlayer VP9 Extension #
+# ExoPlayer VP9 extension #
## Description ##
-The VP9 Extension is a [Renderer][] implementation that helps you bundle libvpx
+The VP9 extension is a [Renderer][] implementation that helps you bundle libvpx
(the VP9 decoding library) into your app and use it along with ExoPlayer to play
VP9 video on Android devices.
[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html
-## Build Instructions ##
+## Build instructions ##
-* Checkout ExoPlayer along with Extensions:
-
-```
-git clone https://github.com/google/ExoPlayer.git
-```
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][]. In addition, it's necessary to build the extension's
+native components as follows:
* Set the following environment variables:
@@ -26,8 +25,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
* Download the [Android NDK][] and set its location in an environment variable:
-[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-
```
NDK_PATH=""
```
@@ -66,23 +63,8 @@ cd "${VP9_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4
```
-* In your project, you can add a dependency to the VP9 Extension by using a the
- following rule:
-
-```
-// in settings.gradle
-include ':..:ExoPlayer:library'
-include ':..:ExoPlayer:extension-vp9'
-
-// in build.gradle
-dependencies {
- compile project(':..:ExoPlayer:library')
- compile project(':..:ExoPlayer:extension-vp9')
-}
-```
-
-* Now, when you build your app, the VP9 extension will be built and the native
- libraries will be packaged along with the APK.
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
## Notes ##
@@ -94,4 +76,3 @@ dependencies {
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
please note that `generate_libvpx_android_configs.sh` and the makefiles need
to be modified to work with arbitrary versions of libvpx and libyuv.
-
diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle
index 5068586a4a..de6dc65f74 100644
--- a/extensions/vp9/build.gradle
+++ b/extensions/vp9/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -30,7 +31,7 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
}
ext {
diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml
index d9fa8af2c3..b8b28fc346 100644
--- a/extensions/vp9/src/androidTest/AndroidManifest.xml
+++ b/extensions/vp9/src/androidTest/AndroidManifest.xml
@@ -28,7 +28,6 @@
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
index 2647776b74..0bc945174e 100644
--- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
+++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
@@ -86,7 +87,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
}
}
- private static class TestPlaybackThread extends Thread implements ExoPlayer.EventListener {
+ private static class TestPlaybackThread extends Thread implements Player.EventListener {
private final Context context;
private final Uri uri;
@@ -152,12 +153,17 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
- if (playbackState == ExoPlayer.STATE_ENDED
- || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) {
+ if (playbackState == Player.STATE_ENDED
+ || (playbackState == Player.STATE_IDLE && playbackException != null)) {
releasePlayerAndQuitLooper();
}
}
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
private void releasePlayerAndQuitLooper() {
player.release();
Looper.myLooper().quit();
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
index 29423547b6..a947378de5 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
@@ -20,6 +20,7 @@ import android.graphics.Canvas;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
+import android.support.annotation.IntDef;
import android.view.Surface;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
@@ -30,6 +31,7 @@ import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
@@ -38,12 +40,35 @@ import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* Decodes and renders video using the native VP9 decoder.
*/
public final class LibvpxVideoRenderer extends BaseRenderer {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
+ REINITIALIZATION_STATE_WAIT_END_OF_STREAM})
+ private @interface ReinitializationState {}
+ /**
+ * The decoder does not need to be re-initialized.
+ */
+ private static final int REINITIALIZATION_STATE_NONE = 0;
+ /**
+ * The input format has changed in a way that requires the decoder to be re-initialized, but we
+ * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to
+ * ensure that it outputs any remaining buffers before we release it.
+ */
+ private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
+ /**
+ * The input format has changed in a way that requires the decoder to be re-initialized, and we've
+ * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an
+ * end of stream signal to indicate that it has output any remaining buffers before we release it.
+ */
+ private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
+
/**
* The type of a message that can be passed to an instance of this class via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
@@ -52,11 +77,18 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE;
/**
- * The number of input buffers and the number of output buffers. The renderer may limit the
- * minimum possible value due to requiring multiple output buffers to be dequeued at a time for it
- * to make progress.
+ * The number of input buffers.
+ */
+ private static final int NUM_INPUT_BUFFERS = 8;
+ /**
+ * The number of output buffers. The renderer may limit the minimum possible value due to
+ * requiring multiple output buffers to be dequeued at a time for it to make progress.
+ */
+ private static final int NUM_OUTPUT_BUFFERS = 16;
+ /**
+ * The initial input buffer size. Input buffers are reallocated dynamically if this value is
+ * insufficient.
*/
- private static final int NUM_BUFFERS = 16;
private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
private final boolean scaleToFit;
@@ -71,12 +103,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private DecoderCounters decoderCounters;
private Format format;
private VpxDecoder decoder;
- private DecoderInputBuffer inputBuffer;
+ private VpxInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer;
private VpxOutputBuffer nextOutputBuffer;
private DrmSession drmSession;
private DrmSession pendingDrmSession;
+ @ReinitializationState
+ private int decoderReinitializationState;
+ private boolean decoderReceivedBuffers;
+
private Bitmap bitmap;
private boolean renderedFirstFrame;
private long joiningDeadlineMs;
@@ -153,6 +189,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
outputMode = VpxDecoder.OUTPUT_MODE_NONE;
+ decoderReinitializationState = REINITIALIZATION_STATE_NONE;
}
@Override
@@ -185,49 +222,25 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
- // We have a format.
- drmSession = pendingDrmSession;
- ExoMediaCrypto mediaCrypto = null;
- if (drmSession != null) {
- int drmSessionState = drmSession.getState();
- if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
- } else if (drmSessionState == DrmSession.STATE_OPENED
- || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
- mediaCrypto = drmSession.getMediaCrypto();
- } else {
- // The drm session isn't open yet.
- return;
- }
- }
- try {
- if (decoder == null) {
- // If we don't have a decoder yet, we need to instantiate one.
- long codecInitializingTimestamp = SystemClock.elapsedRealtime();
- TraceUtil.beginSection("createVpxDecoder");
- decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
- decoder.setOutputMode(outputMode);
+ // If we don't have a decoder yet, we need to instantiate one.
+ maybeInitDecoder();
+
+ if (decoder != null) {
+ try {
+ // Rendering loop.
+ TraceUtil.beginSection("drainAndFeed");
+ while (drainOutputBuffer(positionUs)) {}
+ while (feedInputBuffer()) {}
TraceUtil.endSection();
- long codecInitializedTimestamp = SystemClock.elapsedRealtime();
- eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
- codecInitializedTimestamp - codecInitializingTimestamp);
- decoderCounters.decoderInitCount++;
+ } catch (VpxDecoderException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
}
- TraceUtil.beginSection("drainAndFeed");
- while (drainOutputBuffer(positionUs)) {}
- while (feedInputBuffer()) {}
- TraceUtil.endSection();
- } catch (VpxDecoderException e) {
- throw ExoPlaybackException.createForRenderer(e, getIndex());
+ decoderCounters.ensureUpdated();
}
- decoderCounters.ensureUpdated();
}
- private boolean drainOutputBuffer(long positionUs) throws VpxDecoderException {
- if (outputStreamEnded) {
- return false;
- }
-
+ private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException,
+ VpxDecoderException {
// Acquire outputBuffer either from nextOutputBuffer or from the decoder.
if (outputBuffer == null) {
if (nextOutputBuffer != null) {
@@ -247,15 +260,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
if (outputBuffer.isEndOfStream()) {
- outputStreamEnded = true;
- outputBuffer.release();
- outputBuffer = null;
+ if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
+ // We're waiting to re-initialize the decoder, and have now processed all final buffers.
+ releaseDecoder();
+ maybeInitDecoder();
+ } else {
+ outputBuffer.release();
+ outputBuffer = null;
+ outputStreamEnded = true;
+ }
return false;
}
if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
- if (outputBuffer.timeUs <= positionUs) {
+ if (isBufferLate(outputBuffer.timeUs - positionUs)) {
skipBuffer();
return true;
}
@@ -280,7 +299,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return false;
}
-
/**
* Returns whether the current frame should be dropped.
*
@@ -293,10 +311,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
*/
protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs,
long positionUs, long joiningDeadlineMs) {
- // Drop the frame if we're joining and are more than 30ms late, or if we have the next frame
- // and that's also late. Else we'll render what we have.
- return (joiningDeadlineMs != C.TIME_UNSET && outputBufferTimeUs < positionUs - 30000)
- || (nextOutputBufferTimeUs != C.TIME_UNSET && nextOutputBufferTimeUs < positionUs);
+ return isBufferLate(outputBufferTimeUs - positionUs)
+ && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
}
private void renderBuffer() {
@@ -356,7 +372,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
- if (inputStreamEnded) {
+ if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
+ || inputStreamEnded) {
+ // We need to reinitialize the decoder or the input stream has ended.
return false;
}
@@ -367,6 +385,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
+ if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
+ inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ decoder.queueInputBuffer(inputBuffer);
+ inputBuffer = null;
+ decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
+ return false;
+ }
+
int result;
if (waitingForKeys) {
// We've already read an encrypted sample into buffer, and are waiting for keys.
@@ -394,36 +420,43 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return false;
}
inputBuffer.flip();
+ inputBuffer.colorInfo = formatHolder.format.colorInfo;
decoder.queueInputBuffer(inputBuffer);
+ decoderReceivedBuffers = true;
decoderCounters.inputBufferCount++;
inputBuffer = null;
return true;
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null) {
+ if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
- int drmSessionState = drmSession.getState();
+ @DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
- return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
- && (bufferEncrypted || !playClearSamplesWithoutKeys);
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
- private void flushDecoder() {
- inputBuffer = null;
+ private void flushDecoder() throws ExoPlaybackException {
waitingForKeys = false;
- if (outputBuffer != null) {
- outputBuffer.release();
- outputBuffer = null;
+ if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
+ releaseDecoder();
+ maybeInitDecoder();
+ } else {
+ inputBuffer = null;
+ if (outputBuffer != null) {
+ outputBuffer.release();
+ outputBuffer = null;
+ }
+ if (nextOutputBuffer != null) {
+ nextOutputBuffer.release();
+ nextOutputBuffer = null;
+ }
+ decoder.flush();
+ decoderReceivedBuffers = false;
}
- if (nextOutputBuffer != null) {
- nextOutputBuffer.release();
- nextOutputBuffer = null;
- }
- decoder.flush();
}
@Override
@@ -461,7 +494,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
@Override
- protected void onPositionReset(long positionUs, boolean joining) {
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
inputStreamEnded = false;
outputStreamEnded = false;
clearRenderedFirstFrame();
@@ -490,8 +523,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
@Override
protected void onDisabled() {
- inputBuffer = null;
- outputBuffer = null;
format = null;
waitingForKeys = false;
clearReportedVideoSize();
@@ -518,20 +549,54 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
- private void releaseDecoder() {
+ private void maybeInitDecoder() throws ExoPlaybackException {
if (decoder != null) {
- decoder.release();
- decoder = null;
- decoderCounters.decoderReleaseCount++;
- waitingForKeys = false;
- if (drmSession != null && pendingDrmSession != drmSession) {
- try {
- drmSessionManager.releaseSession(drmSession);
- } finally {
- drmSession = null;
+ return;
+ }
+
+ drmSession = pendingDrmSession;
+ ExoMediaCrypto mediaCrypto = null;
+ if (drmSession != null) {
+ mediaCrypto = drmSession.getMediaCrypto();
+ if (mediaCrypto == null) {
+ DrmSessionException drmError = drmSession.getError();
+ if (drmError != null) {
+ throw ExoPlaybackException.createForRenderer(drmError, getIndex());
}
+ // The drm session isn't open yet.
+ return;
}
}
+
+ try {
+ long codecInitializingTimestamp = SystemClock.elapsedRealtime();
+ TraceUtil.beginSection("createVpxDecoder");
+ decoder = new VpxDecoder(NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
+ mediaCrypto);
+ decoder.setOutputMode(outputMode);
+ TraceUtil.endSection();
+ long codecInitializedTimestamp = SystemClock.elapsedRealtime();
+ eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
+ codecInitializedTimestamp - codecInitializingTimestamp);
+ decoderCounters.decoderInitCount++;
+ } catch (VpxDecoderException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+
+ private void releaseDecoder() {
+ if (decoder == null) {
+ return;
+ }
+
+ inputBuffer = null;
+ outputBuffer = null;
+ nextOutputBuffer = null;
+ decoder.release();
+ decoder = null;
+ decoderCounters.decoderReleaseCount++;
+ decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+ decoderReceivedBuffers = false;
}
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
@@ -555,6 +620,17 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
+ if (pendingDrmSession != drmSession) {
+ if (decoderReceivedBuffers) {
+ // Signal end of stream and wait for any final output buffers before re-initialization.
+ decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
+ } else {
+ // There aren't any final output buffers, so release the decoder immediately.
+ releaseDecoder();
+ maybeInitDecoder();
+ }
+ }
+
eventDispatcher.inputFormatChanged(format);
}
@@ -654,4 +730,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
+ private static boolean isBufferLate(long earlyUs) {
+ // Class a buffer as late if it should have been presented more than 30ms ago.
+ return earlyUs < -30000;
+ }
+
}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
index 73ec7c2f96..4bec5bdf4c 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
-import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
@@ -27,7 +26,7 @@ import java.nio.ByteBuffer;
* Vpx decoder.
*/
/* package */ final class VpxDecoder extends
- SimpleDecoder {
+ SimpleDecoder {
public static final int OUTPUT_MODE_NONE = -1;
public static final int OUTPUT_MODE_YUV = 0;
@@ -54,7 +53,7 @@ import java.nio.ByteBuffer;
*/
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
- super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
+ super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries.");
}
@@ -85,8 +84,8 @@ import java.nio.ByteBuffer;
}
@Override
- protected DecoderInputBuffer createInputBuffer() {
- return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
+ protected VpxInputBuffer createInputBuffer() {
+ return new VpxInputBuffer();
}
@Override
@@ -100,7 +99,7 @@ import java.nio.ByteBuffer;
}
@Override
- protected VpxDecoderException decode(DecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
+ protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
boolean reset) {
ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit();
@@ -128,6 +127,7 @@ import java.nio.ByteBuffer;
} else if (getFrameResult == -1) {
return new VpxDecoderException("Buffer initialization failed.");
}
+ outputBuffer.colorInfo = inputBuffer.colorInfo;
return null;
}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java
new file mode 100644
index 0000000000..fcae9dc6bc
--- /dev/null
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.vp9;
+
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.video.ColorInfo;
+
+/**
+ * Input buffer to a {@link VpxDecoder}.
+ */
+/* package */ final class VpxInputBuffer extends DecoderInputBuffer {
+
+ public ColorInfo colorInfo;
+
+ public VpxInputBuffer() {
+ super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
+ }
+
+}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
index 24331127ec..854576b4b2 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
/**
@@ -22,6 +23,10 @@ import com.google.android.exoplayer2.util.LibraryLoader;
*/
public final class VpxLibrary {
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.vpx");
+ }
+
private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxJNI");
private VpxLibrary() {}
@@ -30,6 +35,8 @@ public final class VpxLibrary {
* Override the names of the Vpx native libraries. If an application wishes to call this method,
* it must do so before calling any other method defined by this class, and before instantiating a
* {@link LibvpxVideoRenderer} instance.
+ *
+ * @param libraries The names of the Vpx native libraries.
*/
public static void setLibraries(String... libraries) {
LOADER.setLibraries(libraries);
@@ -70,4 +77,5 @@ public final class VpxLibrary {
private static native String vpxGetVersion();
private static native String vpxGetBuildConfig();
public static native boolean vpxIsSecureDecodeSupported();
+
}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
index db3cf49b0c..2618bf7c62 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.decoder.OutputBuffer;
+import com.google.android.exoplayer2.video.ColorInfo;
import java.nio.ByteBuffer;
/**
@@ -37,6 +38,8 @@ import java.nio.ByteBuffer;
public ByteBuffer data;
public int width;
public int height;
+ public ColorInfo colorInfo;
+
/**
* YUV planes for YUV mode.
*/
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java
index 8f43a0207b..d07e24d920 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java
@@ -22,6 +22,8 @@ public interface VpxOutputBufferRenderer {
/**
* Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer.
+ *
+ * @param outputBuffer The output buffer to be rendered.
*/
void setOutputBuffer(VpxOutputBuffer outputBuffer);
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 8c0a9b91f6..fc42154505 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Mon Mar 13 11:17:14 GMT 2017
+#Wed Jul 12 10:31:13 BST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.1-all.zip
diff --git a/library/all/build.gradle b/library/all/build.gradle
index 63943ada77..79ed9c747b 100644
--- a/library/all/build.gradle
+++ b/library/all/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -24,11 +25,11 @@ android {
}
dependencies {
- compile project(':library-core')
- compile project(':library-dash')
- compile project(':library-hls')
- compile project(':library-smoothstreaming')
- compile project(':library-ui')
+ compile project(modulePrefix + 'library-core')
+ compile project(modulePrefix + 'library-dash')
+ compile project(modulePrefix + 'library-hls')
+ compile project(modulePrefix + 'library-smoothstreaming')
+ compile project(modulePrefix + 'library-ui')
}
ext {
diff --git a/library/core/build.gradle b/library/core/build.gradle
index bb0adaa4c7..ecad1e58b5 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
apply plugin: 'com.android.library'
+apply from: '../../constants.gradle'
android {
compileSdkVersion project.ext.compileSdkVersion
@@ -22,6 +23,7 @@ android {
targetSdkVersion project.ext.targetSdkVersion
}
+ // Workaround to prevent circular dependency on project :testutils.
sourceSets {
androidTest {
java.srcDirs += "../../testutils/src/main/java/"
@@ -29,9 +31,11 @@ android {
}
buildTypes {
- debug {
- testCoverageEnabled = true
- }
+ // Re-enable test coverage when the following issue is fixed:
+ // https://issuetracker.google.com/issues/37019591
+ // debug {
+ // testCoverageEnabled = true
+ // }
}
}
diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml
index a50de35b62..aeddc611cf 100644
--- a/library/core/src/androidTest/AndroidManifest.xml
+++ b/library/core/src/androidTest/AndroidManifest.xml
@@ -31,7 +31,6 @@
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump
index 1932ab78f7..f533e14c3f 100644
--- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump
+++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump
@@ -30,5 +30,6 @@ track 1:
time = 0
flags = 1073741824
data = length 39, hash B7FE77F4
+ crypto mode = 1
encryption key = length 16, hash 4CE944CF
tracksEnded = true
diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump
index 8751c99b20..d84c549dea 100644
--- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump
+++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump
@@ -30,5 +30,6 @@ track 1:
time = 0
flags = 1073741824
data = length 24, hash E58668B1
+ crypto mode = 1
encryption key = length 16, hash 4CE944CF
tracksEnded = true
diff --git a/library/core/src/androidTest/assets/ssa/empty b/library/core/src/androidTest/assets/ssa/empty
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/library/core/src/androidTest/assets/ssa/invalid_timecodes b/library/core/src/androidTest/assets/ssa/invalid_timecodes
new file mode 100644
index 0000000000..89f3bb3f1c
--- /dev/null
+++ b/library/core/src/androidTest/assets/ssa/invalid_timecodes
@@ -0,0 +1,12 @@
+[Script Info]
+Title: SomeTitle
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1
+
+[Events]
+Format: Layer, Start, End, Style, Name, Text
+Dialogue: 0,Invalid,0:00:01.23,Default,Olly,This is the first subtitle{ignored}.
+Dialogue: 0,0:00:02.34,Invalid,Default,Olly,This is the second subtitle \nwith a newline \Nand another.
+Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma.
\ No newline at end of file
diff --git a/library/core/src/androidTest/assets/ssa/no_end_timecodes b/library/core/src/androidTest/assets/ssa/no_end_timecodes
new file mode 100644
index 0000000000..c2c57ac64e
--- /dev/null
+++ b/library/core/src/androidTest/assets/ssa/no_end_timecodes
@@ -0,0 +1,12 @@
+[Script Info]
+Title: SomeTitle
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1
+
+[Events]
+Format: Layer, Start, End, Style, Name, Text
+Dialogue: 0,0:00:00.00, ,Default,Olly,This is the first subtitle.
+Dialogue: 0,0:00:02.34, ,Default,Olly,This is the second subtitle \nwith a newline \Nand another.
+Dialogue: 0,0:00:04.56, ,Default,Olly,This is the third subtitle, with a comma.
\ No newline at end of file
diff --git a/library/core/src/androidTest/assets/ssa/typical b/library/core/src/androidTest/assets/ssa/typical
new file mode 100644
index 0000000000..8a49099c5c
--- /dev/null
+++ b/library/core/src/androidTest/assets/ssa/typical
@@ -0,0 +1,12 @@
+[Script Info]
+Title: SomeTitle
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1
+
+[Events]
+Format: Layer, Start, End, Style, Name, Text
+Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}.
+Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \nwith a newline \Nand another.
+Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma.
\ No newline at end of file
diff --git a/library/core/src/androidTest/assets/ssa/typical_dialogue b/library/core/src/androidTest/assets/ssa/typical_dialogue
new file mode 100644
index 0000000000..5cdab5a84b
--- /dev/null
+++ b/library/core/src/androidTest/assets/ssa/typical_dialogue
@@ -0,0 +1,3 @@
+Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}.
+Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \nwith a newline \Nand another.
+Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma.
\ No newline at end of file
diff --git a/library/core/src/androidTest/assets/ssa/typical_format b/library/core/src/androidTest/assets/ssa/typical_format
new file mode 100644
index 0000000000..0cc5f1690f
--- /dev/null
+++ b/library/core/src/androidTest/assets/ssa/typical_format
@@ -0,0 +1 @@
+Format: Layer, Start, End, Style, Name, Text
\ No newline at end of file
diff --git a/library/core/src/androidTest/assets/ssa/typical_header b/library/core/src/androidTest/assets/ssa/typical_header
new file mode 100644
index 0000000000..3e96bcf14e
--- /dev/null
+++ b/library/core/src/androidTest/assets/ssa/typical_header
@@ -0,0 +1,6 @@
+[Script Info]
+Title: SomeTitle
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1
\ No newline at end of file
diff --git a/library/core/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/androidTest/assets/ts/sample.ps.0.dump
index 3b44fb6fb9..98f3c6a85a 100644
--- a/library/core/src/androidTest/assets/ts/sample.ps.0.dump
+++ b/library/core/src/androidTest/assets/ts/sample.ps.0.dump
@@ -69,7 +69,7 @@ track 224:
sample 0:
time = 40000
flags = 1
- data = length 20616, hash CA38A5B5
+ data = length 20646, hash 576390B
sample 1:
time = 80000
flags = 0
diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump
index 26c6665aaa..91e48b1722 100644
--- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump
+++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump
@@ -30,7 +30,7 @@ track 256:
sample 0:
time = 33366
flags = 1
- data = length 20669, hash 26DABA0F
+ data = length 20711, hash 34341E8
sample 1:
time = 66733
flags = 0
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
index 2c10bfe6a0..bf4ea6e972 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -15,28 +15,20 @@
*/
package com.google.android.exoplayer2;
-import android.os.Handler;
-import android.os.HandlerThread;
import android.util.Pair;
-import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.upstream.Allocator;
-import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.testutil.ExoPlayerWrapper;
+import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
+import com.google.android.exoplayer2.testutil.FakeMediaSource;
+import com.google.android.exoplayer2.testutil.FakeRenderer;
+import com.google.android.exoplayer2.testutil.FakeTimeline;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.util.MimeTypes;
-import java.io.IOException;
-import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import junit.framework.TestCase;
/**
@@ -62,10 +54,10 @@ public final class ExoPlayerTest extends TestCase {
* error.
*/
public void testPlayEmptyTimeline() throws Exception {
- PlayerWrapper playerWrapper = new PlayerWrapper();
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = Timeline.EMPTY;
MediaSource mediaSource = new FakeMediaSource(timeline, null);
- FakeRenderer renderer = new FakeRenderer(null);
+ FakeRenderer renderer = new FakeRenderer();
playerWrapper.setup(mediaSource, renderer);
playerWrapper.blockUntilEnded(TIMEOUT_MS);
assertEquals(0, playerWrapper.positionDiscontinuityCount);
@@ -79,7 +71,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes a single period.
*/
public void testPlaySinglePeriodTimeline() throws Exception {
- PlayerWrapper playerWrapper = new PlayerWrapper();
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
Object manifest = new Object();
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
@@ -98,7 +90,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes three periods.
*/
public void testPlayMultiPeriodTimeline() throws Exception {
- PlayerWrapper playerWrapper = new PlayerWrapper();
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0),
@@ -119,7 +111,7 @@ public final class ExoPlayerTest extends TestCase {
* source.
*/
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
- final PlayerWrapper playerWrapper = new PlayerWrapper();
+ final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10),
@@ -166,7 +158,7 @@ public final class ExoPlayerTest extends TestCase {
}
public void testRepreparationGivesFreshSourceInfo() throws Exception {
- PlayerWrapper playerWrapper = new PlayerWrapper();
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
@@ -218,501 +210,54 @@ public final class ExoPlayerTest extends TestCase {
Pair.create(timeline, thirdSourceManifest));
}
- /**
- * Wraps a player with its own handler thread.
- */
- private static final class PlayerWrapper implements ExoPlayer.EventListener {
-
- private final CountDownLatch sourceInfoCountDownLatch;
- private final CountDownLatch endedCountDownLatch;
- private final HandlerThread playerThread;
- private final Handler handler;
- private final LinkedList> sourceInfos;
-
- private ExoPlayer player;
- private TrackGroupArray trackGroups;
- private Exception exception;
-
- // Written only on the main thread.
- private volatile int positionDiscontinuityCount;
-
- public PlayerWrapper() {
- sourceInfoCountDownLatch = new CountDownLatch(1);
- endedCountDownLatch = new CountDownLatch(1);
- playerThread = new HandlerThread("ExoPlayerTest thread");
- playerThread.start();
- handler = new Handler(playerThread.getLooper());
- sourceInfos = new LinkedList<>();
- }
-
- // Called on the test thread.
-
- public void blockUntilEnded(long timeoutMs) throws Exception {
- if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
- exception = new TimeoutException("Test playback timed out waiting for playback to end.");
- }
- release();
- // Throw any pending exception (from playback, timing out or releasing).
- if (exception != null) {
- throw exception;
- }
- }
-
- public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception {
- if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
- throw new TimeoutException("Test playback timed out waiting for source info.");
- }
- }
-
- public void setup(final MediaSource mediaSource, final Renderer... renderers) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- try {
- player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector());
- player.addListener(PlayerWrapper.this);
- player.setPlayWhenReady(true);
- player.prepare(mediaSource);
- } catch (Exception e) {
- handleError(e);
- }
+ public void testRepeatModeChanges() throws Exception {
+ Timeline timeline = new FakeTimeline(
+ new TimelineWindowDefinition(true, false, 100000),
+ new TimelineWindowDefinition(true, false, 100000),
+ new TimelineWindowDefinition(true, false, 100000));
+ final int[] actionSchedule = { // 0 -> 1
+ Player.REPEAT_MODE_ONE, // 1 -> 1
+ Player.REPEAT_MODE_OFF, // 1 -> 2
+ Player.REPEAT_MODE_ONE, // 2 -> 2
+ Player.REPEAT_MODE_ALL, // 2 -> 0
+ Player.REPEAT_MODE_ONE, // 0 -> 0
+ -1, // 0 -> 0
+ Player.REPEAT_MODE_OFF, // 0 -> 1
+ -1, // 1 -> 2
+ -1 // 2 -> ended
+ };
+ int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2};
+ final LinkedList windowIndices = new LinkedList<>();
+ final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length);
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() {
+ @Override
+ @SuppressWarnings("ResourceType")
+ public void onPositionDiscontinuity() {
+ super.onPositionDiscontinuity();
+ int actionIndex = actionSchedule.length - (int) actionCounter.getCount();
+ if (actionSchedule[actionIndex] != -1) {
+ player.setRepeatMode(actionSchedule[actionIndex]);
}
- });
- }
-
- public void prepare(final MediaSource mediaSource) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- try {
- player.prepare(mediaSource);
- } catch (Exception e) {
- handleError(e);
- }
- }
- });
- }
-
- public void release() throws InterruptedException {
- handler.post(new Runnable() {
- @Override
- public void run() {
- try {
- if (player != null) {
- player.release();
- }
- } catch (Exception e) {
- handleError(e);
- } finally {
- playerThread.quit();
- }
- }
- });
- playerThread.join();
- }
-
- private void handleError(Exception exception) {
- if (this.exception == null) {
- this.exception = exception;
+ windowIndices.add(player.getCurrentWindowIndex());
+ actionCounter.countDown();
}
- endedCountDownLatch.countDown();
+ };
+ MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
+ FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
+ playerWrapper.setup(mediaSource, renderer);
+ boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ playerWrapper.release();
+ assertTrue("Test playback timed out waiting for action schedule to end.", finished);
+ if (playerWrapper.exception != null) {
+ throw playerWrapper.exception;
}
-
- @SafeVarargs
- public final void assertSourceInfosEquals(Pair... sourceInfos) {
- assertEquals(sourceInfos.length, this.sourceInfos.size());
- for (Pair sourceInfo : sourceInfos) {
- assertEquals(sourceInfo, this.sourceInfos.remove());
- }
+ assertEquals(expectedWindowIndices.length, windowIndices.size());
+ for (int i = 0; i < expectedWindowIndices.length; i++) {
+ assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue());
}
-
- // ExoPlayer.EventListener implementation.
-
- @Override
- public void onLoadingChanged(boolean isLoading) {
- // Do nothing.
- }
-
- @Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
- if (playbackState == ExoPlayer.STATE_ENDED) {
- endedCountDownLatch.countDown();
- }
- }
-
- @Override
- public void onTimelineChanged(Timeline timeline, Object manifest) {
- sourceInfos.add(Pair.create(timeline, manifest));
- sourceInfoCountDownLatch.countDown();
- }
-
- @Override
- public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
- this.trackGroups = trackGroups;
- }
-
- @Override
- public void onPlayerError(ExoPlaybackException exception) {
- handleError(exception);
- }
-
- @SuppressWarnings("NonAtomicVolatileUpdate")
- @Override
- public void onPositionDiscontinuity() {
- positionDiscontinuityCount++;
- }
-
- @Override
- public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- // Do nothing.
- }
-
- }
-
- private static final class TimelineWindowDefinition {
-
- public final boolean isSeekable;
- public final boolean isDynamic;
- public final long durationUs;
-
- public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) {
- this.isSeekable = isSeekable;
- this.isDynamic = isDynamic;
- this.durationUs = durationUs;
- }
-
- }
-
- private static final class FakeTimeline extends Timeline {
-
- private final TimelineWindowDefinition[] windowDefinitions;
-
- public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {
- this.windowDefinitions = windowDefinitions;
- }
-
- @Override
- public int getWindowCount() {
- return windowDefinitions.length;
- }
-
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
- Object id = setIds ? windowIndex : null;
- return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable,
- windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0);
- }
-
- @Override
- public int getPeriodCount() {
- return windowDefinitions.length;
- }
-
- @Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
- Object id = setIds ? periodIndex : null;
- return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false);
- }
-
- @Override
- public int getIndexOfPeriod(Object uid) {
- if (!(uid instanceof Integer)) {
- return C.INDEX_UNSET;
- }
- int index = (Integer) uid;
- return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET;
- }
-
- }
-
- /**
- * Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating
- * the period will return a {@link FakeMediaPeriod}.
- */
- private static class FakeMediaSource implements MediaSource {
-
- private final Timeline timeline;
- private final Object manifest;
- private final TrackGroupArray trackGroupArray;
- private final ArrayList activeMediaPeriods;
-
- private boolean preparedSource;
- private boolean releasedSource;
-
- public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) {
- this.timeline = timeline;
- this.manifest = manifest;
- TrackGroup[] trackGroups = new TrackGroup[formats.length];
- for (int i = 0; i < formats.length; i++) {
- trackGroups[i] = new TrackGroup(formats[i]);
- }
- trackGroupArray = new TrackGroupArray(trackGroups);
- activeMediaPeriods = new ArrayList<>();
- }
-
- @Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- assertFalse(preparedSource);
- preparedSource = true;
- listener.onSourceInfoRefreshed(timeline, manifest);
- }
-
- @Override
- public void maybeThrowSourceInfoRefreshError() throws IOException {
- assertTrue(preparedSource);
- }
-
- @Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- Assertions.checkIndex(index, 0, timeline.getPeriodCount());
- assertTrue(preparedSource);
- assertFalse(releasedSource);
- assertEquals(0, positionUs);
- FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
- activeMediaPeriods.add(mediaPeriod);
- return mediaPeriod;
- }
-
- @Override
- public void releasePeriod(MediaPeriod mediaPeriod) {
- assertTrue(preparedSource);
- assertFalse(releasedSource);
- FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod;
- assertTrue(activeMediaPeriods.remove(fakeMediaPeriod));
- fakeMediaPeriod.release();
- }
-
- @Override
- public void releaseSource() {
- assertTrue(preparedSource);
- assertFalse(releasedSource);
- assertTrue(activeMediaPeriods.isEmpty());
- releasedSource = true;
- }
-
- }
-
- /**
- * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that
- * track will give the player a {@link FakeSampleStream}.
- */
- private static final class FakeMediaPeriod implements MediaPeriod {
-
- private final TrackGroupArray trackGroupArray;
-
- private boolean preparedPeriod;
-
- public FakeMediaPeriod(TrackGroupArray trackGroupArray) {
- this.trackGroupArray = trackGroupArray;
- }
-
- public void release() {
- preparedPeriod = false;
- }
-
- @Override
- public void prepare(Callback callback) {
- assertFalse(preparedPeriod);
- preparedPeriod = true;
- callback.onPrepared(this);
- }
-
- @Override
- public void maybeThrowPrepareError() throws IOException {
- assertTrue(preparedPeriod);
- }
-
- @Override
- public TrackGroupArray getTrackGroups() {
- assertTrue(preparedPeriod);
- return trackGroupArray;
- }
-
- @Override
- public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
- SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
- assertTrue(preparedPeriod);
- int rendererCount = selections.length;
- for (int i = 0; i < rendererCount; i++) {
- if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
- streams[i] = null;
- }
- }
- for (int i = 0; i < rendererCount; i++) {
- if (streams[i] == null && selections[i] != null) {
- TrackSelection selection = selections[i];
- assertEquals(1, selection.length());
- assertEquals(0, selection.getIndexInTrackGroup(0));
- TrackGroup trackGroup = selection.getTrackGroup();
- assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET);
- streams[i] = new FakeSampleStream(trackGroup.getFormat(0));
- streamResetFlags[i] = true;
- }
- }
- return 0;
- }
-
- @Override
- public void discardBuffer(long positionUs) {
- // Do nothing.
- }
-
- @Override
- public long readDiscontinuity() {
- assertTrue(preparedPeriod);
- return C.TIME_UNSET;
- }
-
- @Override
- public long getBufferedPositionUs() {
- assertTrue(preparedPeriod);
- return C.TIME_END_OF_SOURCE;
- }
-
- @Override
- public long seekToUs(long positionUs) {
- assertTrue(preparedPeriod);
- assertEquals(0, positionUs);
- return positionUs;
- }
-
- @Override
- public long getNextLoadPositionUs() {
- assertTrue(preparedPeriod);
- return C.TIME_END_OF_SOURCE;
- }
-
- @Override
- public boolean continueLoading(long positionUs) {
- assertTrue(preparedPeriod);
- return false;
- }
-
- }
-
- /**
- * Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag
- * on its input buffer.
- */
- private static final class FakeSampleStream implements SampleStream {
-
- private final Format format;
-
- private boolean readFormat;
-
- public FakeSampleStream(Format format) {
- this.format = format;
- }
-
- @Override
- public boolean isReady() {
- return true;
- }
-
- @Override
- public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
- boolean formatRequired) {
- if (formatRequired || !readFormat) {
- formatHolder.format = format;
- readFormat = true;
- return C.RESULT_FORMAT_READ;
- } else {
- buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
- return C.RESULT_BUFFER_READ;
- }
- }
-
- @Override
- public void maybeThrowError() throws IOException {
- // Do nothing.
- }
-
- @Override
- public void skipData(long positionUs) {
- // Do nothing.
- }
-
- }
-
- /**
- * Fake {@link Renderer} that supports any format with the matching MIME type. The renderer
- * verifies that it reads a given {@link Format}.
- */
- private static class FakeRenderer extends BaseRenderer {
-
- private final Format expectedFormat;
-
- public int positionResetCount;
- public int formatReadCount;
- public int bufferReadCount;
- public boolean isEnded;
-
- public FakeRenderer(Format expectedFormat) {
- super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN
- : MimeTypes.getTrackType(expectedFormat.sampleMimeType));
- this.expectedFormat = expectedFormat;
- }
-
- @Override
- protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
- positionResetCount++;
- isEnded = false;
- }
-
- @Override
- public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
- if (isEnded) {
- return;
- }
-
- // Verify the format matches the expected format.
- FormatHolder formatHolder = new FormatHolder();
- DecoderInputBuffer buffer =
- new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
- int result = readSource(formatHolder, buffer, false);
- if (result == C.RESULT_FORMAT_READ) {
- formatReadCount++;
- assertEquals(expectedFormat, formatHolder.format);
- } else if (result == C.RESULT_BUFFER_READ) {
- bufferReadCount++;
- if (buffer.isEndOfStream()) {
- isEnded = true;
- }
- }
- }
-
- @Override
- public boolean isReady() {
- return isSourceReady();
- }
-
- @Override
- public boolean isEnded() {
- return isEnded;
- }
-
- @Override
- public int supportsFormat(Format format) throws ExoPlaybackException {
- return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED
- : FORMAT_UNSUPPORTED_TYPE;
- }
-
- }
-
- private abstract static class FakeMediaClockRenderer extends FakeRenderer implements MediaClock {
-
- public FakeMediaClockRenderer(Format expectedFormat) {
- super(expectedFormat);
- }
-
- @Override
- public MediaClock getMediaClock() {
- return this;
- }
-
+ assertEquals(9, playerWrapper.positionDiscontinuityCount);
+ assertTrue(renderer.isEnded);
+ playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
}
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java
index a47a3fb12d..bdea08638b 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java
@@ -53,9 +53,9 @@ public final class FormatTest extends TestCase {
}
public void testParcelable() {
- DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4,
+ DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4,
TestUtil.buildTestData(128, 1 /* data seed */));
- DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM,
+ DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM,
TestUtil.buildTestData(128, 1 /* data seed */));
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
byte[] projectionData = new byte[] {1, 2, 3};
@@ -94,10 +94,10 @@ public final class FormatTest extends TestCase {
500, 128, 5, 44100, INIT_DATA, null, 0, null));
testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null,
500, Format.NO_VALUE, 5, 44100, null, null, 0, null));
- testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", null,
- Format.NO_VALUE, 0, "eng", null));
- testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", null,
- Format.NO_VALUE, 0, null, null));
+ testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0,
+ "eng"));
+ testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0,
+ null));
}
@SuppressLint("InlinedApi")
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java
new file mode 100644
index 0000000000..d9ee27bd62
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.testutil.FakeTimeline;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
+import junit.framework.TestCase;
+
+/**
+ * Unit test for {@link Timeline}.
+ */
+public class TimelineTest extends TestCase {
+
+ public void testEmptyTimeline() {
+ TimelineAsserts.assertEmpty(Timeline.EMPTY);
+ }
+
+ public void testSinglePeriodTimeline() {
+ Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111));
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ TimelineAsserts.assertPeriodCounts(timeline, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
+ }
+
+ public void testMultiPeriodTimeline() {
+ Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111));
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ TimelineAsserts.assertPeriodCounts(timeline, 5);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
+ }
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java
index df2e8756a5..aa8cbfdb62 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java
@@ -31,16 +31,16 @@ import junit.framework.TestCase;
*/
public class DrmInitDataTest extends TestCase {
- private static final SchemeData DATA_1 =
- new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */));
- private static final SchemeData DATA_2 =
- new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */));
- private static final SchemeData DATA_1B =
- new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */));
- private static final SchemeData DATA_2B =
- new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */));
- private static final SchemeData DATA_UNIVERSAL =
- new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */));
+ private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
+ TestUtil.buildTestData(128, 1 /* data seed */));
+ private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
+ TestUtil.buildTestData(128, 2 /* data seed */));
+ private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
+ TestUtil.buildTestData(128, 1 /* data seed */));
+ private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
+ TestUtil.buildTestData(128, 2 /* data seed */));
+ private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, null, VIDEO_MP4,
+ TestUtil.buildTestData(128, 3 /* data seed */));
public void testParcelable() {
DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2);
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
index afd690762b..9f5b067b5e 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
@@ -154,7 +154,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
}
private static DrmInitData newDrmInitData() {
- return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType",
+ return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "cenc", "mimeType",
new byte[] {1, 4, 7, 0, 3, 6}));
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java
index 321181621e..fc8d181eac 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.flv;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link FlvExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class FlvExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new FlvExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
index 48eee69b50..624a5ccb7e 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mkv;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Tests for {@link MatroskaExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class MatroskaExtractorTest extends InstrumentationTestCase {
public void testMkvSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new MatroskaExtractor();
@@ -34,7 +35,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
}
public void testWebmSubsampleEncryption() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new MatroskaExtractor();
@@ -43,7 +44,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
}
public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new MatroskaExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java
index c70710f1ee..0f98624d69 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mp3;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link Mp3Extractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class Mp3ExtractorTest extends InstrumentationTestCase {
public void testMp3Sample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new Mp3Extractor();
@@ -34,7 +35,7 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase {
}
public void testTrimmedMp3Sample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new Mp3Extractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
index 95ad8b446e..76c13495c1 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
@@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link FragmentedMp4Extractor}.
@@ -26,26 +27,28 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
+ ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4",
+ getInstrumentation());
}
public void testSampleWithSeiPayloadParsing() throws Exception {
// Enabling the CEA-608 track enables SEI payload parsing.
- TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
+ ExtractorAsserts.assertBehavior(
+ getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
}
public void testAtomWithZeroSize() throws Exception {
- TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
+ ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
getInstrumentation(), ParserException.class);
}
- private static TestUtil.ExtractorFactory getExtractorFactory() {
+ private static ExtractorFactory getExtractorFactory() {
return getExtractorFactory(0);
}
- private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) {
- return new TestUtil.ExtractorFactory() {
+ private static ExtractorFactory getExtractorFactory(final int flags) {
+ return new ExtractorFactory() {
@Override
public Extractor create() {
return new FragmentedMp4Extractor(flags, null);
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java
index 6ad777da70..5e327e5502 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java
@@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
import android.annotation.TargetApi;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Tests for {@link Mp4Extractor}.
@@ -27,7 +28,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class Mp4ExtractorTest extends InstrumentationTestCase {
public void testMp4Sample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new Mp4Extractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java
index 04a6131652..3be23422cc 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java
@@ -17,9 +17,10 @@ package com.google.android.exoplayer2.extractor.ogg;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
-import com.google.android.exoplayer2.testutil.TestUtil.ExtractorFactory;
import java.io.IOException;
/**
@@ -35,20 +36,22 @@ public final class OggExtractorTest extends InstrumentationTestCase {
};
public void testOpus() throws Exception {
- TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation());
+ ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation());
}
public void testFlac() throws Exception {
- TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation());
+ ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg",
+ getInstrumentation());
}
public void testFlacNoSeektable() throws Exception {
- TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg",
+ ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg",
getInstrumentation());
}
public void testVorbis() throws Exception {
- TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", getInstrumentation());
+ ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg",
+ getInstrumentation());
}
public void testSniffVorbis() throws Exception {
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java
index 4e99e2745e..18050f48a3 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java
@@ -19,7 +19,8 @@ import android.annotation.TargetApi;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.util.MimeTypes;
/**
@@ -29,8 +30,8 @@ import com.google.android.exoplayer2.util.MimeTypes;
public final class RawCcExtractorTest extends InstrumentationTestCase {
public void testRawCcSample() throws Exception {
- TestUtil.assertOutput(
- new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(
+ new ExtractorFactory() {
@Override
public Extractor create() {
return new RawCcExtractor(
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java
index ab44e3aed3..31633361db 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link Ac3Extractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class Ac3ExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new Ac3Extractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
index e30a863d07..9eb65d2091 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link AdtsExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class AdtsExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new AdtsExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java
index ef97bef0ff..78ef05a769 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link PsExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class PsExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new PsExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java
index efd653b8d9..b6eddb5112 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java
@@ -25,6 +25,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
@@ -43,7 +45,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new TsExtractor();
@@ -65,7 +67,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
fileData = out.toByteArray();
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new TsExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
index a416d644b7..36c05aa72e 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.wav;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link WavExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class WavExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new WavExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
index 0933fb858b..66b0337450 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
@@ -15,20 +15,17 @@
*/
package com.google.android.exoplayer2.source;
-import static org.mockito.Mockito.doAnswer;
-
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window;
-import com.google.android.exoplayer2.source.MediaSource.Listener;
+import com.google.android.exoplayer2.testutil.FakeMediaSource;
+import com.google.android.exoplayer2.testutil.FakeTimeline;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TestUtil;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
/**
* Unit tests for {@link ClippingMediaSource}.
@@ -38,15 +35,11 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
private static final long TEST_PERIOD_DURATION_US = 1000000;
private static final long TEST_CLIP_AMOUNT_US = 300000;
- @Mock
- private MediaSource mockMediaSource;
- private Timeline clippedTimeline;
private Window window;
private Period period;
@Override
protected void setUp() throws Exception {
- TestUtil.setUpMockito(this);
window = new Timeline.Window();
period = new Timeline.Period();
}
@@ -109,35 +102,29 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
clippedTimeline.getPeriod(0, period).getDurationUs());
}
+ public void testWindowAndPeriodIndices() {
+ Timeline timeline = new FakeTimeline(
+ new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US));
+ Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
+ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
+ TimelineAsserts.assertWindowIds(clippedTimeline, 111);
+ TimelineAsserts.assertPeriodCounts(clippedTimeline, 1);
+ TimelineAsserts.assertPreviousWindowIndices(
+ clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0);
+ }
+
/**
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/
- private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
- mockMediaSourceSourceWithTimeline(timeline);
- new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true,
- new Listener() {
- @Override
- public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
- clippedTimeline = timeline;
- }
- });
- return clippedTimeline;
- }
-
- /**
- * Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info.
- */
- private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) {
- doAnswer(new Answer() {
- @Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
- MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2];
- listener.onSourceInfoRefreshed(timeline, null);
- return null;
- }
- }).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(),
- Mockito.any(MediaSource.Listener.class));
- return mockMediaSource;
+ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
+ MediaSource mediaSource = new FakeMediaSource(timeline, null);
+ return TestUtil.extractTimelineFromMediaSource(
+ new ClippingMediaSource(mediaSource, startMs, endMs));
}
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java
new file mode 100644
index 0000000000..3bf89f9bcc
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.testutil.FakeMediaSource;
+import com.google.android.exoplayer2.testutil.FakeTimeline;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link ConcatenatingMediaSource}.
+ */
+public final class ConcatenatingMediaSourceTest extends TestCase {
+
+ public void testSingleMediaSource() {
+ Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111));
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ TimelineAsserts.assertPeriodCounts(timeline, 3);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
+
+ timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111));
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ TimelineAsserts.assertPeriodCounts(timeline, 3);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
+ }
+
+ public void testMultipleMediaSources() {
+ Timeline[] timelines = { createFakeTimeline(3, 111), createFakeTimeline(1, 222),
+ createFakeTimeline(3, 333) };
+ Timeline timeline = getConcatenatedTimeline(false, timelines);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
+ TimelineAsserts.assertPreviousWindowIndices(
+ timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0);
+
+ timeline = getConcatenatedTimeline(true, timelines);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
+ TimelineAsserts.assertPreviousWindowIndices(
+ timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 2, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 1, 2, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0);
+ }
+
+ public void testNestedMediaSources() {
+ Timeline timeline = getConcatenatedTimeline(false,
+ getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)),
+ getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444)));
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1);
+ TimelineAsserts.assertPreviousWindowIndices(
+ timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 3, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(
+ timeline, Player.REPEAT_MODE_OFF, 1, 2, 3, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 3, 0);
+ }
+
+ /**
+ * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns
+ * the concatenated timeline.
+ */
+ private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic,
+ Timeline... timelines) {
+ MediaSource[] mediaSources = new MediaSource[timelines.length];
+ for (int i = 0; i < timelines.length; i++) {
+ mediaSources[i] = new FakeMediaSource(timelines[i], null);
+ }
+ return TestUtil.extractTimelineFromMediaSource(
+ new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources));
+ }
+
+ private static FakeTimeline createFakeTimeline(int periodCount, int windowId) {
+ return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId));
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
new file mode 100644
index 0000000000..f8636b9990
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.MediaSource.Listener;
+import com.google.android.exoplayer2.testutil.FakeMediaSource;
+import com.google.android.exoplayer2.testutil.FakeTimeline;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.Allocator;
+import java.io.IOException;
+import java.util.Arrays;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link DynamicConcatenatingMediaSource}
+ */
+public final class DynamicConcatenatingMediaSourceTest extends TestCase {
+
+ private static final int TIMEOUT_MS = 10000;
+
+ private Timeline timeline;
+ private boolean timelineUpdated;
+
+ public void testPlaylistChangesAfterPreparation() throws InterruptedException {
+ timeline = null;
+ FakeMediaSource[] childSources = createMediaSources(7);
+ DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
+ prepareAndListenToTimelineUpdates(mediaSource);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertEmpty(timeline);
+
+ // Add first source.
+ mediaSource.addMediaSource(childSources[0]);
+ waitForTimelineUpdate();
+ assertNotNull(timeline);
+ TimelineAsserts.assertPeriodCounts(timeline, 1);
+ TimelineAsserts.assertWindowIds(timeline, 111);
+
+ // Add at front of queue.
+ mediaSource.addMediaSource(0, childSources[1]);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 1);
+ TimelineAsserts.assertWindowIds(timeline, 222, 111);
+
+ // Add at back of queue.
+ mediaSource.addMediaSource(childSources[2]);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
+
+ // Add in the middle.
+ mediaSource.addMediaSource(1, childSources[3]);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333);
+
+ // Add bulk.
+ mediaSource.addMediaSources(3, Arrays.asList((MediaSource) childSources[4],
+ (MediaSource) childSources[5], (MediaSource) childSources[6]));
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
+
+ // Move sources.
+ mediaSource.moveMediaSource(2, 3);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333);
+ mediaSource.moveMediaSource(3, 2);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
+ mediaSource.moveMediaSource(0, 6);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2);
+ TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222);
+ mediaSource.moveMediaSource(6, 0);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
+
+ // Remove in the middle.
+ mediaSource.removeMediaSource(3);
+ waitForTimelineUpdate();
+ mediaSource.removeMediaSource(3);
+ waitForTimelineUpdate();
+ mediaSource.removeMediaSource(3);
+ waitForTimelineUpdate();
+ mediaSource.removeMediaSource(1);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
+ for (int i = 3; i <= 6; i++) {
+ childSources[i].assertReleased();
+ }
+
+ // Assert correct next and previous indices behavior after some insertions and removals.
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0);
+ TimelineAsserts.assertPreviousWindowIndices(
+ timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1);
+
+ // Remove at front of queue.
+ mediaSource.removeMediaSource(0);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 3);
+ TimelineAsserts.assertWindowIds(timeline, 111, 333);
+ childSources[1].assertReleased();
+
+ // Remove at back of queue.
+ mediaSource.removeMediaSource(1);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 1);
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ childSources[2].assertReleased();
+
+ // Remove last source.
+ mediaSource.removeMediaSource(0);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertEmpty(timeline);
+ childSources[3].assertReleased();
+ }
+
+ public void testPlaylistChangesBeforePreparation() throws InterruptedException {
+ timeline = null;
+ FakeMediaSource[] childSources = createMediaSources(4);
+ DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
+ mediaSource.addMediaSource(childSources[0]);
+ mediaSource.addMediaSource(childSources[1]);
+ mediaSource.addMediaSource(0, childSources[2]);
+ mediaSource.moveMediaSource(0, 2);
+ mediaSource.removeMediaSource(0);
+ mediaSource.moveMediaSource(1, 0);
+ mediaSource.addMediaSource(1, childSources[3]);
+ assertNull(timeline);
+
+ prepareAndListenToTimelineUpdates(mediaSource);
+ waitForTimelineUpdate();
+ assertNotNull(timeline);
+ TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2);
+ TimelineAsserts.assertWindowIds(timeline, 333, 444, 222);
+
+ mediaSource.releaseSource();
+ for (int i = 1; i < 4; i++) {
+ childSources[i].assertReleased();
+ }
+ }
+
+ public void testPlaylistWithLazyMediaSource() throws InterruptedException {
+ timeline = null;
+ FakeMediaSource[] childSources = createMediaSources(2);
+ LazyMediaSource[] lazySources = new LazyMediaSource[4];
+ for (int i = 0; i < 4; i++) {
+ lazySources[i] = new LazyMediaSource();
+ }
+
+ //Add lazy sources before preparation
+ DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
+ mediaSource.addMediaSource(lazySources[0]);
+ mediaSource.addMediaSource(0, childSources[0]);
+ mediaSource.removeMediaSource(1);
+ mediaSource.addMediaSource(1, lazySources[1]);
+ assertNull(timeline);
+ prepareAndListenToTimelineUpdates(mediaSource);
+ waitForTimelineUpdate();
+ assertNotNull(timeline);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1);
+ TimelineAsserts.assertWindowIds(timeline, 111, null);
+ TimelineAsserts.assertWindowIsDynamic(timeline, false, true);
+
+ lazySources[1].triggerTimelineUpdate(createFakeTimeline(8));
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 9);
+ TimelineAsserts.assertWindowIds(timeline, 111, 999);
+ TimelineAsserts.assertWindowIsDynamic(timeline, false, false);
+
+ //Add lazy sources after preparation
+ mediaSource.addMediaSource(1, lazySources[2]);
+ waitForTimelineUpdate();
+ mediaSource.addMediaSource(2, childSources[1]);
+ waitForTimelineUpdate();
+ mediaSource.addMediaSource(0, lazySources[3]);
+ waitForTimelineUpdate();
+ mediaSource.removeMediaSource(2);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9);
+ TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999);
+ TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false);
+
+ lazySources[3].triggerTimelineUpdate(createFakeTimeline(7));
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9);
+ TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999);
+ TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false);
+
+ mediaSource.releaseSource();
+ childSources[0].assertReleased();
+ childSources[1].assertReleased();
+ }
+
+ public void testIllegalArguments() {
+ DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
+ MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null);
+
+ // Null sources.
+ try {
+ mediaSource.addMediaSource(null);
+ fail("Null mediaSource not allowed.");
+ } catch (NullPointerException e) {
+ // Expected.
+ }
+
+ MediaSource[] mediaSources = { validSource, null };
+ try {
+ mediaSource.addMediaSources(Arrays.asList(mediaSources));
+ fail("Null mediaSource not allowed.");
+ } catch (NullPointerException e) {
+ // Expected.
+ }
+
+ // Duplicate sources.
+ mediaSource.addMediaSource(validSource);
+ try {
+ mediaSource.addMediaSource(validSource);
+ fail("Duplicate mediaSource not allowed.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ mediaSources = new MediaSource[] {
+ new FakeMediaSource(createFakeTimeline(2), null), validSource };
+ try {
+ mediaSource.addMediaSources(Arrays.asList(mediaSources));
+ fail("Duplicate mediaSource not allowed.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) {
+ mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
+ timeline = newTimeline;
+ synchronized (DynamicConcatenatingMediaSourceTest.this) {
+ timelineUpdated = true;
+ DynamicConcatenatingMediaSourceTest.this.notify();
+ }
+ }
+ });
+ }
+
+ private synchronized void waitForTimelineUpdate() throws InterruptedException {
+ long timeoutMs = System.currentTimeMillis() + TIMEOUT_MS;
+ while (!timelineUpdated) {
+ wait(TIMEOUT_MS);
+ if (System.currentTimeMillis() >= timeoutMs) {
+ fail("No timeline update occurred within timeout.");
+ }
+ }
+ timelineUpdated = false;
+ }
+
+ private static FakeMediaSource[] createMediaSources(int count) {
+ FakeMediaSource[] sources = new FakeMediaSource[count];
+ for (int i = 0; i < count; i++) {
+ sources[i] = new FakeMediaSource(createFakeTimeline(i), null);
+ }
+ return sources;
+ }
+
+ private static FakeTimeline createFakeTimeline(int index) {
+ return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111));
+ }
+
+ private static class LazyMediaSource implements MediaSource {
+
+ private Listener listener;
+
+ public void triggerTimelineUpdate(Timeline timeline) {
+ listener.onSourceInfoRefreshed(timeline, null);
+ }
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ return null;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ }
+
+ @Override
+ public void releaseSource() {
+ }
+
+ }
+
+ /**
+ * Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread.
+ */
+ private static class StubExoPlayer implements ExoPlayer, Handler.Callback {
+
+ private final Handler handler;
+
+ public StubExoPlayer() {
+ HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper(), this);
+ }
+
+ @Override
+ public Looper getPlaybackLooper() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addListener(Player.EventListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeListener(Player.EventListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getPlaybackState() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setPlayWhenReady(boolean playWhenReady) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getPlayWhenReady() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getRepeatMode() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isLoading() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void seekToDefaultPosition() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void seekToDefaultPosition(int windowIndex) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void seekTo(long positionMs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void seekTo(int windowIndex, long positionMs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void stop() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void release() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void sendMessages(ExoPlayerMessage... messages) {
+ handler.obtainMessage(0, messages).sendToTarget();
+ }
+
+ @Override
+ public void blockingSendMessages(ExoPlayerMessage... messages) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getRendererCount() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getRendererType(int index) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public TrackGroupArray getCurrentTrackGroups() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public TrackSelectionArray getCurrentTrackSelections() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object getCurrentManifest() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Timeline getCurrentTimeline() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getCurrentPeriodIndex() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getCurrentWindowIndex() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getDuration() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getBufferedPercentage() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isCurrentWindowDynamic() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isCurrentWindowSeekable() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isPlayingAd() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getCurrentAdGroupIndex() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getCurrentAdIndexInAdGroup() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getContentPosition() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj;
+ for (ExoPlayerMessage message : messages) {
+ try {
+ message.target.handleMessage(message.messageType, message.message);
+ } catch (ExoPlaybackException e) {
+ fail("Unexpected ExoPlaybackException.");
+ }
+ }
+ return true;
+ }
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java
new file mode 100644
index 0000000000..d2045c29a5
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.testutil.FakeMediaSource;
+import com.google.android.exoplayer2.testutil.FakeTimeline;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link LoopingMediaSource}.
+ */
+public class LoopingMediaSourceTest extends TestCase {
+
+ private final Timeline multiWindowTimeline;
+
+ public LoopingMediaSourceTest() {
+ multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource(
+ new FakeTimeline(new TimelineWindowDefinition(1, 111),
+ new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null));
+ }
+
+ public void testSingleLoop() {
+ Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
+ TimelineAsserts.assertPreviousWindowIndices(
+ timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0);
+ }
+
+ public void testMultiLoop() {
+ Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF,
+ C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL,
+ 8, 0, 1, 2, 3, 4, 5, 6, 7);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF,
+ 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL,
+ 1, 2, 3, 4, 5, 6, 7, 8, 0);
+ }
+
+ public void testInfiniteLoop() {
+ Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, 2, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0);
+ }
+
+ /**
+ * Wraps the specified timeline in a {@link LoopingMediaSource} and returns
+ * the looping timeline.
+ */
+ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) {
+ MediaSource mediaSource = new FakeMediaSource(timeline, null);
+ return TestUtil.extractTimelineFromMediaSource(
+ new LoopingMediaSource(mediaSource, loopCount));
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
new file mode 100644
index 0000000000..76ea0e34cf
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
@@ -0,0 +1,688 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.test.MoreAsserts;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import junit.framework.TestCase;
+
+/**
+ * Test for {@link SampleQueue}.
+ */
+public class SampleQueueTest extends TestCase {
+
+ private static final int ALLOCATION_SIZE = 16;
+
+ private static final Format TEST_FORMAT_1 = Format.createSampleFormat("1", "mimeType", 0);
+ private static final Format TEST_FORMAT_2 = Format.createSampleFormat("2", "mimeType", 0);
+ private static final Format TEST_FORMAT_1_COPY = Format.createSampleFormat("1", "mimeType", 0);
+ private static final byte[] TEST_DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10);
+
+ /*
+ * TEST_SAMPLE_SIZES and TEST_SAMPLE_OFFSETS are intended to test various boundary cases (with
+ * respect to the allocation size). TEST_SAMPLE_OFFSETS values are defined as the backward offsets
+ * (as expected by SampleQueue.sampleMetadata) assuming that TEST_DATA has been written to the
+ * sampleQueue in full. The allocations are filled as follows, where | indicates a boundary
+ * between allocations and x indicates a byte that doesn't belong to a sample:
+ *
+ * x|xx|x|x|||xx|
+ */
+ private static final int[] TEST_SAMPLE_SIZES = new int[] {
+ ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 2, ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 1,
+ ALLOCATION_SIZE, ALLOCATION_SIZE * 2, ALLOCATION_SIZE * 2 - 2, ALLOCATION_SIZE
+ };
+ private static final int[] TEST_SAMPLE_OFFSETS = new int[] {
+ ALLOCATION_SIZE * 9, ALLOCATION_SIZE * 8 + 1, ALLOCATION_SIZE * 7, ALLOCATION_SIZE * 6 + 1,
+ ALLOCATION_SIZE * 5, ALLOCATION_SIZE * 3, ALLOCATION_SIZE + 1, 0
+ };
+ private static final long[] TEST_SAMPLE_TIMESTAMPS = new long[] {
+ 0, 1000, 2000, 3000, 4000, 5000, 6000, 7000
+ };
+ private static final long LAST_SAMPLE_TIMESTAMP =
+ TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1];
+ private static final int[] TEST_SAMPLE_FLAGS = new int[] {
+ C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0, C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0
+ };
+ private static final Format[] TEST_SAMPLE_FORMATS = new Format[] {
+ TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_2, TEST_FORMAT_2,
+ TEST_FORMAT_2, TEST_FORMAT_2
+ };
+ private static final int TEST_DATA_SECOND_KEYFRAME_INDEX = 4;
+
+ private Allocator allocator;
+ private SampleQueue sampleQueue;
+ private FormatHolder formatHolder;
+ private DecoderInputBuffer inputBuffer;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ allocator = new DefaultAllocator(false, ALLOCATION_SIZE);
+ sampleQueue = new SampleQueue(allocator);
+ formatHolder = new FormatHolder();
+ inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ allocator = null;
+ sampleQueue = null;
+ formatHolder = null;
+ inputBuffer = null;
+ }
+
+ public void testResetReleasesAllocations() {
+ writeTestData();
+ assertAllocationCount(10);
+ sampleQueue.reset();
+ assertAllocationCount(0);
+ }
+
+ public void testReadWithoutWrite() {
+ assertNoSamplesToRead(null);
+ }
+
+ public void testReadFormatDeduplicated() {
+ sampleQueue.format(TEST_FORMAT_1);
+ assertReadFormat(false, TEST_FORMAT_1);
+ // If the same format is input then it should be de-duplicated (i.e. not output again).
+ sampleQueue.format(TEST_FORMAT_1);
+ assertNoSamplesToRead(TEST_FORMAT_1);
+ // The same applies for a format that's equal (but a different object).
+ sampleQueue.format(TEST_FORMAT_1_COPY);
+ assertNoSamplesToRead(TEST_FORMAT_1);
+ }
+
+ public void testReadSingleSamples() {
+ sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
+
+ assertAllocationCount(1);
+ // Nothing to read.
+ assertNoSamplesToRead(null);
+
+ sampleQueue.format(TEST_FORMAT_1);
+
+ // Read the format.
+ assertReadFormat(false, TEST_FORMAT_1);
+ // Nothing to read.
+ assertNoSamplesToRead(TEST_FORMAT_1);
+
+ sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
+
+ // If formatRequired, should read the format rather than the sample.
+ assertReadFormat(true, TEST_FORMAT_1);
+ // Otherwise should read the sample.
+ assertSampleRead(1000, true, TEST_DATA, 0, ALLOCATION_SIZE);
+ // Allocation should still be held.
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ // The allocation should have been released.
+ assertAllocationCount(0);
+
+ // Nothing to read.
+ assertNoSamplesToRead(TEST_FORMAT_1);
+
+ // Write a second sample followed by one byte that does not belong to it.
+ sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
+ sampleQueue.sampleMetadata(2000, 0, ALLOCATION_SIZE - 1, 1, null);
+
+ // If formatRequired, should read the format rather than the sample.
+ assertReadFormat(true, TEST_FORMAT_1);
+ // Read the sample.
+ assertSampleRead(2000, false, TEST_DATA, 0, ALLOCATION_SIZE - 1);
+ // Allocation should still be held.
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ // The last byte written to the sample queue may belong to a sample whose metadata has yet to be
+ // written, so an allocation should still be held.
+ assertAllocationCount(1);
+
+ // Write metadata for a third sample containing the remaining byte.
+ sampleQueue.sampleMetadata(3000, 0, 1, 0, null);
+
+ // If formatRequired, should read the format rather than the sample.
+ assertReadFormat(true, TEST_FORMAT_1);
+ // Read the sample.
+ assertSampleRead(3000, false, TEST_DATA, ALLOCATION_SIZE - 1, 1);
+ // Allocation should still be held.
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ // The allocation should have been released.
+ assertAllocationCount(0);
+ }
+
+ public void testReadMultiSamples() {
+ writeTestData();
+ assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
+ assertAllocationCount(10);
+ assertReadTestData();
+ assertAllocationCount(10);
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ }
+
+ public void testReadMultiSamplesTwice() {
+ writeTestData();
+ writeTestData();
+ assertAllocationCount(20);
+ assertReadTestData(TEST_FORMAT_2);
+ assertReadTestData(TEST_FORMAT_2);
+ assertAllocationCount(20);
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ }
+
+ public void testReadMultiWithRewind() {
+ writeTestData();
+ assertReadTestData();
+ assertEquals(8, sampleQueue.getReadIndex());
+ assertAllocationCount(10);
+ // Rewind.
+ sampleQueue.rewind();
+ assertAllocationCount(10);
+ // Read again.
+ assertEquals(0, sampleQueue.getReadIndex());
+ assertReadTestData();
+ }
+
+ public void testRewindAfterDiscard() {
+ writeTestData();
+ assertReadTestData();
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ // Rewind.
+ sampleQueue.rewind();
+ assertAllocationCount(0);
+ // Can't read again.
+ assertEquals(8, sampleQueue.getReadIndex());
+ assertReadEndOfStream(false);
+ }
+
+ public void testAdvanceToEnd() {
+ writeTestData();
+ sampleQueue.advanceToEnd();
+ assertAllocationCount(10);
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ // Despite skipping all samples, we should still read the last format, since this is the
+ // expected format for a subsequent sample.
+ assertReadFormat(false, TEST_FORMAT_2);
+ // Once the format has been read, there's nothing else to read.
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToEndRetainsUnassignedData() {
+ sampleQueue.format(TEST_FORMAT_1);
+ sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
+ sampleQueue.advanceToEnd();
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ // Skipping shouldn't discard data that may belong to a sample whose metadata has yet to be
+ // written.
+ assertAllocationCount(1);
+ // We should be able to read the format.
+ assertReadFormat(false, TEST_FORMAT_1);
+ // Once the format has been read, there's nothing else to read.
+ assertNoSamplesToRead(TEST_FORMAT_1);
+
+ sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
+ // Once the metadata has been written, check the sample can be read as expected.
+ assertSampleRead(0, true, TEST_DATA, 0, ALLOCATION_SIZE);
+ assertNoSamplesToRead(TEST_FORMAT_1);
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ }
+
+ public void testAdvanceToBeforeBuffer() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false);
+ // Should fail and have no effect.
+ assertFalse(result);
+ assertReadTestData();
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToStartOfBuffer() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false);
+ // Should succeed but have no effect (we're already at the first frame).
+ assertTrue(result);
+ assertReadTestData();
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToEndOfBuffer() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
+ // Should succeed and skip to 2nd keyframe.
+ assertTrue(result);
+ assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToAfterBuffer() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false);
+ // Should fail and have no effect.
+ assertFalse(result);
+ assertReadTestData();
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToAfterBufferAllowed() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
+ // Should succeed and skip to 2nd keyframe.
+ assertTrue(result);
+ assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testDiscardToEnd() {
+ writeTestData();
+ // Should discard everything.
+ sampleQueue.discardToEnd();
+ assertEquals(8, sampleQueue.getReadIndex());
+ assertAllocationCount(0);
+ // We should still be able to read the upstream format.
+ assertReadFormat(false, TEST_FORMAT_2);
+ // We should be able to write and read subsequent samples.
+ writeTestData();
+ assertReadTestData(TEST_FORMAT_2);
+ }
+
+ public void testDiscardToStopAtReadPosition() {
+ writeTestData();
+ // Shouldn't discard anything.
+ sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);
+ assertEquals(0, sampleQueue.getReadIndex());
+ assertAllocationCount(10);
+ // Read the first sample.
+ assertReadTestData(null, 0, 1);
+ // Shouldn't discard anything.
+ sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, true);
+ assertEquals(1, sampleQueue.getReadIndex());
+ assertAllocationCount(10);
+ // Should discard the read sample.
+ sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, true);
+ assertAllocationCount(9);
+ // Shouldn't discard anything.
+ sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);
+ assertAllocationCount(9);
+ // Should be able to read the remaining samples.
+ assertReadTestData(TEST_FORMAT_1, 1, 7);
+ assertEquals(8, sampleQueue.getReadIndex());
+ // Should discard up to the second last sample
+ sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP - 1, false, true);
+ assertAllocationCount(3);
+ // Should discard up the last sample
+ sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);
+ assertAllocationCount(1);
+ }
+
+ public void testDiscardToDontStopAtReadPosition() {
+ writeTestData();
+ // Shouldn't discard anything.
+ sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, false);
+ assertEquals(0, sampleQueue.getReadIndex());
+ assertAllocationCount(10);
+ // Should discard the first sample.
+ sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, false);
+ assertEquals(1, sampleQueue.getReadIndex());
+ assertAllocationCount(9);
+ // Should be able to read the remaining samples.
+ assertReadTestData(TEST_FORMAT_1, 1, 7);
+ }
+
+ public void testDiscardUpstream() {
+ writeTestData();
+ sampleQueue.discardUpstreamSamples(8);
+ assertAllocationCount(10);
+ sampleQueue.discardUpstreamSamples(7);
+ assertAllocationCount(9);
+ sampleQueue.discardUpstreamSamples(6);
+ assertAllocationCount(7);
+ sampleQueue.discardUpstreamSamples(5);
+ assertAllocationCount(5);
+ sampleQueue.discardUpstreamSamples(4);
+ assertAllocationCount(4);
+ sampleQueue.discardUpstreamSamples(3);
+ assertAllocationCount(3);
+ sampleQueue.discardUpstreamSamples(2);
+ assertAllocationCount(2);
+ sampleQueue.discardUpstreamSamples(1);
+ assertAllocationCount(1);
+ sampleQueue.discardUpstreamSamples(0);
+ assertAllocationCount(0);
+ assertReadFormat(false, TEST_FORMAT_2);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testDiscardUpstreamMulti() {
+ writeTestData();
+ sampleQueue.discardUpstreamSamples(4);
+ assertAllocationCount(4);
+ sampleQueue.discardUpstreamSamples(0);
+ assertAllocationCount(0);
+ assertReadFormat(false, TEST_FORMAT_2);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testDiscardUpstreamBeforeRead() {
+ writeTestData();
+ sampleQueue.discardUpstreamSamples(4);
+ assertAllocationCount(4);
+ assertReadTestData(null, 0, 4);
+ assertReadFormat(false, TEST_FORMAT_2);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testDiscardUpstreamAfterRead() {
+ writeTestData();
+ assertReadTestData(null, 0, 3);
+ sampleQueue.discardUpstreamSamples(8);
+ assertAllocationCount(10);
+ sampleQueue.discardToRead();
+ assertAllocationCount(7);
+ sampleQueue.discardUpstreamSamples(7);
+ assertAllocationCount(6);
+ sampleQueue.discardUpstreamSamples(6);
+ assertAllocationCount(4);
+ sampleQueue.discardUpstreamSamples(5);
+ assertAllocationCount(2);
+ sampleQueue.discardUpstreamSamples(4);
+ assertAllocationCount(1);
+ sampleQueue.discardUpstreamSamples(3);
+ assertAllocationCount(0);
+ assertReadFormat(false, TEST_FORMAT_2);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testLargestQueuedTimestampWithDiscardUpstream() {
+ writeTestData();
+ assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 1);
+ // Discarding from upstream should reduce the largest timestamp.
+ assertEquals(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 2],
+ sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(0);
+ // Discarding everything from upstream without reading should unset the largest timestamp.
+ assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs());
+ }
+
+ public void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() {
+ long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000};
+ writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, decodeOrderTimestamps,
+ TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS);
+ assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 2);
+ // Discarding the last two samples should not change the largest timestamp, due to the decode
+ // ordering of the timestamps.
+ assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 3);
+ // Once a third sample is discarded, the largest timestamp should have changed.
+ assertEquals(4000, sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(0);
+ // Discarding everything from upstream without reading should unset the largest timestamp.
+ assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs());
+ }
+
+ public void testLargestQueuedTimestampWithRead() {
+ writeTestData();
+ assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
+ assertReadTestData();
+ // Reading everything should not reduce the largest timestamp.
+ assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
+ }
+
+ // Internal methods.
+
+ /**
+ * Writes standard test data to {@code sampleQueue}.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void writeTestData() {
+ writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, TEST_SAMPLE_TIMESTAMPS,
+ TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS);
+ }
+
+ /**
+ * Writes the specified test data to {@code sampleQueue}.
+ *
+ *
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void writeTestData(byte[] data, int[] sampleSizes, int[] sampleOffsets,
+ long[] sampleTimestamps, Format[] sampleFormats, int[] sampleFlags) {
+ sampleQueue.sampleData(new ParsableByteArray(data), data.length);
+ Format format = null;
+ for (int i = 0; i < sampleTimestamps.length; i++) {
+ if (sampleFormats[i] != format) {
+ sampleQueue.format(sampleFormats[i]);
+ format = sampleFormats[i];
+ }
+ sampleQueue.sampleMetadata(sampleTimestamps[i], sampleFlags[i], sampleSizes[i],
+ sampleOffsets[i], null);
+ }
+ }
+
+ /**
+ * Asserts correct reading of standard test data from {@code sampleQueue}.
+ */
+ private void assertReadTestData() {
+ assertReadTestData(null, 0);
+ }
+
+ /**
+ * Asserts correct reading of standard test data from {@code sampleQueue}.
+ *
+ * @param startFormat The format of the last sample previously read from {@code sampleQueue}.
+ */
+ private void assertReadTestData(Format startFormat) {
+ assertReadTestData(startFormat, 0);
+ }
+
+ /**
+ * Asserts correct reading of standard test data from {@code sampleQueue}.
+ *
+ * @param startFormat The format of the last sample previously read from {@code sampleQueue}.
+ * @param firstSampleIndex The index of the first sample that's expected to be read.
+ */
+ private void assertReadTestData(Format startFormat, int firstSampleIndex) {
+ assertReadTestData(startFormat, firstSampleIndex,
+ TEST_SAMPLE_TIMESTAMPS.length - firstSampleIndex);
+ }
+
+ /**
+ * Asserts correct reading of standard test data from {@code sampleQueue}.
+ *
+ * @param startFormat The format of the last sample previously read from {@code sampleQueue}.
+ * @param firstSampleIndex The index of the first sample that's expected to be read.
+ * @param sampleCount The number of samples to read.
+ */
+ private void assertReadTestData(Format startFormat, int firstSampleIndex, int sampleCount) {
+ Format format = startFormat;
+ for (int i = firstSampleIndex; i < firstSampleIndex + sampleCount; i++) {
+ // Use equals() on the read side despite using referential equality on the write side, since
+ // sampleQueue de-duplicates written formats using equals().
+ if (!TEST_SAMPLE_FORMATS[i].equals(format)) {
+ // If the format has changed, we should read it.
+ assertReadFormat(false, TEST_SAMPLE_FORMATS[i]);
+ format = TEST_SAMPLE_FORMATS[i];
+ }
+ // If we require the format, we should always read it.
+ assertReadFormat(true, TEST_SAMPLE_FORMATS[i]);
+ // Assert the sample is as expected.
+ assertSampleRead(TEST_SAMPLE_TIMESTAMPS[i],
+ (TEST_SAMPLE_FLAGS[i] & C.BUFFER_FLAG_KEY_FRAME) != 0,
+ TEST_DATA,
+ TEST_DATA.length - TEST_SAMPLE_OFFSETS[i] - TEST_SAMPLE_SIZES[i],
+ TEST_SAMPLE_SIZES[i]);
+ }
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} is behaving correctly, given there are no samples to read and
+ * the last format to be written to the sample queue is {@code endFormat}.
+ *
+ * @param endFormat The last format to be written to the sample queue, or null of no format has
+ * been written.
+ */
+ private void assertNoSamplesToRead(Format endFormat) {
+ // If not formatRequired or loadingFinished, should read nothing.
+ assertReadNothing(false);
+ // If formatRequired, should read the end format if set, else read nothing.
+ if (endFormat == null) {
+ assertReadNothing(true);
+ } else {
+ assertReadFormat(true, endFormat);
+ }
+ // If loadingFinished, should read end of stream.
+ assertReadEndOfStream(false);
+ assertReadEndOfStream(true);
+ // Having read end of stream should not affect other cases.
+ assertReadNothing(false);
+ if (endFormat == null) {
+ assertReadNothing(true);
+ } else {
+ assertReadFormat(true, endFormat);
+ }
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} returns {@link C#RESULT_NOTHING_READ}.
+ *
+ * @param formatRequired The value of {@code formatRequired} passed to readData.
+ */
+ private void assertReadNothing(boolean formatRequired) {
+ clearFormatHolderAndInputBuffer();
+ int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0);
+ assertEquals(C.RESULT_NOTHING_READ, result);
+ // formatHolder should not be populated.
+ assertNull(formatHolder.format);
+ // inputBuffer should not be populated.
+ assertInputBufferContainsNoSampleData();
+ assertInputBufferHasNoDefaultFlagsSet();
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the
+ * {@link DecoderInputBuffer#isEndOfStream()} is set.
+ *
+ * @param formatRequired The value of {@code formatRequired} passed to readData.
+ */
+ private void assertReadEndOfStream(boolean formatRequired) {
+ clearFormatHolderAndInputBuffer();
+ int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, true, 0);
+ assertEquals(C.RESULT_BUFFER_READ, result);
+ // formatHolder should not be populated.
+ assertNull(formatHolder.format);
+ // inputBuffer should not contain sample data, but end of stream flag should be set.
+ assertInputBufferContainsNoSampleData();
+ assertTrue(inputBuffer.isEndOfStream());
+ assertFalse(inputBuffer.isDecodeOnly());
+ assertFalse(inputBuffer.isEncrypted());
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} returns {@link C#RESULT_FORMAT_READ} and that the format
+ * holder is filled with a {@link Format} that equals {@code format}.
+ *
+ * @param formatRequired The value of {@code formatRequired} passed to readData.
+ * @param format The expected format.
+ */
+ private void assertReadFormat(boolean formatRequired, Format format) {
+ clearFormatHolderAndInputBuffer();
+ int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0);
+ assertEquals(C.RESULT_FORMAT_READ, result);
+ // formatHolder should be populated.
+ assertEquals(format, formatHolder.format);
+ // inputBuffer should not be populated.
+ assertInputBufferContainsNoSampleData();
+ assertInputBufferHasNoDefaultFlagsSet();
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the buffer is
+ * filled with the specified sample data.
+ *
+ * @param timeUs The expected buffer timestamp.
+ * @param isKeyframe The expected keyframe flag.
+ * @param sampleData An array containing the expected sample data.
+ * @param offset The offset in {@code sampleData} of the expected sample data.
+ * @param length The length of the expected sample data.
+ */
+ private void assertSampleRead(long timeUs, boolean isKeyframe, byte[] sampleData, int offset,
+ int length) {
+ clearFormatHolderAndInputBuffer();
+ int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0);
+ assertEquals(C.RESULT_BUFFER_READ, result);
+ // formatHolder should not be populated.
+ assertNull(formatHolder.format);
+ // inputBuffer should be populated.
+ assertEquals(timeUs, inputBuffer.timeUs);
+ assertEquals(isKeyframe, inputBuffer.isKeyFrame());
+ assertFalse(inputBuffer.isDecodeOnly());
+ assertFalse(inputBuffer.isEncrypted());
+ inputBuffer.flip();
+ assertEquals(length, inputBuffer.data.limit());
+ byte[] readData = new byte[length];
+ inputBuffer.data.get(readData);
+ MoreAsserts.assertEquals(Arrays.copyOfRange(sampleData, offset, offset + length), readData);
+ }
+
+ /**
+ * Asserts the number of allocations currently in use by {@code sampleQueue}.
+ *
+ * @param count The expected number of allocations.
+ */
+ private void assertAllocationCount(int count) {
+ assertEquals(ALLOCATION_SIZE * count, allocator.getTotalBytesAllocated());
+ }
+
+ /**
+ * Asserts {@code inputBuffer} does not contain any sample data.
+ */
+ private void assertInputBufferContainsNoSampleData() {
+ if (inputBuffer.data == null) {
+ return;
+ }
+ inputBuffer.flip();
+ assertEquals(0, inputBuffer.data.limit());
+ }
+
+ private void assertInputBufferHasNoDefaultFlagsSet() {
+ assertFalse(inputBuffer.isEndOfStream());
+ assertFalse(inputBuffer.isDecodeOnly());
+ assertFalse(inputBuffer.isEncrypted());
+ }
+
+ private void clearFormatHolderAndInputBuffer() {
+ formatHolder.format = null;
+ inputBuffer.clear();
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java
new file mode 100644
index 0000000000..9ed4d79307
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.ssa;
+
+import android.test.InstrumentationTestCase;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Unit test for {@link SsaDecoder}.
+ */
+public final class SsaDecoderTest extends InstrumentationTestCase {
+
+ private static final String EMPTY = "ssa/empty";
+ private static final String TYPICAL = "ssa/typical";
+ private static final String TYPICAL_HEADER_ONLY = "ssa/typical_header";
+ private static final String TYPICAL_DIALOGUE_ONLY = "ssa/typical_dialogue";
+ private static final String TYPICAL_FORMAT_ONLY = "ssa/typical_format";
+ private static final String INVALID_TIMECODES = "ssa/invalid_timecodes";
+ private static final String NO_END_TIMECODES = "ssa/no_end_timecodes";
+
+ public void testDecodeEmpty() throws IOException {
+ SsaDecoder decoder = new SsaDecoder();
+ byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY);
+ SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
+ assertEquals(0, subtitle.getEventTimeCount());
+ assertTrue(subtitle.getCues(0).isEmpty());
+ }
+
+ public void testDecodeTypical() throws IOException {
+ SsaDecoder decoder = new SsaDecoder();
+ byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL);
+ SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
+ assertEquals(6, subtitle.getEventTimeCount());
+ assertTypicalCue1(subtitle, 0);
+ assertTypicalCue2(subtitle, 2);
+ assertTypicalCue3(subtitle, 4);
+ }
+
+ public void testDecodeTypicalWithInitializationData() throws IOException {
+ byte[] headerBytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_HEADER_ONLY);
+ byte[] formatBytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FORMAT_ONLY);
+ ArrayList initializationData = new ArrayList<>();
+ initializationData.add(formatBytes);
+ initializationData.add(headerBytes);
+ SsaDecoder decoder = new SsaDecoder(initializationData);
+ byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_DIALOGUE_ONLY);
+ SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
+ assertEquals(6, subtitle.getEventTimeCount());
+ assertTypicalCue1(subtitle, 0);
+ assertTypicalCue2(subtitle, 2);
+ assertTypicalCue3(subtitle, 4);
+ }
+
+ public void testDecodeInvalidTimecodes() throws IOException {
+ // Parsing should succeed, parsing the third cue only.
+ SsaDecoder decoder = new SsaDecoder();
+ byte[] bytes = TestUtil.getByteArray(getInstrumentation(), INVALID_TIMECODES);
+ SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
+ assertEquals(2, subtitle.getEventTimeCount());
+ assertTypicalCue3(subtitle, 0);
+ }
+
+ public void testDecodeNoEndTimecodes() throws IOException {
+ SsaDecoder decoder = new SsaDecoder();
+ byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES);
+ SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
+ assertEquals(3, subtitle.getEventTimeCount());
+
+ assertEquals(0, subtitle.getEventTime(0));
+ assertEquals("This is the first subtitle.",
+ subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
+
+ assertEquals(2340000, subtitle.getEventTime(1));
+ assertEquals("This is the second subtitle \nwith a newline \nand another.",
+ subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString());
+
+ assertEquals(4560000, subtitle.getEventTime(2));
+ assertEquals("This is the third subtitle, with a comma.",
+ subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
+ }
+
+ private static void assertTypicalCue1(SsaSubtitle subtitle, int eventIndex) {
+ assertEquals(0, subtitle.getEventTime(eventIndex));
+ assertEquals("This is the first subtitle.",
+ subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString());
+ assertEquals(1230000, subtitle.getEventTime(eventIndex + 1));
+ }
+
+ private static void assertTypicalCue2(SsaSubtitle subtitle, int eventIndex) {
+ assertEquals(2340000, subtitle.getEventTime(eventIndex));
+ assertEquals("This is the second subtitle \nwith a newline \nand another.",
+ subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString());
+ assertEquals(3450000, subtitle.getEventTime(eventIndex + 1));
+ }
+
+ private static void assertTypicalCue3(SsaSubtitle subtitle, int eventIndex) {
+ assertEquals(4560000, subtitle.getEventTime(eventIndex));
+ assertEquals("This is the third subtitle, with a comma.",
+ subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString());
+ assertEquals(8900000, subtitle.getEventTime(eventIndex + 1));
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java
index 880a214fb3..167499fcdc 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java
@@ -37,7 +37,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
- // Assert that the subtitle is empty.
+
assertEquals(0, subtitle.getEventTimeCount());
assertTrue(subtitle.getCues(0).isEmpty());
}
@@ -46,6 +46,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
@@ -56,6 +57,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_BYTE_ORDER_MARK);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
@@ -66,6 +68,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_EXTRA_BLANK_LINE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
@@ -77,6 +80,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_TIMECODE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
assertEquals(4, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue3(subtitle, 2);
@@ -87,6 +91,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_SEQUENCE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
assertEquals(4, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue3(subtitle, 2);
@@ -97,6 +102,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_NEGATIVE_TIMESTAMPS);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
assertEquals(2, subtitle.getEventTimeCount());
assertTypicalCue3(subtitle, 0);
}
@@ -106,20 +112,16 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
- // Test event count.
assertEquals(3, subtitle.getEventTimeCount());
- // Test first cue.
assertEquals(0, subtitle.getEventTime(0));
assertEquals("SubRip doesn't technically allow missing end timecodes.",
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
- // Test second cue.
assertEquals(2345000, subtitle.getEventTime(1));
assertEquals("We interpret it to mean that a subtitle extends to the start of the next one.",
subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString());
- // Test third cue.
assertEquals(3456000, subtitle.getEventTime(2));
assertEquals("Or to the end of the media.",
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java
index f471370e4c..d6be100877 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java
@@ -125,25 +125,25 @@ public final class CssParserTest extends InstrumentationTestCase {
String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n";
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput));
StringBuilder builder = new StringBuilder();
- assertEquals(CssParser.parseNextToken(input, builder), "lorem");
- assertEquals(CssParser.parseNextToken(input, builder), ":");
- assertEquals(CssParser.parseNextToken(input, builder), "ipsum");
- assertEquals(CssParser.parseNextToken(input, builder), "{");
- assertEquals(CssParser.parseNextToken(input, builder), "dolor");
- assertEquals(CssParser.parseNextToken(input, builder), "}");
- assertEquals(CssParser.parseNextToken(input, builder), "#sit");
- assertEquals(CssParser.parseNextToken(input, builder), ",");
- assertEquals(CssParser.parseNextToken(input, builder), "amet");
- assertEquals(CssParser.parseNextToken(input, builder), ";");
- assertEquals(CssParser.parseNextToken(input, builder), "lorem");
- assertEquals(CssParser.parseNextToken(input, builder), ":");
- assertEquals(CssParser.parseNextToken(input, builder), "ipsum");
- assertEquals(CssParser.parseNextToken(input, builder), "dolor");
- assertEquals(CssParser.parseNextToken(input, builder), "(");
- assertEquals(CssParser.parseNextToken(input, builder), "(");
- assertEquals(CssParser.parseNextToken(input, builder), ")");
- assertEquals(CssParser.parseNextToken(input, builder), ")");
- assertEquals(CssParser.parseNextToken(input, builder), null);
+ assertEquals("lorem", CssParser.parseNextToken(input, builder));
+ assertEquals(":", CssParser.parseNextToken(input, builder));
+ assertEquals("ipsum", CssParser.parseNextToken(input, builder));
+ assertEquals("{", CssParser.parseNextToken(input, builder));
+ assertEquals("dolor", CssParser.parseNextToken(input, builder));
+ assertEquals("}", CssParser.parseNextToken(input, builder));
+ assertEquals("#sit", CssParser.parseNextToken(input, builder));
+ assertEquals(",", CssParser.parseNextToken(input, builder));
+ assertEquals("amet", CssParser.parseNextToken(input, builder));
+ assertEquals(";", CssParser.parseNextToken(input, builder));
+ assertEquals("lorem", CssParser.parseNextToken(input, builder));
+ assertEquals(":", CssParser.parseNextToken(input, builder));
+ assertEquals("ipsum", CssParser.parseNextToken(input, builder));
+ assertEquals("dolor", CssParser.parseNextToken(input, builder));
+ assertEquals("(", CssParser.parseNextToken(input, builder));
+ assertEquals("(", CssParser.parseNextToken(input, builder));
+ assertEquals(")", CssParser.parseNextToken(input, builder));
+ assertEquals(")", CssParser.parseNextToken(input, builder));
+ assertEquals(null, CssParser.parseNextToken(input, builder));
}
public void testStyleScoreSystem() {
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
index c76e4989d8..e7ff2a6811 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
@@ -15,12 +15,14 @@
*/
package com.google.android.exoplayer2.upstream.cache;
+import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
+
import android.net.Uri;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData;
import com.google.android.exoplayer2.testutil.FakeDataSource;
-import com.google.android.exoplayer2.testutil.FakeDataSource.FakeData;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.util.Util;
@@ -38,27 +40,29 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private static final String KEY_1 = "key 1";
private static final String KEY_2 = "key 2";
- private File cacheDir;
- private SimpleCache simpleCache;
+ private File tempFolder;
+ private SimpleCache cache;
@Override
- protected void setUp() throws Exception {
- cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
- simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
+ public void setUp() throws Exception {
+ super.setUp();
+ tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
+ cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
}
@Override
- protected void tearDown() throws Exception {
- Util.recursiveDelete(cacheDir);
+ public void tearDown() throws Exception {
+ Util.recursiveDelete(tempFolder);
+ super.tearDown();
}
public void testMaxCacheFileSize() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false);
assertReadDataContentLength(cacheDataSource, false, false);
- File[] files = cacheDir.listFiles();
- for (File file : files) {
- if (!file.getName().equals(CachedContentIndex.FILE_NAME)) {
- assertTrue(file.length() <= MAX_CACHE_FILE_SIZE);
+ for (String key : cache.getKeys()) {
+ for (CacheSpan cacheSpan : cache.getCachedSpans(key)) {
+ assertTrue(cacheSpan.length <= MAX_CACHE_FILE_SIZE);
+ assertTrue(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE);
}
}
}
@@ -104,7 +108,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
// Read partial at EOS but don't cross it so length is unknown
CacheDataSource cacheDataSource = createCacheDataSource(false, true);
assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2);
- assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
+ assertEquals(C.LENGTH_UNSET, cache.getContentLength(KEY_1));
// Now do an unbounded request for whole data. This will cause a bounded request from upstream.
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
@@ -124,13 +128,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
CacheDataSource cacheDataSource = createCacheDataSource(false, true,
CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET);
- MoreAsserts.assertEmpty(simpleCache.getKeys());
+ MoreAsserts.assertEmpty(cache.getKeys());
}
public void testReadOnlyCache() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
assertReadDataContentLength(cacheDataSource, false, false);
- assertEquals(0, cacheDir.list().length);
+ assertCacheEmpty(cache);
}
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
@@ -155,30 +159,30 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
assertReadData(cacheDataSource, unknownLength, 0, length);
assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache "
+ "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length,
- simpleCache.getContentLength(KEY_1));
+ cache.getContentLength(KEY_1));
}
private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position,
int length) throws IOException {
- int actualLength = TEST_DATA.length - position;
+ int testDataLength = TEST_DATA.length - position;
if (length != C.LENGTH_UNSET) {
- actualLength = Math.min(actualLength, length);
+ testDataLength = Math.min(testDataLength, length);
}
- assertEquals(unknownLength ? length : actualLength,
+ assertEquals(unknownLength ? length : testDataLength,
cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1)));
byte[] buffer = new byte[100];
- int index = 0;
+ int totalBytesRead = 0;
while (true) {
- int read = cacheDataSource.read(buffer, index, buffer.length - index);
+ int read = cacheDataSource.read(buffer, totalBytesRead, buffer.length - totalBytesRead);
if (read == C.RESULT_END_OF_INPUT) {
break;
}
- index += read;
+ totalBytesRead += read;
}
- assertEquals(actualLength, index);
- MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + actualLength),
- Arrays.copyOf(buffer, index));
+ assertEquals(testDataLength, totalBytesRead);
+ MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + testDataLength),
+ Arrays.copyOf(buffer, totalBytesRead));
cacheDataSource.close();
}
@@ -192,7 +196,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private CacheDataSource createCacheDataSource(boolean setReadException,
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
return createCacheDataSource(setReadException, simulateUnknownLength, flags,
- new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE));
+ new CacheDataSink(cache, MAX_CACHE_FILE_SIZE));
}
private CacheDataSource createCacheDataSource(boolean setReadException,
@@ -204,7 +208,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
if (setReadException) {
fakeData.appendReadError(new IOException("Shouldn't read from upstream"));
}
- return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink,
+ return new CacheDataSource(cache, upstream, new FileDataSource(), cacheWriteDataSink,
flags, null);
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
new file mode 100644
index 0000000000..df9975d43b
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream.cache;
+
+import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
+import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
+
+import android.net.Uri;
+import android.test.InstrumentationTestCase;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.FakeDataSet;
+import com.google.android.exoplayer2.testutil.FakeDataSource;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.File;
+import org.mockito.Answers;
+import org.mockito.Mock;
+
+/**
+ * Tests {@link CacheUtil}.
+ */
+public class CacheUtilTest extends InstrumentationTestCase {
+
+ /**
+ * Abstract fake Cache implementation used by the test. This class must be public so Mockito can
+ * create a proxy for it.
+ */
+ public abstract static class AbstractFakeCache implements Cache {
+
+ // This array is set to alternating length of cached and not cached regions in tests:
+ // spansAndGaps = {, ,
+ // , , ... }
+ // Ideally it should end with a cached region but it shouldn't matter for any code.
+ private int[] spansAndGaps;
+ private long contentLength;
+
+ private void init() {
+ spansAndGaps = new int[] {};
+ contentLength = C.LENGTH_UNSET;
+ }
+
+ @Override
+ public long getCachedBytes(String key, long position, long length) {
+ for (int i = 0; i < spansAndGaps.length; i++) {
+ int spanOrGap = spansAndGaps[i];
+ if (position < spanOrGap) {
+ long left = Math.min(spanOrGap - position, length);
+ return (i & 1) == 1 ? -left : left;
+ }
+ position -= spanOrGap;
+ }
+ return -length;
+ }
+
+ @Override
+ public long getContentLength(String key) {
+ return contentLength;
+ }
+ }
+
+ @Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache;
+ private File tempFolder;
+ private SimpleCache cache;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ TestUtil.setUpMockito(this);
+ mockCache.init();
+ tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
+ cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ Util.recursiveDelete(tempFolder);
+ super.tearDown();
+ }
+
+ public void testGenerateKey() throws Exception {
+ assertNotNull(CacheUtil.generateKey(Uri.EMPTY));
+
+ Uri testUri = Uri.parse("test");
+ String key = CacheUtil.generateKey(testUri);
+ assertNotNull(key);
+
+ // Should generate the same key for the same input
+ assertEquals(key, CacheUtil.generateKey(testUri));
+
+ // Should generate different key for different input
+ assertFalse(key.equals(CacheUtil.generateKey(Uri.parse("test2"))));
+ }
+
+ public void testGetKey() throws Exception {
+ Uri testUri = Uri.parse("test");
+ String key = "key";
+ // If DataSpec.key is present, returns it
+ assertEquals(key, CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, key)));
+ // If not generates a new one using DataSpec.uri
+ assertEquals(CacheUtil.generateKey(testUri),
+ CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, null)));
+ }
+
+ public void testGetCachedNoData() throws Exception {
+ CachingCounters counters = new CachingCounters();
+ CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters);
+
+ assertCounters(counters, 0, 0, C.LENGTH_UNSET);
+ }
+
+ public void testGetCachedDataUnknownLength() throws Exception {
+ // Mock there is 100 bytes cached at the beginning
+ mockCache.spansAndGaps = new int[] {100};
+ CachingCounters counters = new CachingCounters();
+ CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters);
+
+ assertCounters(counters, 100, 0, C.LENGTH_UNSET);
+ }
+
+ public void testGetCachedNoDataKnownLength() throws Exception {
+ mockCache.contentLength = 1000;
+ CachingCounters counters = new CachingCounters();
+ CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters);
+
+ assertCounters(counters, 0, 0, 1000);
+ }
+
+ public void testGetCached() throws Exception {
+ mockCache.contentLength = 1000;
+ mockCache.spansAndGaps = new int[] {100, 100, 200};
+ CachingCounters counters = new CachingCounters();
+ CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters);
+
+ assertCounters(counters, 300, 0, 1000);
+ }
+
+ public void testCache() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ CachingCounters counters = new CachingCounters();
+ CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, counters);
+
+ assertCounters(counters, 0, 100, 100);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheSetOffsetAndLength() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri testUri = Uri.parse("test_data");
+ DataSpec dataSpec = new DataSpec(testUri, 10, 20, null);
+ CachingCounters counters = new CachingCounters();
+ CacheUtil.cache(dataSpec, cache, dataSource, counters);
+
+ assertCounters(counters, 0, 20, 20);
+
+ CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters);
+
+ assertCounters(counters, 20, 80, 100);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheUnknownLength() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data")
+ .setSimulateUnknownLength(true)
+ .appendReadData(TestUtil.buildTestData(100)).endData();
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ DataSpec dataSpec = new DataSpec(Uri.parse("test_data"));
+ CachingCounters counters = new CachingCounters();
+ CacheUtil.cache(dataSpec, cache, dataSource, counters);
+
+ assertCounters(counters, 0, 100, 100);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheUnknownLengthPartialCaching() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data")
+ .setSimulateUnknownLength(true)
+ .appendReadData(TestUtil.buildTestData(100)).endData();
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri testUri = Uri.parse("test_data");
+ DataSpec dataSpec = new DataSpec(testUri, 10, 20, null);
+ CachingCounters counters = new CachingCounters();
+ CacheUtil.cache(dataSpec, cache, dataSource, counters);
+
+ assertCounters(counters, 0, 20, 20);
+
+ CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters);
+
+ assertCounters(counters, 20, 80, 100);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheLengthExceedsActualDataLength() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri testUri = Uri.parse("test_data");
+ DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null);
+ CachingCounters counters = new CachingCounters();
+ CacheUtil.cache(dataSpec, cache, dataSource, counters);
+
+ assertCounters(counters, 0, 100, 1000);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheThrowEOFException() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri testUri = Uri.parse("test_data");
+ DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null);
+
+ try {
+ CacheUtil.cache(dataSpec, cache, new CacheDataSource(cache, dataSource),
+ new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null,
+ /*enableEOFException*/ true);
+ fail();
+ } catch (EOFException e) {
+ // Do nothing.
+ }
+ }
+
+ public void testCachePolling() throws Exception {
+ final CachingCounters counters = new CachingCounters();
+ FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data")
+ .appendReadData(TestUtil.buildTestData(100))
+ .appendReadAction(new Runnable() {
+ @Override
+ public void run() {
+ assertCounters(counters, 0, 100, 300);
+ }
+ })
+ .appendReadData(TestUtil.buildTestData(100))
+ .appendReadAction(new Runnable() {
+ @Override
+ public void run() {
+ assertCounters(counters, 0, 200, 300);
+ }
+ })
+ .appendReadData(TestUtil.buildTestData(100)).endData();
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, counters);
+
+ assertCounters(counters, 0, 300, 300);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testRemove() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri uri = Uri.parse("test_data");
+ CacheUtil.cache(new DataSpec(uri), cache,
+ // set maxCacheFileSize to 10 to make sure there are multiple spans
+ new CacheDataSource(cache, dataSource, 0, 10),
+ new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null, true);
+ CacheUtil.remove(cache, CacheUtil.generateKey(uri));
+
+ assertCacheEmpty(cache);
+ }
+
+ private static void assertCounters(CachingCounters counters, int alreadyCachedBytes,
+ int newlyCachedBytes, int contentLength) {
+ assertEquals(alreadyCachedBytes, counters.alreadyCachedBytes);
+ assertEquals(newlyCachedBytes, counters.newlyCachedBytes);
+ assertEquals(contentLength, counters.contentLength);
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
index 396584a39e..a88a1dd615 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
@@ -96,7 +96,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
this.stream = stream;
readEndOfStream = false;
streamOffsetUs = offsetUs;
- onStreamChanged(formats);
+ onStreamChanged(formats, offsetUs);
}
@Override
@@ -183,16 +183,19 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* The default implementation is a no-op.
*
* @param formats The enabled formats.
+ * @param offsetUs The offset that will be added to the timestamps of buffers read via
+ * {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input
+ * buffers have monotonically increasing timestamps.
* @throws ExoPlaybackException If an error occurs.
*/
- protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
// Do nothing.
}
/**
* Called when the position is reset. This occurs when the renderer is enabled after
- * {@link #onStreamChanged(Format[])} has been called, and also when a position discontinuity
- * is encountered.
+ * {@link #onStreamChanged(Format[], long)} has been called, and also when a position
+ * discontinuity is encountered.
*
* After a position reset, the renderer's {@link SampleStream} is guaranteed to provide samples
* starting from a key frame.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java
index 35a69df39e..d7d0ed40aa 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java
@@ -31,6 +31,7 @@ import java.util.UUID;
/**
* Defines constants used by the library.
*/
+@SuppressWarnings("InlinedApi")
public final class C {
private C() {}
@@ -83,12 +84,12 @@ public final class C {
public static final String UTF16_NAME = "UTF-16";
/**
- * * The name of the serif font family.
+ * The name of the serif font family.
*/
public static final String SERIF_NAME = "serif";
/**
- * * The name of the sans-serif font family.
+ * The name of the sans-serif font family.
*/
public static final String SANS_SERIF_NAME = "sans-serif";
@@ -101,24 +102,20 @@ public final class C {
/**
* @see MediaCodec#CRYPTO_MODE_UNENCRYPTED
*/
- @SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
/**
* @see MediaCodec#CRYPTO_MODE_AES_CTR
*/
- @SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
/**
* @see MediaCodec#CRYPTO_MODE_AES_CBC
*/
- @SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC;
/**
* Represents an unset {@link android.media.AudioTrack} session identifier. Equal to
* {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
*/
- @SuppressWarnings("InlinedApi")
public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;
/**
@@ -160,28 +157,24 @@ public final class C {
/**
* @see AudioFormat#ENCODING_AC3
*/
- @SuppressWarnings("InlinedApi")
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/**
* @see AudioFormat#ENCODING_E_AC3
*/
- @SuppressWarnings("InlinedApi")
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
/**
* @see AudioFormat#ENCODING_DTS
*/
- @SuppressWarnings("InlinedApi")
public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
/**
* @see AudioFormat#ENCODING_DTS_HD
*/
- @SuppressWarnings("InlinedApi")
public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
/**
* @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND
*/
- @SuppressWarnings({"InlinedApi", "deprecation"})
+ @SuppressWarnings("deprecation")
public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23
? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
@@ -189,13 +182,17 @@ public final class C {
* Stream types for an {@link android.media.AudioTrack}.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING,
- STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL})
+ @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_DTMF, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION,
+ STREAM_TYPE_RING, STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL, STREAM_TYPE_USE_DEFAULT})
public @interface StreamType {}
/**
* @see AudioManager#STREAM_ALARM
*/
public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
+ /**
+ * @see AudioManager#STREAM_DTMF
+ */
+ public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF;
/**
* @see AudioManager#STREAM_MUSIC
*/
@@ -216,11 +213,143 @@ public final class C {
* @see AudioManager#STREAM_VOICE_CALL
*/
public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
+ /**
+ * @see AudioManager#USE_DEFAULT_STREAM_TYPE
+ */
+ public static final int STREAM_TYPE_USE_DEFAULT = AudioManager.USE_DEFAULT_STREAM_TYPE;
/**
* The default stream type used by audio renderers.
*/
public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
+ /**
+ * Content types for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CONTENT_TYPE_MOVIE, CONTENT_TYPE_MUSIC, CONTENT_TYPE_SONIFICATION, CONTENT_TYPE_SPEECH,
+ CONTENT_TYPE_UNKNOWN})
+ public @interface AudioContentType {}
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE
+ */
+ public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE;
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC
+ */
+ public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION
+ */
+ public static final int CONTENT_TYPE_SONIFICATION =
+ android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION;
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH
+ */
+ public static final int CONTENT_TYPE_SPEECH =
+ android.media.AudioAttributes.CONTENT_TYPE_SPEECH;
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN
+ */
+ public static final int CONTENT_TYPE_UNKNOWN =
+ android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN;
+
+ /**
+ * Flags for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
+ *
+ * Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting the
+ * flag when tunneling is enabled via a track selector.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {FLAG_AUDIBILITY_ENFORCED})
+ public @interface AudioFlags {}
+ /**
+ * @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED
+ */
+ public static final int FLAG_AUDIBILITY_ENFORCED =
+ android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED;
+
+ /**
+ * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({USAGE_ALARM, USAGE_ASSISTANCE_ACCESSIBILITY, USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
+ USAGE_ASSISTANCE_SONIFICATION, USAGE_GAME, USAGE_MEDIA, USAGE_NOTIFICATION,
+ USAGE_NOTIFICATION_COMMUNICATION_DELAYED, USAGE_NOTIFICATION_COMMUNICATION_INSTANT,
+ USAGE_NOTIFICATION_COMMUNICATION_REQUEST, USAGE_NOTIFICATION_EVENT,
+ USAGE_NOTIFICATION_RINGTONE, USAGE_UNKNOWN, USAGE_VOICE_COMMUNICATION,
+ USAGE_VOICE_COMMUNICATION_SIGNALLING})
+ public @interface AudioUsage {}
+ /**
+ * @see android.media.AudioAttributes#USAGE_ALARM
+ */
+ public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM;
+ /**
+ * @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY
+ */
+ public static final int USAGE_ASSISTANCE_ACCESSIBILITY =
+ android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
+ /**
+ * @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE
+ */
+ public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE =
+ android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
+ /**
+ * @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION
+ */
+ public static final int USAGE_ASSISTANCE_SONIFICATION =
+ android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
+ /**
+ * @see android.media.AudioAttributes#USAGE_GAME
+ */
+ public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME;
+ /**
+ * @see android.media.AudioAttributes#USAGE_MEDIA
+ */
+ public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION
+ */
+ public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED
+ */
+ public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT
+ */
+ public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST
+ */
+ public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT
+ */
+ public static final int USAGE_NOTIFICATION_EVENT =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE
+ */
+ public static final int USAGE_NOTIFICATION_RINGTONE =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
+ /**
+ * @see android.media.AudioAttributes#USAGE_UNKNOWN
+ */
+ public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN;
+ /**
+ * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION
+ */
+ public static final int USAGE_VOICE_COMMUNICATION =
+ android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
+ /**
+ * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING
+ */
+ public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =
+ android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
+
/**
* Flags which can apply to a buffer containing a media sample.
*/
@@ -231,12 +360,10 @@ public final class C {
/**
* Indicates that a buffer holds a synchronization sample.
*/
- @SuppressWarnings("InlinedApi")
public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME;
/**
* Flag for empty buffers that signal that the end of the stream was reached.
*/
- @SuppressWarnings("InlinedApi")
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
/**
* Indicates that a buffer is (at least partially) encrypted.
@@ -256,13 +383,11 @@ public final class C {
/**
* @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
*/
- @SuppressWarnings("InlinedApi")
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT;
/**
* @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
*/
- @SuppressWarnings("InlinedApi")
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;
/**
@@ -453,6 +578,26 @@ public final class C {
public static final int DEFAULT_MUXED_BUFFER_SIZE = DEFAULT_VIDEO_BUFFER_SIZE
+ DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE;
+ /**
+ * "cenc" scheme type name as defined in ISO/IEC 23001-7:2016.
+ */
+ public static final String CENC_TYPE_cenc = "cenc";
+
+ /**
+ * "cbc1" scheme type name as defined in ISO/IEC 23001-7:2016.
+ */
+ public static final String CENC_TYPE_cbc1 = "cbc1";
+
+ /**
+ * "cens" scheme type name as defined in ISO/IEC 23001-7:2016.
+ */
+ public static final String CENC_TYPE_cens = "cens";
+
+ /**
+ * "cbcs" scheme type name as defined in ISO/IEC 23001-7:2016.
+ */
+ public static final String CENC_TYPE_cbcs = "cbcs";
+
/**
* The Nil UUID as defined by
* RFC4122.
@@ -498,16 +643,25 @@ public final class C {
/**
* A type of a message that can be passed to an audio {@link Renderer} via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
- * should be one of the integer stream types in {@link C.StreamType}, and will specify the stream
- * type of the underlying {@link android.media.AudioTrack}. See also
- * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type
- * is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}.
+ * should be an {@link com.google.android.exoplayer2.audio.AudioAttributes} instance that will
+ * configure the underlying audio track. If not set, the default audio attributes will be used.
+ * They are suitable for general media playback.
*
- * Note that when the stream type changes, the AudioTrack must be reinitialized, which can
- * introduce a brief gap in audio output. Note also that tracks in the same audio session must
- * share the same routing, so a new audio session id will be generated.
+ * Setting the audio attributes during playback may introduce a short gap in audio output as the
+ * audio track is recreated. A new audio session id will also be generated.
+ *
+ * If tunneling is enabled by the track selector, the specified audio attributes will be ignored,
+ * but they will take effect if audio is later played without tunneling.
+ *
+ * If the device is running a build before platform API version 21, audio attributes cannot be set
+ * directly on the underlying audio track. In this case, the usage will be mapped onto an
+ * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.
+ *
+ * To get audio attributes that are equivalent to a legacy stream type, pass the stream type to
+ * {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link C.AudioUsage} to build
+ * an audio attributes instance.
*/
- public static final int MSG_SET_STREAM_TYPE = 3;
+ public static final int MSG_SET_AUDIO_ATTRIBUTES = 3;
/**
* The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
@@ -564,17 +718,14 @@ public final class C {
/**
* @see MediaFormat#COLOR_STANDARD_BT709
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
/**
* @see MediaFormat#COLOR_STANDARD_BT601_PAL
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;
/**
* @see MediaFormat#COLOR_STANDARD_BT2020
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
/**
@@ -586,17 +737,14 @@ public final class C {
/**
* @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO;
/**
* @see MediaFormat#COLOR_TRANSFER_ST2084
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084;
/**
* @see MediaFormat#COLOR_TRANSFER_HLG
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
/**
@@ -608,12 +756,10 @@ public final class C {
/**
* @see MediaFormat#COLOR_RANGE_LIMITED
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED;
/**
* @see MediaFormat#COLOR_RANGE_FULL
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
/**
@@ -632,24 +778,24 @@ public final class C {
/**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving
- * {@link #TIME_UNSET} values.
+ * {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
*
* @param timeUs The time in microseconds.
* @return The corresponding time in milliseconds.
*/
public static long usToMs(long timeUs) {
- return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000);
+ return (timeUs == TIME_UNSET || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000);
}
/**
* Converts a time in milliseconds to the corresponding time in microseconds, preserving
- * {@link #TIME_UNSET} values.
+ * {@link #TIME_UNSET} values and {@link #TIME_END_OF_SOURCE} values.
*
* @param timeMs The time in milliseconds.
* @return The corresponding time in microseconds.
*/
public static long msToUs(long timeMs) {
- return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000);
+ return (timeMs == TIME_UNSET || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index 18bf9eeb8c..b096b5ae12 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -16,29 +16,27 @@
package com.google.android.exoplayer2;
import android.os.Looper;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
+import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
-import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
/**
- * An extensible media player exposing traditional high-level media player functionality, such as
- * the ability to buffer media, play, pause and seek. Instances can be obtained from
+ * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from
* {@link ExoPlayerFactory}.
*
- *
Player composition
+ *
Player components
*
ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the
* type of the media being played, how and where it is stored, and how it is rendered. Rather than
* implementing the loading and rendering of media directly, ExoPlayer implementations delegate this
@@ -46,18 +44,20 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* Components common to all ExoPlayer implementations are:
*
*
A {@link MediaSource} that defines the media to be played, loads the media, and from
- * which the loaded media can be read. A MediaSource is injected via {@link #prepare} at the start
- * of playback. The library modules provide default implementations for regular media files
- * ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS
- * (HlsMediaSource), implementations for merging ({@link MergingMediaSource}) and concatenating
- * ({@link ConcatenatingMediaSource}) other MediaSources, and an implementation for loading single
- * samples ({@link SingleSampleMediaSource}) most often used for side-loaded subtitle and closed
- * caption files.
+ * which the loaded media can be read. A MediaSource is injected via {@link #prepare(MediaSource)}
+ * at the start of playback. The library modules provide default implementations for regular media
+ * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource)
+ * and HLS (HlsMediaSource), an implementation for loading single media samples
+ * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and
+ * implementations for building more complex MediaSources from simpler ones
+ * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource},
+ * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and
+ * {@link ClippingMediaSource}).
*
{@link Renderer}s that render individual components of the media. The library
* provides default implementations for common media types ({@link MediaCodecVideoRenderer},
* {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer
- * consumes media of its corresponding type from the MediaSource being played. Renderers are
- * injected when the player is created.
+ * consumes media from the MediaSource being played. Renderers are injected when the player is
+ * created.
*
A {@link TrackSelector} that selects tracks provided by the MediaSource to be
* consumed by each of the available Renderers. The library provides a default implementation
* ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when
@@ -70,14 +70,14 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
*
An ExoPlayer can be built using the default components provided by the library, but may also
* be built using custom implementations if non-standard behaviors are required. For example a
* custom LoadControl could be injected to change the player's buffering strategy, or a custom
- * Renderer could be injected to use a video codec not supported natively by Android.
+ * Renderer could be injected to add support for a video codec not supported natively by Android.
*
*
The concept of injecting components that implement pieces of player functionality is present
* throughout the library. The default component implementations listed above delegate work to
* further injected components. This allows many sub-components to be individually replaced with
* custom implementations. For example the default MediaSource implementations require one or more
* {@link DataSource} factories to be injected via their constructors. By providing a custom factory
- * it's possible to load data from a non-standard source or through a different network stack.
+ * it's possible to load data from a non-standard source, or through a different network stack.
*
*
Threading model
*
The figure below shows ExoPlayer's threading model.
@@ -103,87 +103,16 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* thread via a second message queue. The application thread consumes messages from the queue,
* updating the application visible state and calling corresponding listener methods.
*
Injected player components may use additional background threads. For example a MediaSource
- * may use a background thread to load data. These are implementation specific.
+ * may use background threads to load data. These are implementation specific.
*
*/
-public interface ExoPlayer {
+public interface ExoPlayer extends Player {
/**
- * Listener of changes in player state.
+ * @deprecated Use {@link Player.EventListener} instead.
*/
- interface EventListener {
-
- /**
- * Called when the timeline and/or manifest has been refreshed.
- *
- * Note that if the timeline has changed then a position discontinuity may also have occurred.
- * For example, the current period index may have changed as a result of periods being added or
- * removed from the timeline. This will not be reported via a separate call to
- * {@link #onPositionDiscontinuity()}.
- *
- * @param timeline The latest timeline. Never null, but may be empty.
- * @param manifest The latest manifest. May be null.
- */
- void onTimelineChanged(Timeline timeline, Object manifest);
-
- /**
- * Called when the available or selected tracks change.
- *
- * @param trackGroups The available tracks. Never null, but may be of length zero.
- * @param trackSelections The track selections for each {@link Renderer}. Never null and always
- * of length {@link #getRendererCount()}, but may contain null elements.
- */
- void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
-
- /**
- * Called when the player starts or stops loading the source.
- *
- * @param isLoading Whether the source is currently being loaded.
- */
- void onLoadingChanged(boolean isLoading);
-
- /**
- * Called when the value returned from either {@link #getPlayWhenReady()} or
- * {@link #getPlaybackState()} changes.
- *
- * @param playWhenReady Whether playback will proceed when ready.
- * @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer}
- * interface.
- */
- void onPlayerStateChanged(boolean playWhenReady, int playbackState);
-
- /**
- * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
- * immediately after this method is called. The player instance can still be used, and
- * {@link #release()} must still be called on the player should it no longer be required.
- *
- * @param error The error.
- */
- void onPlayerError(ExoPlaybackException error);
-
- /**
- * Called when a position discontinuity occurs without a change to the timeline. A position
- * discontinuity occurs when the current window or period index changes (as a result of playback
- * transitioning from one period in the timeline to the next), or when the playback position
- * jumps within the period currently being played (as a result of a seek being performed, or
- * when the source introduces a discontinuity internally).
- *
- * When a position discontinuity occurs as a result of a change to the timeline this method is
- * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.
- */
- void onPositionDiscontinuity();
-
- /**
- * Called when the current playback parameters change. The playback parameters may change due to
- * a call to {@link ExoPlayer#setPlaybackParameters(PlaybackParameters)}, or the player itself
- * may change them (for example, if audio playback switches to passthrough mode, where speed
- * adjustment is no longer possible).
- *
- * @param playbackParameters The playback parameters.
- */
- void onPlaybackParametersChanged(PlaybackParameters playbackParameters);
-
- }
+ @Deprecated
+ interface EventListener extends Player.EventListener {}
/**
* A component of an {@link ExoPlayer} that can receive messages on the playback thread.
@@ -236,47 +165,48 @@ public interface ExoPlayer {
}
/**
- * The player does not have a source to play, so it is neither buffering nor ready to play.
+ * @deprecated Use {@link Player#STATE_IDLE} instead.
*/
- int STATE_IDLE = 1;
+ @Deprecated
+ int STATE_IDLE = Player.STATE_IDLE;
/**
- * The player not able to immediately play from the current position. The cause is
- * {@link Renderer} specific, but this state typically occurs when more data needs to be
- * loaded to be ready to play, or more data needs to be buffered for playback to resume.
+ * @deprecated Use {@link Player#STATE_BUFFERING} instead.
*/
- int STATE_BUFFERING = 2;
+ @Deprecated
+ int STATE_BUFFERING = Player.STATE_BUFFERING;
/**
- * The player is able to immediately play from the current position. The player will be playing if
- * {@link #getPlayWhenReady()} returns true, and paused otherwise.
+ * @deprecated Use {@link Player#STATE_READY} instead.
*/
- int STATE_READY = 3;
+ @Deprecated
+ int STATE_READY = Player.STATE_READY;
/**
- * The player has finished playing the media.
+ * @deprecated Use {@link Player#STATE_ENDED} instead.
*/
- int STATE_ENDED = 4;
+ @Deprecated
+ int STATE_ENDED = Player.STATE_ENDED;
/**
- * Register a listener to receive events from the player. The listener's methods will be called on
- * the thread that was used to construct the player. However, if the thread used to construct the
- * player does not have a {@link Looper}, then the listener will be called on the main thread.
- *
- * @param listener The listener to register.
+ * @deprecated Use {@link Player#REPEAT_MODE_OFF} instead.
*/
- void addListener(EventListener listener);
+ @Deprecated
+ @RepeatMode int REPEAT_MODE_OFF = Player.REPEAT_MODE_OFF;
+ /**
+ * @deprecated Use {@link Player#REPEAT_MODE_ONE} instead.
+ */
+ @Deprecated
+ @RepeatMode int REPEAT_MODE_ONE = Player.REPEAT_MODE_ONE;
+ /**
+ * @deprecated Use {@link Player#REPEAT_MODE_ALL} instead.
+ */
+ @Deprecated
+ @RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL;
/**
- * Unregister a listener. The listener will no longer receive events from the player.
+ * Gets the {@link Looper} associated with the playback thread.
*
- * @param listener The listener to unregister.
+ * @return The {@link Looper} associated with the playback thread.
*/
- void removeListener(EventListener listener);
-
- /**
- * Returns the current state of the player.
- *
- * @return One of the {@code STATE} constants defined in this interface.
- */
- int getPlaybackState();
+ Looper getPlaybackLooper();
/**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
@@ -298,104 +228,6 @@ public interface ExoPlayer {
*/
void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
- /**
- * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
- *
- * If the player is already in the ready state then this method can be used to pause and resume
- * playback.
- *
- * @param playWhenReady Whether playback should proceed when ready.
- */
- void setPlayWhenReady(boolean playWhenReady);
-
- /**
- * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
- *
- * @return Whether playback will proceed when ready.
- */
- boolean getPlayWhenReady();
-
- /**
- * Whether the player is currently loading the source.
- *
- * @return Whether the player is currently loading the source.
- */
- boolean isLoading();
-
- /**
- * Seeks to the default position associated with the current window. The position can depend on
- * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically
- * be the live edge of the window. For other streams it will typically be the start of the window.
- */
- void seekToDefaultPosition();
-
- /**
- * Seeks to the default position associated with the specified window. The position can depend on
- * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically
- * be the live edge of the window. For other streams it will typically be the start of the window.
- *
- * @param windowIndex The index of the window whose associated default position should be seeked
- * to.
- */
- void seekToDefaultPosition(int windowIndex);
-
- /**
- * Seeks to a position specified in milliseconds in the current window.
- *
- * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to
- * the window's default position.
- */
- void seekTo(long positionMs);
-
- /**
- * Seeks to a position specified in milliseconds in the specified window.
- *
- * @param windowIndex The index of the window.
- * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to
- * the window's default position.
- */
- void seekTo(int windowIndex, long positionMs);
-
- /**
- * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the
- * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.
- *
- * Playback parameters changes may cause the player to buffer.
- * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever
- * the currently active playback parameters change. When that listener is called, the parameters
- * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch
- * may be out of range, in which case they are constrained to a set of permitted values. If it is
- * not possible to change the playback parameters, the listener will not be invoked.
- *
- * @param playbackParameters The playback parameters, or {@code null} to use the defaults.
- */
- void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
-
- /**
- * Returns the currently active playback parameters.
- *
- * @see EventListener#onPlaybackParametersChanged(PlaybackParameters)
- */
- PlaybackParameters getPlaybackParameters();
-
- /**
- * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
- * is to pause playback.
- *
- * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The
- * player instance can still be used, and {@link #release()} must still be called on the player if
- * it's no longer required.
- *
- * Calling this method does not reset the playback position.
- */
- void stop();
-
- /**
- * Releases the player. This method must be called when the player is no longer required. The
- * player must not be used after calling this method.
- */
- void release();
-
/**
* Sends messages to their target components. The messages are delivered on the playback thread.
* If a component throws an {@link ExoPlaybackException} then it is propagated out of the player
@@ -413,88 +245,4 @@ public interface ExoPlayer {
*/
void blockingSendMessages(ExoPlayerMessage... messages);
- /**
- * Returns the number of renderers.
- */
- int getRendererCount();
-
- /**
- * Returns the track type that the renderer at a given index handles.
- *
- * @see Renderer#getTrackType()
- * @param index The index of the renderer.
- * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
- */
- int getRendererType(int index);
-
- /**
- * Returns the available track groups.
- */
- TrackGroupArray getCurrentTrackGroups();
-
- /**
- * Returns the current track selections for each renderer.
- */
- TrackSelectionArray getCurrentTrackSelections();
-
- /**
- * Returns the current manifest. The type depends on the {@link MediaSource} passed to
- * {@link #prepare}. May be null.
- */
- Object getCurrentManifest();
-
- /**
- * Returns the current {@link Timeline}. Never null, but may be empty.
- */
- Timeline getCurrentTimeline();
-
- /**
- * Returns the index of the period currently being played.
- */
- int getCurrentPeriodIndex();
-
- /**
- * Returns the index of the window currently being played.
- */
- int getCurrentWindowIndex();
-
- /**
- * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the
- * duration is not known.
- */
- long getDuration();
-
- /**
- * Returns the playback position in the current window, in milliseconds.
- */
- long getCurrentPosition();
-
- /**
- * Returns an estimate of the position in the current window up to which data is buffered, in
- * milliseconds.
- */
- long getBufferedPosition();
-
- /**
- * Returns an estimate of the percentage in the current window up to which data is buffered, or 0
- * if no estimate is available.
- */
- int getBufferedPercentage();
-
- /**
- * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
- * empty.
- *
- * @see Timeline.Window#isDynamic
- */
- boolean isCurrentWindowDynamic();
-
- /**
- * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is
- * empty.
- *
- * @see Timeline.Window#isSeekable
- */
- boolean isCurrentWindowSeekable();
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index c70d729fc3..c3a76cd962 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -24,6 +24,7 @@ import android.util.Log;
import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@@ -45,12 +46,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final TrackSelectionArray emptyTrackSelections;
private final Handler eventHandler;
private final ExoPlayerImplInternal internalPlayer;
- private final CopyOnWriteArraySet listeners;
+ private final CopyOnWriteArraySet listeners;
private final Timeline.Window window;
private final Timeline.Period period;
private boolean tracksSelected;
private boolean playWhenReady;
+ private @RepeatMode int repeatMode;
private int playbackState;
private int pendingSeekAcks;
private int pendingPrepareAcks;
@@ -78,12 +80,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
*/
@SuppressLint("HandlerLeak")
public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
- Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION_SLASHY + " [" + Util.DEVICE_DEBUG_INFO + "]");
+ Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]");
Assertions.checkState(renderers.length > 0);
this.renderers = Assertions.checkNotNull(renderers);
this.trackSelector = Assertions.checkNotNull(trackSelector);
this.playWhenReady = false;
- this.playbackState = STATE_IDLE;
+ this.repeatMode = Player.REPEAT_MODE_OFF;
+ this.playbackState = Player.STATE_IDLE;
this.listeners = new CopyOnWriteArraySet<>();
emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]);
timeline = Timeline.EMPTY;
@@ -101,16 +105,21 @@ import java.util.concurrent.CopyOnWriteArraySet;
};
playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0);
internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady,
- eventHandler, playbackInfo, this);
+ repeatMode, eventHandler, playbackInfo, this);
}
@Override
- public void addListener(EventListener listener) {
+ public Looper getPlaybackLooper() {
+ return internalPlayer.getPlaybackLooper();
+ }
+
+ @Override
+ public void addListener(Player.EventListener listener) {
listeners.add(listener);
}
@Override
- public void removeListener(EventListener listener) {
+ public void removeListener(Player.EventListener listener) {
listeners.remove(listener);
}
@@ -130,7 +139,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (!timeline.isEmpty() || manifest != null) {
timeline = Timeline.EMPTY;
manifest = null;
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(timeline, manifest);
}
}
@@ -139,7 +148,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
trackGroups = TrackGroupArray.EMPTY;
trackSelections = emptyTrackSelections;
trackSelector.onSelectionActivated(null);
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onTracksChanged(trackGroups, trackSelections);
}
}
@@ -153,7 +162,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (this.playWhenReady != playWhenReady) {
this.playWhenReady = playWhenReady;
internalPlayer.setPlayWhenReady(playWhenReady);
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
}
@@ -164,6 +173,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
return playWhenReady;
}
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ if (this.repeatMode != repeatMode) {
+ this.repeatMode = repeatMode;
+ internalPlayer.setRepeatMode(repeatMode);
+ for (Player.EventListener listener : listeners) {
+ listener.onRepeatModeChanged(repeatMode);
+ }
+ }
+ }
+
+ @Override
+ public @RepeatMode int getRepeatMode() {
+ return repeatMode;
+ }
+
@Override
public boolean isLoading() {
return isLoading;
@@ -195,10 +220,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskingPeriodIndex = 0;
} else {
timeline.getWindow(windowIndex, window);
- long resolvedPositionMs =
- positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : positionMs;
+ long resolvedPositionUs =
+ positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : C.msToUs(positionMs);
int periodIndex = window.firstPeriodIndex;
- long periodPositionUs = window.getPositionInFirstPeriodUs() + C.msToUs(resolvedPositionMs);
+ long periodPositionUs = window.getPositionInFirstPeriodUs() + resolvedPositionUs;
long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
&& periodIndex < window.lastPeriodIndex) {
@@ -213,7 +238,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
} else {
maskingWindowPositionMs = positionMs;
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity();
}
}
@@ -239,6 +264,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void release() {
+ Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
+ + ExoPlayerLibraryInfo.registeredModules() + "]");
internalPlayer.release();
eventHandler.removeCallbacksAndMessages(null);
}
@@ -258,7 +286,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingPeriodIndex;
} else {
- return playbackInfo.periodIndex;
+ return playbackInfo.periodId.periodIndex;
}
}
@@ -267,7 +295,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingWindowIndex;
} else {
- return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex;
+ return timeline.getPeriod(playbackInfo.periodId.periodIndex, period).windowIndex;
}
}
@@ -276,7 +304,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty()) {
return C.TIME_UNSET;
}
- return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ if (isPlayingAd()) {
+ MediaPeriodId periodId = playbackInfo.periodId;
+ timeline.getPeriod(periodId.periodIndex, period);
+ long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup);
+ return C.usToMs(adDurationUs);
+ } else {
+ return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ }
}
@Override
@@ -284,7 +319,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingWindowPositionMs;
} else {
- timeline.getPeriod(playbackInfo.periodIndex, period);
+ timeline.getPeriod(playbackInfo.periodId.periodIndex, period);
return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs);
}
}
@@ -295,7 +330,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingWindowPositionMs;
} else {
- timeline.getPeriod(playbackInfo.periodIndex, period);
+ timeline.getPeriod(playbackInfo.periodId.periodIndex, period);
return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs);
}
}
@@ -321,6 +356,31 @@ import java.util.concurrent.CopyOnWriteArraySet;
return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
}
+ @Override
+ public boolean isPlayingAd() {
+ return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getCurrentAdGroupIndex() {
+ return pendingSeekAcks == 0 ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getCurrentAdIndexInAdGroup() {
+ return pendingSeekAcks == 0 ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;
+ }
+
+ @Override
+ public long getContentPosition() {
+ if (isPlayingAd()) {
+ timeline.getPeriod(playbackInfo.periodId.periodIndex, period);
+ return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);
+ } else {
+ return getCurrentPosition();
+ }
+ }
+
@Override
public int getRendererCount() {
return renderers.length;
@@ -360,14 +420,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
playbackState = msg.arg1;
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
break;
}
case ExoPlayerImplInternal.MSG_LOADING_CHANGED: {
isLoading = msg.arg1 != 0;
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onLoadingChanged(isLoading);
}
break;
@@ -379,7 +439,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
trackGroups = trackSelectorResult.groups;
trackSelections = trackSelectorResult.selections;
trackSelector.onSelectionActivated(trackSelectorResult.info);
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onTracksChanged(trackGroups, trackSelections);
}
}
@@ -389,7 +449,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (--pendingSeekAcks == 0) {
playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
if (msg.arg1 != 0) {
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity();
}
}
@@ -399,7 +459,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: {
if (pendingSeekAcks == 0) {
playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity();
}
}
@@ -412,7 +472,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
timeline = sourceInfo.timeline;
manifest = sourceInfo.manifest;
playbackInfo = sourceInfo.playbackInfo;
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(timeline, manifest);
}
}
@@ -422,7 +482,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj;
if (!this.playbackParameters.equals(playbackParameters)) {
this.playbackParameters = playbackParameters;
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onPlaybackParametersChanged(playbackParameters);
}
}
@@ -430,7 +490,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
case ExoPlayerImplInternal.MSG_ERROR: {
ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
- for (EventListener listener : listeners) {
+ for (Player.EventListener listener : listeners) {
listener.onPlayerError(exception);
}
break;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index bf5b3f6482..cb04501fc0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -17,14 +17,18 @@ package com.google.android.exoplayer2;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
+import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo;
+import com.google.android.exoplayer2.source.ClippingMediaPeriod;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@@ -48,21 +52,32 @@ import java.io.IOException;
*/
public static final class PlaybackInfo {
- public final int periodIndex;
+ public final MediaPeriodId periodId;
public final long startPositionUs;
+ public final long contentPositionUs;
public volatile long positionUs;
public volatile long bufferedPositionUs;
public PlaybackInfo(int periodIndex, long startPositionUs) {
- this.periodIndex = periodIndex;
+ this(new MediaPeriodId(periodIndex), startPositionUs);
+ }
+
+ public PlaybackInfo(MediaPeriodId periodId, long startPositionUs) {
+ this(periodId, startPositionUs, C.TIME_UNSET);
+ }
+
+ public PlaybackInfo(MediaPeriodId periodId, long startPositionUs, long contentPositionUs) {
+ this.periodId = periodId;
this.startPositionUs = startPositionUs;
+ this.contentPositionUs = contentPositionUs;
positionUs = startPositionUs;
bufferedPositionUs = startPositionUs;
}
public PlaybackInfo copyWithPeriodIndex(int periodIndex) {
- PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs);
+ PlaybackInfo playbackInfo = new PlaybackInfo(periodId.copyWithPeriodIndex(periodIndex),
+ startPositionUs, contentPositionUs);
playbackInfo.positionUs = positionUs;
playbackInfo.bufferedPositionUs = bufferedPositionUs;
return playbackInfo;
@@ -112,6 +127,7 @@ import java.io.IOException;
private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
private static final int MSG_CUSTOM = 11;
+ private static final int MSG_SET_REPEAT_MODE = 12;
private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
@@ -143,6 +159,7 @@ import java.io.IOException;
private final ExoPlayer player;
private final Timeline.Window window;
private final Timeline.Period period;
+ private final MediaPeriodInfoSequence mediaPeriodInfoSequence;
private PlaybackInfo playbackInfo;
private PlaybackParameters playbackParameters;
@@ -155,6 +172,7 @@ import java.io.IOException;
private boolean rebuffering;
private boolean isLoading;
private int state;
+ private @Player.RepeatMode int repeatMode;
private int customMessagesSent;
private int customMessagesProcessed;
private long elapsedRealtimeUs;
@@ -170,14 +188,15 @@ import java.io.IOException;
private Timeline timeline;
public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector,
- LoadControl loadControl, boolean playWhenReady, Handler eventHandler,
- PlaybackInfo playbackInfo, ExoPlayer player) {
+ LoadControl loadControl, boolean playWhenReady, @Player.RepeatMode int repeatMode,
+ Handler eventHandler, PlaybackInfo playbackInfo, ExoPlayer player) {
this.renderers = renderers;
this.trackSelector = trackSelector;
this.loadControl = loadControl;
this.playWhenReady = playWhenReady;
+ this.repeatMode = repeatMode;
this.eventHandler = eventHandler;
- this.state = ExoPlayer.STATE_IDLE;
+ this.state = Player.STATE_IDLE;
this.playbackInfo = playbackInfo;
this.player = player;
@@ -190,6 +209,7 @@ import java.io.IOException;
enabledRenderers = new Renderer[0];
window = new Timeline.Window();
period = new Timeline.Period();
+ mediaPeriodInfoSequence = new MediaPeriodInfoSequence();
trackSelector.init(this);
playbackParameters = PlaybackParameters.DEFAULT;
@@ -210,6 +230,10 @@ import java.io.IOException;
handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
}
+ public void setRepeatMode(@Player.RepeatMode int repeatMode) {
+ handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget();
+ }
+
public void seekTo(Timeline timeline, int windowIndex, long positionUs) {
handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs))
.sendToTarget();
@@ -263,6 +287,10 @@ import java.io.IOException;
internalPlaybackThread.quit();
}
+ public Looper getPlaybackLooper() {
+ return internalPlaybackThread.getLooper();
+ }
+
// MediaSource.Listener implementation.
@Override
@@ -304,6 +332,10 @@ import java.io.IOException;
setPlayWhenReadyInternal(msg.arg1 != 0);
return true;
}
+ case MSG_SET_REPEAT_MODE: {
+ setRepeatModeInternal(msg.arg1);
+ return true;
+ }
case MSG_DO_SOME_WORK: {
doSomeWork();
return true;
@@ -391,7 +423,7 @@ import java.io.IOException;
}
this.mediaSource = mediaSource;
mediaSource.prepareSource(player, true, this);
- setState(ExoPlayer.STATE_BUFFERING);
+ setState(Player.STATE_BUFFERING);
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
@@ -402,15 +434,69 @@ import java.io.IOException;
stopRenderers();
updatePlaybackPositions();
} else {
- if (state == ExoPlayer.STATE_READY) {
+ if (state == Player.STATE_READY) {
startRenderers();
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
- } else if (state == ExoPlayer.STATE_BUFFERING) {
+ } else if (state == Player.STATE_BUFFERING) {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
}
+ private void setRepeatModeInternal(@Player.RepeatMode int repeatMode)
+ throws ExoPlaybackException {
+ this.repeatMode = repeatMode;
+ mediaPeriodInfoSequence.setRepeatMode(repeatMode);
+
+ // Find the last existing period holder that matches the new period order.
+ MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null
+ ? playingPeriodHolder : loadingPeriodHolder;
+ if (lastValidPeriodHolder == null) {
+ return;
+ }
+ while (true) {
+ int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex,
+ period, window, repeatMode);
+ while (lastValidPeriodHolder.next != null
+ && !lastValidPeriodHolder.info.isLastInTimelinePeriod) {
+ lastValidPeriodHolder = lastValidPeriodHolder.next;
+ }
+ if (nextPeriodIndex == C.INDEX_UNSET || lastValidPeriodHolder.next == null
+ || lastValidPeriodHolder.next.info.id.periodIndex != nextPeriodIndex) {
+ break;
+ }
+ lastValidPeriodHolder = lastValidPeriodHolder.next;
+ }
+
+ // Release any period holders that don't match the new period order.
+ int loadingPeriodHolderIndex = loadingPeriodHolder.index;
+ int readingPeriodHolderIndex =
+ readingPeriodHolder != null ? readingPeriodHolder.index : C.INDEX_UNSET;
+ if (lastValidPeriodHolder.next != null) {
+ releasePeriodHoldersFrom(lastValidPeriodHolder.next);
+ lastValidPeriodHolder.next = null;
+ }
+
+ // Update the period info for the last holder, as it may now be the last period in the timeline.
+ lastValidPeriodHolder.info =
+ mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info);
+
+ // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed.
+ boolean seenLoadingPeriodHolder = loadingPeriodHolderIndex <= lastValidPeriodHolder.index;
+ if (!seenLoadingPeriodHolder) {
+ loadingPeriodHolder = lastValidPeriodHolder;
+ }
+ boolean seenReadingPeriodHolder = readingPeriodHolderIndex != C.INDEX_UNSET
+ && readingPeriodHolderIndex <= lastValidPeriodHolder.index;
+ if (!seenReadingPeriodHolder && playingPeriodHolder != null) {
+ // Renderers may have read from a period that's been removed. Seek back to the current
+ // position of the playing period to make sure none of the removed period is played.
+ MediaPeriodId periodId = playingPeriodHolder.info.id;
+ long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs);
+ playbackInfo = new PlaybackInfo(periodId, newPositionUs, playbackInfo.contentPositionUs);
+ }
+ }
+
private void startRenderers() throws ExoPlaybackException {
rebuffering = false;
standaloneMediaClock.start();
@@ -451,8 +537,7 @@ import java.io.IOException;
long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE
: playingPeriodHolder.mediaPeriod.getBufferedPositionUs();
playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE
- ? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs()
- : bufferedPositionUs;
+ ? playingPeriodHolder.info.durationUs : bufferedPositionUs;
}
private void doSomeWork() throws ExoPlaybackException, IOException {
@@ -504,43 +589,43 @@ import java.io.IOException;
}
}
- long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period)
- .getDurationUs();
+ long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
if (allRenderersEnded
&& (playingPeriodDurationUs == C.TIME_UNSET
|| playingPeriodDurationUs <= playbackInfo.positionUs)
- && playingPeriodHolder.isLast) {
- setState(ExoPlayer.STATE_ENDED);
+ && playingPeriodHolder.info.isFinal) {
+ setState(Player.STATE_ENDED);
stopRenderers();
- } else if (state == ExoPlayer.STATE_BUFFERING) {
+ } else if (state == Player.STATE_BUFFERING) {
boolean isNewlyReady = enabledRenderers.length > 0
- ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering))
+ ? (allRenderersReadyOrEnded
+ && loadingPeriodHolder.haveSufficientBuffer(rebuffering, rendererPositionUs))
: isTimelineReady(playingPeriodDurationUs);
if (isNewlyReady) {
- setState(ExoPlayer.STATE_READY);
+ setState(Player.STATE_READY);
if (playWhenReady) {
startRenderers();
}
}
- } else if (state == ExoPlayer.STATE_READY) {
+ } else if (state == Player.STATE_READY) {
boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded
: isTimelineReady(playingPeriodDurationUs);
if (!isStillReady) {
rebuffering = playWhenReady;
- setState(ExoPlayer.STATE_BUFFERING);
+ setState(Player.STATE_BUFFERING);
stopRenderers();
}
}
- if (state == ExoPlayer.STATE_BUFFERING) {
+ if (state == Player.STATE_BUFFERING) {
for (Renderer renderer : enabledRenderers) {
renderer.maybeThrowStreamError();
}
}
- if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
+ if ((playWhenReady && state == Player.STATE_READY) || state == Player.STATE_BUFFERING) {
scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS);
- } else if (enabledRenderers.length != 0) {
+ } else if (enabledRenderers.length != 0 && state != Player.STATE_ENDED) {
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
} else {
handler.removeMessages(MSG_DO_SOME_WORK);
@@ -576,7 +661,7 @@ import java.io.IOException;
// Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't
// ignored.
playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
- setState(ExoPlayer.STATE_ENDED);
+ setState(Player.STATE_ENDED);
// Reset, but retain the source so that it can still be used should a seek occur.
resetInternal(false);
return;
@@ -585,28 +670,34 @@ import java.io.IOException;
boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
int periodIndex = periodPosition.first;
long periodPositionUs = periodPosition.second;
-
+ long contentPositionUs = periodPositionUs;
+ MediaPeriodId periodId =
+ mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs);
+ if (periodId.isAd()) {
+ seekPositionAdjusted = true;
+ periodPositionUs = 0;
+ }
try {
- if (periodIndex == playbackInfo.periodIndex
+ if (periodId.equals(playbackInfo.periodId)
&& ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) {
// Seek position equals the current position. Do nothing.
return;
}
- long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs);
+ long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs);
seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
periodPositionUs = newPeriodPositionUs;
} finally {
- playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs);
+ playbackInfo = new PlaybackInfo(periodId, periodPositionUs, contentPositionUs);
eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo)
.sendToTarget();
}
}
- private long seekToPeriodPosition(int periodIndex, long periodPositionUs)
+ private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs)
throws ExoPlaybackException {
stopRenderers();
rebuffering = false;
- setState(ExoPlayer.STATE_BUFFERING);
+ setState(Player.STATE_BUFFERING);
MediaPeriodHolder newPlayingPeriodHolder = null;
if (playingPeriodHolder == null) {
@@ -618,7 +709,7 @@ import java.io.IOException;
// Clear the timeline, but keep the requested period if it is already prepared.
MediaPeriodHolder periodHolder = playingPeriodHolder;
while (periodHolder != null) {
- if (periodHolder.index == periodIndex && periodHolder.prepared) {
+ if (shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) {
newPlayingPeriodHolder = periodHolder;
} else {
periodHolder.release();
@@ -662,6 +753,19 @@ import java.io.IOException;
return periodPositionUs;
}
+ private boolean shouldKeepPeriodHolder(MediaPeriodId seekPeriodId, long positionUs,
+ MediaPeriodHolder holder) {
+ if (seekPeriodId.equals(holder.info.id) && holder.prepared) {
+ timeline.getPeriod(holder.info.id.periodIndex, period);
+ int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
+ if (nextAdGroupIndex == C.INDEX_UNSET
+ || period.getAdGroupTimeUs(nextAdGroupIndex) == holder.info.endPositionUs) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
rendererPositionUs = playingPeriodHolder == null
? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US
@@ -683,13 +787,13 @@ import java.io.IOException;
private void stopInternal() {
resetInternal(true);
loadControl.onStopped();
- setState(ExoPlayer.STATE_IDLE);
+ setState(Player.STATE_IDLE);
}
private void releaseInternal() {
resetInternal(true);
loadControl.onReleased();
- setState(ExoPlayer.STATE_IDLE);
+ setState(Player.STATE_IDLE);
synchronized (this) {
released = true;
notifyAll();
@@ -724,6 +828,7 @@ import java.io.IOException;
mediaSource.releaseSource();
mediaSource = null;
}
+ mediaPeriodInfoSequence.setTimeline(null);
timeline = null;
}
}
@@ -733,7 +838,7 @@ import java.io.IOException;
for (ExoPlayerMessage message : messages) {
message.target.handleMessage(message.messageType, message.message);
}
- if (mediaSource != null) {
+ if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) {
// The message may have caused something to change that now requires us to do work.
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
@@ -834,7 +939,7 @@ import java.io.IOException;
}
loadingPeriodHolder.next = null;
if (loadingPeriodHolder.prepared) {
- long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs,
+ long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.info.startPositionUs,
loadingPeriodHolder.toPeriodTime(rendererPositionUs));
loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false);
}
@@ -847,23 +952,8 @@ import java.io.IOException;
private boolean isTimelineReady(long playingPeriodDurationUs) {
return playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs
- || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared);
- }
-
- private boolean haveSufficientBuffer(boolean rebuffering) {
- long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared
- ? loadingPeriodHolder.startPositionUs
- : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs();
- if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) {
- if (loadingPeriodHolder.isLast) {
- return true;
- }
- loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period)
- .getDurationUs();
- }
- return loadControl.shouldStartPlayback(
- loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs),
- rebuffering);
+ || (playingPeriodHolder.next != null
+ && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd()));
}
private void maybeThrowPeriodPrepareError() throws IOException {
@@ -882,6 +972,7 @@ import java.io.IOException;
throws ExoPlaybackException {
Timeline oldTimeline = timeline;
timeline = timelineAndManifest.first;
+ mediaPeriodInfoSequence.setTimeline(timeline);
Object manifest = timelineAndManifest.second;
int processedInitialSeekCount = 0;
@@ -895,32 +986,45 @@ import java.io.IOException;
// The seek position was valid for the timeline that it was performed into, but the
// timeline has changed and a suitable seek position could not be resolved in the new one.
handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
- return;
+ } else {
+ int periodIndex = periodPosition.first;
+ long positionUs = periodPosition.second;
+ MediaPeriodId periodId =
+ mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs);
+ playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs, positionUs);
+ notifySourceInfoRefresh(manifest, processedInitialSeekCount);
}
- playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second);
} else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
if (timeline.isEmpty()) {
handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
- return;
+ } else {
+ Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET);
+ int periodIndex = defaultPosition.first;
+ long startPositionUs = defaultPosition.second;
+ MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex,
+ startPositionUs);
+ playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : startPositionUs,
+ startPositionUs);
+ notifySourceInfoRefresh(manifest, processedInitialSeekCount);
}
- Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET);
- playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second);
}
- }
-
- MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder
- : loadingPeriodHolder;
- if (periodHolder == null) {
- // We don't have any period holders, so we're done.
- notifySourceInfoRefresh(manifest, processedInitialSeekCount);
return;
}
- int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid);
+ int playingPeriodIndex = playbackInfo.periodId.periodIndex;
+ MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder
+ : loadingPeriodHolder;
+ if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) {
+ notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+ return;
+ }
+ Object playingPeriodUid = periodHolder == null
+ ? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid;
+ int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid);
if (periodIndex == C.INDEX_UNSET) {
// We didn't find the current period in the new timeline. Attempt to resolve a subsequent
// period whose window we can restart from.
- int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline);
+ int newPeriodIndex = resolveSubsequentPeriod(playingPeriodIndex, oldTimeline, timeline);
if (newPeriodIndex == C.INDEX_UNSET) {
// We failed to resolve a suitable restart position.
handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
@@ -932,52 +1036,75 @@ import java.io.IOException;
newPeriodIndex = defaultPosition.first;
long newPositionUs = defaultPosition.second;
timeline.getPeriod(newPeriodIndex, period, true);
- // Clear the index of each holder that doesn't contain the default position. If a holder
- // contains the default position then update its index so it can be re-used when seeking.
- Object newPeriodUid = period.uid;
- periodHolder.index = C.INDEX_UNSET;
- while (periodHolder.next != null) {
- periodHolder = periodHolder.next;
- periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET;
+ if (periodHolder != null) {
+ // Clear the index of each holder that doesn't contain the default position. If a holder
+ // contains the default position then update its index so it can be re-used when seeking.
+ Object newPeriodUid = period.uid;
+ periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET);
+ while (periodHolder.next != null) {
+ periodHolder = periodHolder.next;
+ if (periodHolder.uid.equals(newPeriodUid)) {
+ periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info,
+ newPeriodIndex);
+ } else {
+ periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET);
+ }
+ }
}
// Actually do the seek.
- newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs);
- playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs);
+ MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex);
+ newPositionUs = seekToPeriodPosition(periodId, newPositionUs);
+ playbackInfo = new PlaybackInfo(periodId, newPositionUs);
notifySourceInfoRefresh(manifest, processedInitialSeekCount);
return;
}
- // The current period is in the new timeline. Update the holder and playbackInfo.
- timeline.getPeriod(periodIndex, period);
- boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
- && !timeline.getWindow(period.windowIndex, window).isDynamic;
- periodHolder.setIndex(periodIndex, isLastPeriod);
- boolean seenReadingPeriod = periodHolder == readingPeriodHolder;
- if (periodIndex != playbackInfo.periodIndex) {
+ // The current period is in the new timeline. Update the playback info.
+ if (periodIndex != playingPeriodIndex) {
playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex);
}
- // If there are subsequent holders, update the index for each of them. If we find a holder
- // that's inconsistent with the new timeline then take appropriate action.
+ if (playbackInfo.periodId.isAd()) {
+ // Check that the playing ad hasn't been marked as played. If it has, skip forward.
+ MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex,
+ playbackInfo.contentPositionUs);
+ if (!periodId.isAd() || periodId.adIndexInAdGroup != playbackInfo.periodId.adIndexInAdGroup) {
+ long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs);
+ long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET;
+ playbackInfo = new PlaybackInfo(periodId, newPositionUs, contentPositionUs);
+ notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+ return;
+ }
+ }
+
+ if (periodHolder == null) {
+ // We don't have any period holders, so we're done.
+ notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+ return;
+ }
+
+ // Update the holder indices. If we find a subsequent holder that's inconsistent with the new
+ // timeline then take appropriate action.
+ periodHolder = updatePeriodInfo(periodHolder, periodIndex);
while (periodHolder.next != null) {
MediaPeriodHolder previousPeriodHolder = periodHolder;
periodHolder = periodHolder.next;
- periodIndex++;
- timeline.getPeriod(periodIndex, period, true);
- isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
- && !timeline.getWindow(period.windowIndex, window).isDynamic;
- if (periodHolder.uid.equals(period.uid)) {
+ periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode);
+ if (periodIndex != C.INDEX_UNSET
+ && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) {
// The holder is consistent with the new timeline. Update its index and continue.
- periodHolder.setIndex(periodIndex, isLastPeriod);
- seenReadingPeriod |= (periodHolder == readingPeriodHolder);
+ periodHolder = updatePeriodInfo(periodHolder, periodIndex);
} else {
// The holder is inconsistent with the new timeline.
- if (!seenReadingPeriod) {
+ boolean seenReadingPeriodHolder =
+ readingPeriodHolder != null && readingPeriodHolder.index < periodHolder.index;
+ if (!seenReadingPeriodHolder) {
// Renderers may have read from a period that's been removed. Seek back to the current
// position of the playing period to make sure none of the removed period is played.
- periodIndex = playingPeriodHolder.index;
- long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs);
- playbackInfo = new PlaybackInfo(periodIndex, newPositionUs);
+ long newPositionUs =
+ seekToPeriodPosition(playingPeriodHolder.info.id, playbackInfo.positionUs);
+ playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id, newPositionUs,
+ playbackInfo.contentPositionUs);
} else {
// Update the loading period to be the last period that's still valid, and release all
// subsequent periods.
@@ -993,6 +1120,17 @@ import java.io.IOException;
notifySourceInfoRefresh(manifest, processedInitialSeekCount);
}
+ private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) {
+ while (true) {
+ periodHolder.info =
+ mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex);
+ if (periodHolder.info.isLastInTimelinePeriod || periodHolder.next == null) {
+ return periodHolder;
+ }
+ periodHolder = periodHolder.next;
+ }
+ }
+
private void handleSourceInfoRefreshEndedPlayback(Object manifest,
int processedInitialSeekCount) {
// Set the playback position to (0,0) for notifying the eventHandler.
@@ -1000,7 +1138,7 @@ import java.io.IOException;
notifySourceInfoRefresh(manifest, processedInitialSeekCount);
// Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored.
playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
- setState(ExoPlayer.STATE_ENDED);
+ setState(Player.STATE_ENDED);
// Reset, but retain the source so that it can still be used should a seek occur.
resetInternal(false);
}
@@ -1023,9 +1161,15 @@ import java.io.IOException;
private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline,
Timeline newTimeline) {
int newPeriodIndex = C.INDEX_UNSET;
- while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) {
+ int maxIterations = oldTimeline.getPeriodCount();
+ for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) {
+ oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode);
+ if (oldPeriodIndex == C.INDEX_UNSET) {
+ // We've reached the end of the old timeline.
+ break;
+ }
newPeriodIndex = newTimeline.getIndexOfPeriod(
- oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid);
+ oldTimeline.getPeriod(oldPeriodIndex, period, true).uid);
}
return newPeriodIndex;
}
@@ -1049,7 +1193,7 @@ import java.io.IOException;
// Map the SeekPosition to a position in the corresponding timeline.
Pair periodPosition;
try {
- periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex,
+ periodPosition = seekTimeline.getPeriodPosition(window, period, seekPosition.windowIndex,
seekPosition.windowPositionUs);
} catch (IndexOutOfBoundsException e) {
// The window index of the seek position was outside the bounds of the timeline.
@@ -1078,53 +1222,11 @@ import java.io.IOException;
}
/**
- * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline.
+ * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the
+ * current timeline.
*/
private Pair getPeriodPosition(int windowIndex, long windowPositionUs) {
- return getPeriodPosition(timeline, windowIndex, windowPositionUs);
- }
-
- /**
- * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position
- * projection.
- */
- private Pair getPeriodPosition(Timeline timeline, int windowIndex,
- long windowPositionUs) {
- return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0);
- }
-
- /**
- * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs).
- *
- * @param timeline The timeline containing the window.
- * @param windowIndex The window index.
- * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default
- * start position.
- * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the
- * duration into the future by which the window's position should be projected.
- * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs}
- * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's
- * position could not be projected by {@code defaultPositionProjectionUs}.
- */
- private Pair getPeriodPosition(Timeline timeline, int windowIndex,
- long windowPositionUs, long defaultPositionProjectionUs) {
- Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount());
- timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs);
- if (windowPositionUs == C.TIME_UNSET) {
- windowPositionUs = window.getDefaultPositionUs();
- if (windowPositionUs == C.TIME_UNSET) {
- return null;
- }
- }
- int periodIndex = window.firstPeriodIndex;
- long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
- long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
- while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
- && periodIndex < window.lastPeriodIndex) {
- periodPositionUs -= periodDurationUs;
- periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs();
- }
- return Pair.create(periodIndex, periodPositionUs);
+ return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
}
private void updatePeriods() throws ExoPlaybackException, IOException {
@@ -1138,7 +1240,7 @@ import java.io.IOException;
maybeUpdateLoadingPeriod();
if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
setIsLoading(false);
- } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) {
+ } else if (loadingPeriodHolder != null && !isLoading) {
maybeContinueLoading();
}
@@ -1154,13 +1256,13 @@ import java.io.IOException;
// the end of the playing period, so advance playback to the next period.
playingPeriodHolder.release();
setPlayingPeriodHolder(playingPeriodHolder.next);
- playbackInfo = new PlaybackInfo(playingPeriodHolder.index,
- playingPeriodHolder.startPositionUs);
+ playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id,
+ playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs);
updatePlaybackPositions();
eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
}
- if (readingPeriodHolder.isLast) {
+ if (readingPeriodHolder.info.isFinal) {
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
@@ -1224,76 +1326,41 @@ import java.io.IOException;
}
private void maybeUpdateLoadingPeriod() throws IOException {
- int newLoadingPeriodIndex;
+ MediaPeriodInfo info;
if (loadingPeriodHolder == null) {
- newLoadingPeriodIndex = playbackInfo.periodIndex;
+ info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo);
} else {
- int loadingPeriodIndex = loadingPeriodHolder.index;
- if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered()
- || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) {
- // Either the existing loading period is the last period, or we are not ready to advance to
- // loading the next period because it hasn't been fully buffered or its duration is unknown.
+ if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered()
+ || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) {
return;
}
- if (playingPeriodHolder != null
- && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) {
- // We are already buffering the maximum number of periods ahead.
- return;
+ if (playingPeriodHolder != null) {
+ int bufferAheadPeriodCount = loadingPeriodHolder.index - playingPeriodHolder.index;
+ if (bufferAheadPeriodCount == MAXIMUM_BUFFER_AHEAD_PERIODS) {
+ // We are already buffering the maximum number of periods ahead.
+ return;
+ }
}
- newLoadingPeriodIndex = loadingPeriodHolder.index + 1;
+ info = mediaPeriodInfoSequence.getNextMediaPeriodInfo(loadingPeriodHolder.info,
+ loadingPeriodHolder.getRendererOffset(), rendererPositionUs);
}
-
- if (newLoadingPeriodIndex >= timeline.getPeriodCount()) {
- // The next period is not available yet.
+ if (info == null) {
mediaSource.maybeThrowSourceInfoRefreshError();
return;
}
- long newLoadingPeriodStartPositionUs;
- if (loadingPeriodHolder == null) {
- newLoadingPeriodStartPositionUs = playbackInfo.positionUs;
- } else {
- int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex;
- if (newLoadingPeriodIndex
- != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) {
- // We're starting to buffer a new period in the current window. Always start from the
- // beginning of the period.
- newLoadingPeriodStartPositionUs = 0;
- } else {
- // We're starting to buffer a new window. When playback transitions to this window we'll
- // want it to be from its default start position. The expected delay until playback
- // transitions is equal the duration of media that's currently buffered (assuming no
- // interruptions). Hence we project the default start position forward by the duration of
- // the buffer, and start buffering from this point.
- long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset()
- + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()
- - rendererPositionUs;
- Pair defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex,
- C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
- if (defaultPosition == null) {
- return;
- }
-
- newLoadingPeriodIndex = defaultPosition.first;
- newLoadingPeriodStartPositionUs = defaultPosition.second;
- }
- }
-
long rendererPositionOffsetUs = loadingPeriodHolder == null
- ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US
- : (loadingPeriodHolder.getRendererOffset()
- + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs());
- timeline.getPeriod(newLoadingPeriodIndex, period, true);
- boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1
- && !timeline.getWindow(period.windowIndex, window).isDynamic;
+ ? RENDERER_TIMESTAMP_OFFSET_US
+ : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs);
+ int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1;
+ Object uid = timeline.getPeriod(info.id.periodIndex, period, true).uid;
MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities,
- rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid,
- newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs);
+ rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, uid, holderIndex, info);
if (loadingPeriodHolder != null) {
loadingPeriodHolder.next = newPeriodHolder;
}
loadingPeriodHolder = newPeriodHolder;
- loadingPeriodHolder.mediaPeriod.prepare(this);
+ loadingPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
setIsLoading(true);
}
@@ -1306,7 +1373,7 @@ import java.io.IOException;
if (playingPeriodHolder == null) {
// This is the first prepared period, so start playing it.
readingPeriodHolder = loadingPeriodHolder;
- resetRendererPosition(readingPeriodHolder.startPositionUs);
+ resetRendererPosition(readingPeriodHolder.info.startPositionUs);
setPlayingPeriodHolder(readingPeriodHolder);
}
maybeContinueLoading();
@@ -1321,21 +1388,10 @@ import java.io.IOException;
}
private void maybeContinueLoading() {
- long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0
- : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs();
- if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
- setIsLoading(false);
- } else {
- long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs);
- long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
- boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs);
- setIsLoading(continueLoading);
- if (continueLoading) {
- loadingPeriodHolder.needsContinueLoading = false;
- loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs);
- } else {
- loadingPeriodHolder.needsContinueLoading = true;
- }
+ boolean continueLoading = loadingPeriodHolder.shouldContinueLoading(rendererPositionUs);
+ setIsLoading(continueLoading);
+ if (continueLoading) {
+ loadingPeriodHolder.continueLoading(rendererPositionUs);
}
}
@@ -1395,7 +1451,7 @@ import java.io.IOException;
RendererConfiguration rendererConfiguration =
playingPeriodHolder.trackSelectorResult.rendererConfigurations[i];
// The renderer needs enabling with its new track selection.
- boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
+ boolean playing = playWhenReady && state == Player.STATE_READY;
// Consider as joining only if the renderer was previously disabled.
boolean joining = !rendererWasEnabledFlags[i] && playing;
// Build an array of formats contained by the selection.
@@ -1432,17 +1488,15 @@ import java.io.IOException;
public final MediaPeriod mediaPeriod;
public final Object uid;
+ public final int index;
public final SampleStream[] sampleStreams;
public final boolean[] mayRetainStreamFlags;
public final long rendererPositionOffsetUs;
- public int index;
- public long startPositionUs;
- public boolean isLast;
+ public MediaPeriodInfo info;
public boolean prepared;
public boolean hasEnabledTracks;
public MediaPeriodHolder next;
- public boolean needsContinueLoading;
public TrackSelectorResult trackSelectorResult;
private final Renderer[] renderers;
@@ -1455,8 +1509,7 @@ import java.io.IOException;
public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
- MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod,
- long startPositionUs) {
+ MediaSource mediaSource, Object periodUid, int index, MediaPeriodInfo info) {
this.renderers = renderers;
this.rendererCapabilities = rendererCapabilities;
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
@@ -1464,13 +1517,17 @@ import java.io.IOException;
this.loadControl = loadControl;
this.mediaSource = mediaSource;
this.uid = Assertions.checkNotNull(periodUid);
- this.index = periodIndex;
- this.isLast = isLastPeriod;
- this.startPositionUs = startPositionUs;
+ this.index = index;
+ this.info = info;
sampleStreams = new SampleStream[renderers.length];
mayRetainStreamFlags = new boolean[renderers.length];
- mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(),
- startPositionUs);
+ MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, loadControl.getAllocator());
+ if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
+ ClippingMediaPeriod clippingMediaPeriod = new ClippingMediaPeriod(mediaPeriod, true);
+ clippingMediaPeriod.setClipping(0, info.endPositionUs);
+ mediaPeriod = clippingMediaPeriod;
+ }
+ this.mediaPeriod = mediaPeriod;
}
public long toRendererTime(long periodTimeUs) {
@@ -1482,12 +1539,8 @@ import java.io.IOException;
}
public long getRendererOffset() {
- return rendererPositionOffsetUs - startPositionUs;
- }
-
- public void setIndex(int index, boolean isLast) {
- this.index = index;
- this.isLast = isLast;
+ return index == 0 ? rendererPositionOffsetUs
+ : (rendererPositionOffsetUs - info.startPositionUs);
}
public boolean isFullyBuffered() {
@@ -1495,10 +1548,40 @@ import java.io.IOException;
&& (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
}
+ public boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs) {
+ long bufferedPositionUs = !prepared ? info.startPositionUs
+ : mediaPeriod.getBufferedPositionUs();
+ if (bufferedPositionUs == C.TIME_END_OF_SOURCE) {
+ if (info.isFinal) {
+ return true;
+ }
+ bufferedPositionUs = info.durationUs;
+ }
+ return loadControl.shouldStartPlayback(bufferedPositionUs - toPeriodTime(rendererPositionUs),
+ rebuffering);
+ }
+
public void handlePrepared() throws ExoPlaybackException {
prepared = true;
selectTracks();
- startPositionUs = updatePeriodTrackSelection(startPositionUs, false);
+ long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false);
+ info = info.copyWithStartPositionUs(newStartPositionUs);
+ }
+
+ public boolean shouldContinueLoading(long rendererPositionUs) {
+ long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+ return false;
+ } else {
+ long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
+ long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
+ return loadControl.shouldContinueLoading(bufferedDurationUs);
+ }
+ }
+
+ public void continueLoading(long rendererPositionUs) {
+ long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
+ mediaPeriod.continueLoading(loadingPeriodPositionUs);
}
public boolean selectTracks() throws ExoPlaybackException {
@@ -1547,7 +1630,11 @@ import java.io.IOException;
public void release() {
try {
- mediaSource.releasePeriod(mediaPeriod);
+ if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
+ mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
+ } else {
+ mediaSource.releasePeriod(mediaPeriod);
+ }
} catch (RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Period release failed.", e);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index 73d91f293e..fd5ead5c85 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -15,22 +15,29 @@
*/
package com.google.android.exoplayer2;
+import java.util.HashSet;
+
/**
* Information about the ExoPlayer library.
*/
-public interface ExoPlayerLibraryInfo {
+public final class ExoPlayerLibraryInfo {
+
+ /**
+ * A tag to use when logging library information.
+ */
+ public static final String TAG = "ExoPlayer";
/**
* 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.
- String VERSION = "2.4.4";
+ public static final String VERSION = "2.5.0";
/**
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- String VERSION_SLASHY = "ExoPlayerLib/2.4.4";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -40,18 +47,41 @@ public interface ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- int VERSION_INT = 2004004;
+ public static final int VERSION_INT = 2005000;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
* checks enabled.
*/
- boolean ASSERTIONS_ENABLED = true;
+ public static final boolean ASSERTIONS_ENABLED = true;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil}
* trace enabled.
*/
- boolean TRACE_ENABLED = true;
+ public static final boolean TRACE_ENABLED = true;
+
+ private static final HashSet registeredModules = new HashSet<>();
+ private static String registeredModulesString = "goog.exo.core";
+
+ private ExoPlayerLibraryInfo() {} // Prevents instantiation.
+
+ /**
+ * Returns a string consisting of registered module names separated by ", ".
+ */
+ public static synchronized String registeredModules() {
+ return registeredModulesString;
+ }
+
+ /**
+ * Registers a module to be returned in the {@link #registeredModules()} string.
+ *
+ * @param name The name of the module being registered.
+ */
+ public static synchronized void registerModule(String name) {
+ if (registeredModules.add(name)) {
+ registeredModulesString = registeredModulesString + ", " + name;
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
index 0bffd28ba5..4e387ac7ce 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
@@ -286,9 +286,14 @@ public final class Format implements Parcelable {
OFFSET_SAMPLE_RELATIVE, null, null, null);
}
- public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) {
- return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+ public static Format createTextSampleFormat(String id, String sampleMimeType,
+ @C.SelectionFlags int selectionFlags, String language) {
+ return createTextSampleFormat(id, sampleMimeType, selectionFlags, language, null);
+ }
+
+ public static Format createTextSampleFormat(String id, String sampleMimeType,
+ @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) {
+ return createTextSampleFormat(id, sampleMimeType, null, NO_VALUE, selectionFlags, language,
NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList());
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java
new file mode 100644
index 0000000000..0e9c65421c
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
+import com.google.android.exoplayer2.Player.RepeatMode;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+
+/**
+ * Provides a sequence of {@link MediaPeriodInfo}s to the player, determining the order and
+ * start/end positions for {@link MediaPeriod}s to load and play.
+ */
+/* package */ final class MediaPeriodInfoSequence {
+
+ // TODO: Consider merging this class with the MediaPeriodHolder queue in ExoPlayerImplInternal.
+
+ /**
+ * Stores the information required to load and play a {@link MediaPeriod}.
+ */
+ public static final class MediaPeriodInfo {
+
+ /**
+ * The media period's identifier.
+ */
+ public final MediaPeriodId id;
+ /**
+ * The start position of the media to play within the media period, in microseconds.
+ */
+ public final long startPositionUs;
+ /**
+ * The end position of the media to play within the media period, in microseconds, or
+ * {@link C#TIME_END_OF_SOURCE} if the end position is the end of the media period.
+ */
+ public final long endPositionUs;
+ /**
+ * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET}
+ * otherwise.
+ */
+ public final long contentPositionUs;
+ /**
+ * The duration of the media to play within the media period, in microseconds, or
+ * {@link C#TIME_UNSET} if not known.
+ */
+ public final long durationUs;
+ /**
+ * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media
+ * period corresponding to a timeline period without ads).
+ */
+ public final boolean isLastInTimelinePeriod;
+ /**
+ * Whether this is the last media period in the entire timeline. If true,
+ * {@link #isLastInTimelinePeriod} will also be true.
+ */
+ public final boolean isFinal;
+
+ private MediaPeriodInfo(MediaPeriodId id, long startPositionUs, long endPositionUs,
+ long contentPositionUs, long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) {
+ this.id = id;
+ this.startPositionUs = startPositionUs;
+ this.endPositionUs = endPositionUs;
+ this.contentPositionUs = contentPositionUs;
+ this.durationUs = durationUs;
+ this.isLastInTimelinePeriod = isLastInTimelinePeriod;
+ this.isFinal = isFinal;
+ }
+
+ /**
+ * Returns a copy of this instance with the period identifier's period index set to the
+ * specified value.
+ */
+ public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) {
+ return new MediaPeriodInfo(id.copyWithPeriodIndex(periodIndex), startPositionUs,
+ endPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal);
+ }
+
+ /**
+ * Returns a copy of this instance with the start position set to the specified value.
+ */
+ public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) {
+ return new MediaPeriodInfo(id, startPositionUs, endPositionUs, contentPositionUs, durationUs,
+ isLastInTimelinePeriod, isFinal);
+ }
+
+ }
+
+ private final Timeline.Period period;
+ private final Timeline.Window window;
+
+ private Timeline timeline;
+ @RepeatMode
+ private int repeatMode;
+
+ /**
+ * Creates a new media period info sequence.
+ */
+ public MediaPeriodInfoSequence() {
+ period = new Timeline.Period();
+ window = new Timeline.Window();
+ }
+
+ /**
+ * Sets the {@link Timeline}. Call {@link #getUpdatedMediaPeriodInfo} to update period information
+ * taking into account the new timeline.
+ */
+ public void setTimeline(Timeline timeline) {
+ this.timeline = timeline;
+ }
+
+ /**
+ * Sets the {@link RepeatMode}. Call {@link #getUpdatedMediaPeriodInfo} to update period
+ * information taking into account the new repeat mode.
+ */
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ this.repeatMode = repeatMode;
+ }
+
+ /**
+ * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position.
+ */
+ public MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) {
+ return getMediaPeriodInfo(playbackInfo.periodId, playbackInfo.contentPositionUs,
+ playbackInfo.startPositionUs);
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} following {@code currentMediaPeriodInfo}.
+ *
+ * @param currentMediaPeriodInfo The current media period info.
+ * @param rendererOffsetUs The current renderer offset in microseconds.
+ * @param rendererPositionUs The current renderer position in microseconds.
+ * @return The following media period info, or {@code null} if it is not yet possible to get the
+ * next media period info.
+ */
+ public MediaPeriodInfo getNextMediaPeriodInfo(MediaPeriodInfo currentMediaPeriodInfo,
+ long rendererOffsetUs, long rendererPositionUs) {
+ // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod
+ // but if the timeline is not ready to provide the next period it can't return a non-null value
+ // until the timeline is updated. Store whether the next timeline period is ready when the
+ // timeline is updated, to avoid repeatedly checking the same timeline.
+ if (currentMediaPeriodInfo.isLastInTimelinePeriod) {
+ int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex,
+ period, window, repeatMode);
+ if (nextPeriodIndex == C.INDEX_UNSET) {
+ // We can't create a next period yet.
+ return null;
+ }
+
+ long startPositionUs;
+ int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period).windowIndex;
+ if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {
+ // We're starting to buffer a new window. When playback transitions to this window we'll
+ // want it to be from its default start position. The expected delay until playback
+ // transitions is equal the duration of media that's currently buffered (assuming no
+ // interruptions). Hence we project the default start position forward by the duration of
+ // the buffer, and start buffering from this point.
+ long defaultPositionProjectionUs =
+ rendererOffsetUs + currentMediaPeriodInfo.durationUs - rendererPositionUs;
+ Pair defaultPosition = timeline.getPeriodPosition(window, period,
+ nextWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
+ if (defaultPosition == null) {
+ return null;
+ }
+ nextPeriodIndex = defaultPosition.first;
+ startPositionUs = defaultPosition.second;
+ } else {
+ startPositionUs = 0;
+ }
+ MediaPeriodId periodId = resolvePeriodPositionForAds(nextPeriodIndex, startPositionUs);
+ return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs);
+ }
+
+ MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id;
+ if (currentPeriodId.isAd()) {
+ int currentAdGroupIndex = currentPeriodId.adGroupIndex;
+ timeline.getPeriod(currentPeriodId.periodIndex, period);
+ int adCountInCurrentAdGroup = period.getAdCountInAdGroup(currentAdGroupIndex);
+ if (adCountInCurrentAdGroup == C.LENGTH_UNSET) {
+ return null;
+ }
+ int nextAdIndexInAdGroup = currentPeriodId.adIndexInAdGroup + 1;
+ if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) {
+ // Play the next ad in the ad group if it's available.
+ return !period.isAdAvailable(currentAdGroupIndex, nextAdIndexInAdGroup) ? null
+ : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, currentAdGroupIndex,
+ nextAdIndexInAdGroup, currentMediaPeriodInfo.contentPositionUs);
+ } else {
+ // Play content from the ad group position.
+ int nextAdGroupIndex =
+ period.getAdGroupIndexAfterPositionUs(currentMediaPeriodInfo.contentPositionUs);
+ long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE
+ : period.getAdGroupTimeUs(nextAdGroupIndex);
+ return getMediaPeriodInfoForContent(currentPeriodId.periodIndex,
+ currentMediaPeriodInfo.contentPositionUs, endUs);
+ }
+ } else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) {
+ // Play the next ad group if it's available.
+ int nextAdGroupIndex =
+ period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs);
+ return !period.isAdAvailable(nextAdGroupIndex, 0) ? null
+ : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, nextAdGroupIndex, 0,
+ currentMediaPeriodInfo.endPositionUs);
+ } else {
+ // Check if the postroll ad should be played.
+ int adGroupCount = period.getAdGroupCount();
+ if (adGroupCount == 0
+ || period.getAdGroupTimeUs(adGroupCount - 1) != C.TIME_END_OF_SOURCE
+ || period.hasPlayedAdGroup(adGroupCount - 1)
+ || !period.isAdAvailable(adGroupCount - 1, 0)) {
+ return null;
+ }
+ long contentDurationUs = period.getDurationUs();
+ return getMediaPeriodInfoForAd(currentPeriodId.periodIndex, adGroupCount - 1, 0,
+ contentDurationUs);
+ }
+ }
+
+ /**
+ * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
+ * played, returning an identifier for an ad group if one needs to be played before the specified
+ * position, or an identifier for a content media period if not.
+ */
+ public MediaPeriodId resolvePeriodPositionForAds(int periodIndex, long positionUs) {
+ timeline.getPeriod(periodIndex, period);
+ int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
+ if (adGroupIndex == C.INDEX_UNSET) {
+ return new MediaPeriodId(periodIndex);
+ } else {
+ int adIndexInAdGroup = period.getPlayedAdCount(adGroupIndex);
+ return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup);
+ }
+ }
+
+ /**
+ * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline.
+ */
+ public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo) {
+ return getUpdatedMediaPeriodInfo(mediaPeriodInfo, mediaPeriodInfo.id);
+ }
+
+ /**
+ * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline,
+ * resetting the identifier of the media period to the specified {@code newPeriodIndex}.
+ */
+ public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo,
+ int newPeriodIndex) {
+ return getUpdatedMediaPeriodInfo(mediaPeriodInfo,
+ mediaPeriodInfo.id.copyWithPeriodIndex(newPeriodIndex));
+ }
+
+ // Internal methods.
+
+ private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) {
+ long startPositionUs = info.startPositionUs;
+ long endPositionUs = info.endPositionUs;
+ boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs);
+ boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod);
+ timeline.getPeriod(newId.periodIndex, period);
+ long durationUs = newId.isAd()
+ ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup)
+ : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs);
+ return new MediaPeriodInfo(newId, startPositionUs, endPositionUs, info.contentPositionUs,
+ durationUs, isLastInPeriod, isLastInTimeline);
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId id, long contentPositionUs,
+ long startPositionUs) {
+ timeline.getPeriod(id.periodIndex, period);
+ if (id.isAd()) {
+ if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) {
+ return null;
+ }
+ return getMediaPeriodInfoForAd(id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup,
+ contentPositionUs);
+ } else {
+ int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
+ long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE
+ : period.getAdGroupTimeUs(nextAdGroupIndex);
+ return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, endUs);
+ }
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfoForAd(int periodIndex, int adGroupIndex,
+ int adIndexInAdGroup, long contentPositionUs) {
+ MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup);
+ boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE);
+ boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
+ long durationUs = timeline.getPeriod(id.periodIndex, period)
+ .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup);
+ long startPositionUs = adIndexInAdGroup == period.getPlayedAdCount(adGroupIndex)
+ ? period.getAdResumePositionUs() : 0;
+ return new MediaPeriodInfo(id, startPositionUs, C.TIME_END_OF_SOURCE, contentPositionUs,
+ durationUs, isLastInPeriod, isLastInTimeline);
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs,
+ long endUs) {
+ MediaPeriodId id = new MediaPeriodId(periodIndex);
+ boolean isLastInPeriod = isLastInPeriod(id, endUs);
+ boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
+ timeline.getPeriod(id.periodIndex, period);
+ long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs;
+ return new MediaPeriodInfo(id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod,
+ isLastInTimeline);
+ }
+
+ private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) {
+ int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount();
+ if (adGroupCount == 0) {
+ return true;
+ }
+
+ int lastAdGroupIndex = adGroupCount - 1;
+ boolean isAd = id.isAd();
+ if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) {
+ // There's no postroll ad.
+ return !isAd && endPositionUs == C.TIME_END_OF_SOURCE;
+ }
+
+ int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex);
+ if (postrollAdCount == C.LENGTH_UNSET) {
+ // We won't know if this is the last ad until we know how many postroll ads there are.
+ return false;
+ }
+
+ boolean isLastAd = isAd && id.adGroupIndex == lastAdGroupIndex
+ && id.adIndexInAdGroup == postrollAdCount - 1;
+ return isLastAd || (!isAd && period.getPlayedAdCount(lastAdGroupIndex) == postrollAdCount);
+ }
+
+ private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
+ int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex;
+ return !timeline.getWindow(windowIndex, window).isDynamic
+ && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode)
+ && isLastMediaPeriodInPeriod;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
new file mode 100644
index 0000000000..d2480c5b3a
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.os.Looper;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A media player interface defining traditional high-level functionality, such as the ability to
+ * play, pause, seek and query properties of the currently playing media.
+ *
+ * Some important properties of media players that implement this interface are:
+ *
+ *
They can provide a {@link Timeline} representing the structure of the media being played,
+ * which can be obtained by calling {@link #getCurrentTimeline()}.
+ *
They can provide a {@link TrackGroupArray} defining the currently available tracks,
+ * which can be obtained by calling {@link #getCurrentTrackGroups()}.
+ *
They contain a number of renderers, each of which is able to render tracks of a single
+ * type (e.g. audio, video or text). The number of renderers and their respective track types
+ * can be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}.
+ *
+ *
They can provide a {@link TrackSelectionArray} defining which of the currently available
+ * tracks are selected to be rendered by each renderer. This can be obtained by calling
+ * {@link #getCurrentTrackSelections()}}.
+ *
+ */
+public interface Player {
+
+ /**
+ * Listener of changes in player state.
+ */
+ interface EventListener {
+
+ /**
+ * Called when the timeline and/or manifest has been refreshed.
+ *
+ * Note that if the timeline has changed then a position discontinuity may also have occurred.
+ * For example, the current period index may have changed as a result of periods being added or
+ * removed from the timeline. This will not be reported via a separate call to
+ * {@link #onPositionDiscontinuity()}.
+ *
+ * @param timeline The latest timeline. Never null, but may be empty.
+ * @param manifest The latest manifest. May be null.
+ */
+ void onTimelineChanged(Timeline timeline, Object manifest);
+
+ /**
+ * Called when the available or selected tracks change.
+ *
+ * @param trackGroups The available tracks. Never null, but may be of length zero.
+ * @param trackSelections The track selections for each renderer. Never null and always of
+ * length {@link #getRendererCount()}, but may contain null elements.
+ */
+ void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
+
+ /**
+ * Called when the player starts or stops loading the source.
+ *
+ * @param isLoading Whether the source is currently being loaded.
+ */
+ void onLoadingChanged(boolean isLoading);
+
+ /**
+ * Called when the value returned from either {@link #getPlayWhenReady()} or
+ * {@link #getPlaybackState()} changes.
+ *
+ * @param playWhenReady Whether playback will proceed when ready.
+ * @param playbackState One of the {@code STATE} constants.
+ */
+ void onPlayerStateChanged(boolean playWhenReady, int playbackState);
+
+ /**
+ * Called when the value of {@link #getRepeatMode()} changes.
+ *
+ * @param repeatMode The {@link RepeatMode} used for playback.
+ */
+ void onRepeatModeChanged(@RepeatMode int repeatMode);
+
+ /**
+ * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
+ * immediately after this method is called. The player instance can still be used, and
+ * {@link #release()} must still be called on the player should it no longer be required.
+ *
+ * @param error The error.
+ */
+ void onPlayerError(ExoPlaybackException error);
+
+ /**
+ * Called when a position discontinuity occurs without a change to the timeline. A position
+ * discontinuity occurs when the current window or period index changes (as a result of playback
+ * transitioning from one period in the timeline to the next), or when the playback position
+ * jumps within the period currently being played (as a result of a seek being performed, or
+ * when the source introduces a discontinuity internally).
+ *
+ * When a position discontinuity occurs as a result of a change to the timeline this method is
+ * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.
+ */
+ void onPositionDiscontinuity();
+
+ /**
+ * Called when the current playback parameters change. The playback parameters may change due to
+ * a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change
+ * them (for example, if audio playback switches to passthrough mode, where speed adjustment is
+ * no longer possible).
+ *
+ * @param playbackParameters The playback parameters.
+ */
+ void onPlaybackParametersChanged(PlaybackParameters playbackParameters);
+
+ }
+
+ /**
+ * The player does not have any media to play.
+ */
+ int STATE_IDLE = 1;
+ /**
+ * The player is not able to immediately play from its current position. This state typically
+ * occurs when more data needs to be loaded.
+ */
+ int STATE_BUFFERING = 2;
+ /**
+ * The player is able to immediately play from its current position. The player will be playing if
+ * {@link #getPlayWhenReady()} is true, and paused otherwise.
+ */
+ int STATE_READY = 3;
+ /**
+ * The player has finished playing the media.
+ */
+ int STATE_ENDED = 4;
+
+ /**
+ * Repeat modes for playback.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
+ public @interface RepeatMode {}
+ /**
+ * Normal playback without repetition.
+ */
+ int REPEAT_MODE_OFF = 0;
+ /**
+ * "Repeat One" mode to repeat the currently playing window infinitely.
+ */
+ int REPEAT_MODE_ONE = 1;
+ /**
+ * "Repeat All" mode to repeat the entire timeline infinitely.
+ */
+ int REPEAT_MODE_ALL = 2;
+
+ /**
+ * Register a listener to receive events from the player. The listener's methods will be called on
+ * the thread that was used to construct the player. However, if the thread used to construct the
+ * player does not have a {@link Looper}, then the listener will be called on the main thread.
+ *
+ * @param listener The listener to register.
+ */
+ void addListener(EventListener listener);
+
+ /**
+ * Unregister a listener. The listener will no longer receive events from the player.
+ *
+ * @param listener The listener to unregister.
+ */
+ void removeListener(EventListener listener);
+
+ /**
+ * Returns the current state of the player.
+ *
+ * @return One of the {@code STATE} constants defined in this interface.
+ */
+ int getPlaybackState();
+
+ /**
+ * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+ *
+ * If the player is already in the ready state then this method can be used to pause and resume
+ * playback.
+ *
+ * @param playWhenReady Whether playback should proceed when ready.
+ */
+ void setPlayWhenReady(boolean playWhenReady);
+
+ /**
+ * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+ *
+ * @return Whether playback will proceed when ready.
+ */
+ boolean getPlayWhenReady();
+
+ /**
+ * Sets the {@link RepeatMode} to be used for playback.
+ *
+ * @param repeatMode A repeat mode.
+ */
+ void setRepeatMode(@RepeatMode int repeatMode);
+
+ /**
+ * Returns the current {@link RepeatMode} used for playback.
+ *
+ * @return The current repeat mode.
+ */
+ @RepeatMode int getRepeatMode();
+
+ /**
+ * Whether the player is currently loading the source.
+ *
+ * @return Whether the player is currently loading the source.
+ */
+ boolean isLoading();
+
+ /**
+ * Seeks to the default position associated with the current window. The position can depend on
+ * the type of media being played. For live streams it will typically be the live edge of the
+ * window. For other streams it will typically be the start of the window.
+ */
+ void seekToDefaultPosition();
+
+ /**
+ * Seeks to the default position associated with the specified window. The position can depend on
+ * the type of media being played. For live streams it will typically be the live edge of the
+ * window. For other streams it will typically be the start of the window.
+ *
+ * @param windowIndex The index of the window whose associated default position should be seeked
+ * to.
+ */
+ void seekToDefaultPosition(int windowIndex);
+
+ /**
+ * Seeks to a position specified in milliseconds in the current window.
+ *
+ * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to
+ * the window's default position.
+ */
+ void seekTo(long positionMs);
+
+ /**
+ * Seeks to a position specified in milliseconds in the specified window.
+ *
+ * @param windowIndex The index of the window.
+ * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to
+ * the window's default position.
+ */
+ void seekTo(int windowIndex, long positionMs);
+
+ /**
+ * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the
+ * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.
+ *
+ * Playback parameters changes may cause the player to buffer.
+ * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever
+ * the currently active playback parameters change. When that listener is called, the parameters
+ * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch
+ * may be out of range, in which case they are constrained to a set of permitted values. If it is
+ * not possible to change the playback parameters, the listener will not be invoked.
+ *
+ * @param playbackParameters The playback parameters, or {@code null} to use the defaults.
+ */
+ void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
+
+ /**
+ * Returns the currently active playback parameters.
+ *
+ * @see EventListener#onPlaybackParametersChanged(PlaybackParameters)
+ */
+ PlaybackParameters getPlaybackParameters();
+
+ /**
+ * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
+ * is to pause playback.
+ *
+ * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The
+ * player instance can still be used, and {@link #release()} must still be called on the player if
+ * it's no longer required.
+ *
+ * Calling this method does not reset the playback position.
+ */
+ void stop();
+
+ /**
+ * Releases the player. This method must be called when the player is no longer required. The
+ * player must not be used after calling this method.
+ */
+ void release();
+
+ /**
+ * Returns the number of renderers.
+ */
+ int getRendererCount();
+
+ /**
+ * Returns the track type that the renderer at a given index handles.
+ *
+ * @see Renderer#getTrackType()
+ * @param index The index of the renderer.
+ * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+ */
+ int getRendererType(int index);
+
+ /**
+ * Returns the available track groups.
+ */
+ TrackGroupArray getCurrentTrackGroups();
+
+ /**
+ * Returns the current track selections for each renderer.
+ */
+ TrackSelectionArray getCurrentTrackSelections();
+
+ /**
+ * Returns the current manifest. The type depends on the type of media being played. May be null.
+ */
+ @Nullable Object getCurrentManifest();
+
+ /**
+ * Returns the current {@link Timeline}. Never null, but may be empty.
+ */
+ Timeline getCurrentTimeline();
+
+ /**
+ * Returns the index of the period currently being played.
+ */
+ int getCurrentPeriodIndex();
+
+ /**
+ * Returns the index of the window currently being played.
+ */
+ int getCurrentWindowIndex();
+
+ /**
+ * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the
+ * duration is not known.
+ */
+ long getDuration();
+
+ /**
+ * Returns the playback position in the current window, in milliseconds.
+ */
+ long getCurrentPosition();
+
+ /**
+ * Returns an estimate of the position in the current window up to which data is buffered, in
+ * milliseconds.
+ */
+ long getBufferedPosition();
+
+ /**
+ * Returns an estimate of the percentage in the current window up to which data is buffered, or 0
+ * if no estimate is available.
+ */
+ int getBufferedPercentage();
+
+ /**
+ * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
+ * empty.
+ *
+ * @see Timeline.Window#isDynamic
+ */
+ boolean isCurrentWindowDynamic();
+
+ /**
+ * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is
+ * empty.
+ *
+ * @see Timeline.Window#isSeekable
+ */
+ boolean isCurrentWindowSeekable();
+
+ /**
+ * Returns whether the player is currently playing an ad.
+ */
+ boolean isPlayingAd();
+
+ /**
+ * If {@link #isPlayingAd()} returns true, returns the index of the ad group in the period
+ * currently being played. Returns {@link C#INDEX_UNSET} otherwise.
+ */
+ int getCurrentAdGroupIndex();
+
+ /**
+ * If {@link #isPlayingAd()} returns true, returns the index of the ad in its ad group. Returns
+ * {@link C#INDEX_UNSET} otherwise.
+ */
+ int getCurrentAdIndexInAdGroup();
+
+ /**
+ * If {@link #isPlayingAd()} returns {@code true}, returns the content position that will be
+ * played once all ads in the ad group have finished playing, in milliseconds. If there is no ad
+ * playing, the returned position is the same as that returned by {@link #getCurrentPosition()}.
+ */
+ long getContentPosition();
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java
index 151453c12c..f841a1b8b5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java
@@ -27,11 +27,11 @@ public interface RendererCapabilities {
* {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES},
* {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
*/
- int FORMAT_SUPPORT_MASK = 0b11;
+ int FORMAT_SUPPORT_MASK = 0b111;
/**
* The {@link Renderer} is capable of rendering the format.
*/
- int FORMAT_HANDLED = 0b11;
+ int FORMAT_HANDLED = 0b100;
/**
* The {@link Renderer} is capable of rendering formats with the same mime type, but the
* properties of the format exceed the renderer's capability.
@@ -40,7 +40,16 @@ public interface RendererCapabilities {
* {@link MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported
* by the underlying H264 decoder.
*/
- int FORMAT_EXCEEDS_CAPABILITIES = 0b10;
+ int FORMAT_EXCEEDS_CAPABILITIES = 0b011;
+ /**
+ * The {@link Renderer} is capable of rendering formats with the same mime type, but the
+ * drm scheme used is not supported.
+ *
+ * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is
+ * {@link MimeTypes#VIDEO_H264}, but the format indicates cbcs encryption, which is not supported
+ * by the underlying content decryption module.
+ */
+ int FORMAT_UNSUPPORTED_DRM = 0b010;
/**
* The {@link Renderer} is a general purpose renderer for formats of the same top-level type,
* but is not capable of rendering the format or any other format with the same mime type because
@@ -49,7 +58,7 @@ public interface RendererCapabilities {
* Example: The {@link Renderer} is a general purpose audio renderer and the format's
* mime type matches audio/[subtype], but there does not exist a suitable decoder for [subtype].
*/
- int FORMAT_UNSUPPORTED_SUBTYPE = 0b01;
+ int FORMAT_UNSUPPORTED_SUBTYPE = 0b001;
/**
* The {@link Renderer} is not capable of rendering the format, either because it does not
* support the format's top-level type, or because it's a specialized renderer for a different
@@ -58,40 +67,40 @@ public interface RendererCapabilities {
* Example: The {@link Renderer} is a general purpose video renderer, but the format has an
* audio mime type.
*/
- int FORMAT_UNSUPPORTED_TYPE = 0b00;
+ int FORMAT_UNSUPPORTED_TYPE = 0b000;
/**
* A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
* {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}.
*/
- int ADAPTIVE_SUPPORT_MASK = 0b1100;
+ int ADAPTIVE_SUPPORT_MASK = 0b11000;
/**
* The {@link Renderer} can seamlessly adapt between formats.
*/
- int ADAPTIVE_SEAMLESS = 0b1000;
+ int ADAPTIVE_SEAMLESS = 0b10000;
/**
* The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity
* (~50-100ms) when adaptation occurs.
*/
- int ADAPTIVE_NOT_SEAMLESS = 0b0100;
+ int ADAPTIVE_NOT_SEAMLESS = 0b01000;
/**
* The {@link Renderer} does not support adaptation between formats.
*/
- int ADAPTIVE_NOT_SUPPORTED = 0b0000;
+ int ADAPTIVE_NOT_SUPPORTED = 0b00000;
/**
* A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
* {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.
*/
- int TUNNELING_SUPPORT_MASK = 0b10000;
+ int TUNNELING_SUPPORT_MASK = 0b100000;
/**
* The {@link Renderer} supports tunneled output.
*/
- int TUNNELING_SUPPORTED = 0b10000;
+ int TUNNELING_SUPPORTED = 0b100000;
/**
* The {@link Renderer} does not support tunneled output.
*/
- int TUNNELING_NOT_SUPPORTED = 0b00000;
+ int TUNNELING_NOT_SUPPORTED = 0b000000;
/**
* Returns the track type that the {@link Renderer} handles. For example, a video renderer will
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 6094513913..08e178878b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -27,6 +27,7 @@ import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
+import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.metadata.Metadata;
@@ -37,6 +38,7 @@ import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.util.List;
@@ -105,8 +107,7 @@ public class SimpleExoPlayer implements ExoPlayer {
private DecoderCounters videoDecoderCounters;
private DecoderCounters audioDecoderCounters;
private int audioSessionId;
- @C.StreamType
- private int audioStreamType;
+ private AudioAttributes audioAttributes;
private float audioVolume;
protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector,
@@ -136,7 +137,7 @@ public class SimpleExoPlayer implements ExoPlayer {
// Set initial values.
audioVolume = 1;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
- audioStreamType = C.STREAM_TYPE_DEFAULT;
+ audioAttributes = AudioAttributes.DEFAULT;
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
// Build the player and associated objects.
@@ -221,8 +222,9 @@ public class SimpleExoPlayer implements ExoPlayer {
if (surfaceHolder == null) {
setVideoSurfaceInternal(null, false);
} else {
- setVideoSurfaceInternal(surfaceHolder.getSurface(), false);
surfaceHolder.addCallback(componentListener);
+ Surface surface = surfaceHolder.getSurface();
+ setVideoSurfaceInternal(surface != null && surface.isValid() ? surface : null, false);
}
}
@@ -273,9 +275,10 @@ public class SimpleExoPlayer implements ExoPlayer {
if (textureView.getSurfaceTextureListener() != null) {
Log.w(TAG, "Replacing existing SurfaceTextureListener.");
}
- SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
- setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
textureView.setSurfaceTextureListener(componentListener);
+ SurfaceTexture surfaceTexture = textureView.isAvailable() ? textureView.getSurfaceTexture()
+ : null;
+ setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
}
}
@@ -292,33 +295,70 @@ public class SimpleExoPlayer implements ExoPlayer {
}
/**
- * Sets the stream type for audio playback (see {@link C.StreamType} and
- * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type
- * is not set, audio renderers use {@link C#STREAM_TYPE_DEFAULT}.
+ * Sets the stream type for audio playback, used by the underlying audio track.
*
- * Note that when the stream type changes, the AudioTrack must be reinitialized, which can
- * introduce a brief gap in audio output. Note also that tracks in the same audio session must
- * share the same routing, so a new audio session id will be generated.
+ * Setting the stream type during playback may introduce a short gap in audio output as the audio
+ * track is recreated. A new audio session id will also be generated.
+ *
+ * Calling this method overwrites any attributes set previously by calling
+ * {@link #setAudioAttributes(AudioAttributes)}.
*
- * @param audioStreamType The stream type for audio playback.
+ * @deprecated Use {@link #setAudioAttributes(AudioAttributes)}.
+ * @param streamType The stream type for audio playback.
*/
- public void setAudioStreamType(@C.StreamType int audioStreamType) {
- this.audioStreamType = audioStreamType;
+ @Deprecated
+ public void setAudioStreamType(@C.StreamType int streamType) {
+ @C.AudioUsage int usage = Util.getAudioUsageForStreamType(streamType);
+ @C.AudioContentType int contentType = Util.getAudioContentTypeForStreamType(streamType);
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder().setUsage(usage).setContentType(contentType).build();
+ setAudioAttributes(audioAttributes);
+ }
+
+ /**
+ * Returns the stream type for audio playback.
+ *
+ * @deprecated Use {@link #getAudioAttributes()}.
+ */
+ @Deprecated
+ public @C.StreamType int getAudioStreamType() {
+ return Util.getStreamTypeForAudioUsage(audioAttributes.usage);
+ }
+
+ /**
+ * Sets the attributes for audio playback, used by the underlying audio track. If not set, the
+ * default audio attributes will be used. They are suitable for general media playback.
+ *
+ * Setting the audio attributes during playback may introduce a short gap in audio output as the
+ * audio track is recreated. A new audio session id will also be generated.
+ *
+ * If tunneling is enabled by the track selector, the specified audio attributes will be ignored,
+ * but they will take effect if audio is later played without tunneling.
+ *
+ * If the device is running a build before platform API version 21, audio attributes cannot be set
+ * directly on the underlying audio track. In this case, the usage will be mapped onto an
+ * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.
+ *
+ * @param audioAttributes The attributes to use for audio playback.
+ */
+ public void setAudioAttributes(AudioAttributes audioAttributes) {
+ this.audioAttributes = audioAttributes;
ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
int count = 0;
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
- messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_STREAM_TYPE, audioStreamType);
+ messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES,
+ audioAttributes);
}
}
player.sendMessages(messages);
}
/**
- * Returns the stream type for audio playback.
+ * Returns the attributes for audio playback.
*/
- public @C.StreamType int getAudioStreamType() {
- return audioStreamType;
+ public AudioAttributes getAudioAttributes() {
+ return audioAttributes;
}
/**
@@ -480,12 +520,17 @@ public class SimpleExoPlayer implements ExoPlayer {
// ExoPlayer implementation
@Override
- public void addListener(EventListener listener) {
+ public Looper getPlaybackLooper() {
+ return player.getPlaybackLooper();
+ }
+
+ @Override
+ public void addListener(Player.EventListener listener) {
player.addListener(listener);
}
@Override
- public void removeListener(EventListener listener) {
+ public void removeListener(Player.EventListener listener) {
player.removeListener(listener);
}
@@ -514,6 +559,16 @@ public class SimpleExoPlayer implements ExoPlayer {
return player.getPlayWhenReady();
}
+ @Override
+ public @RepeatMode int getRepeatMode() {
+ return player.getRepeatMode();
+ }
+
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ player.setRepeatMode(repeatMode);
+ }
+
@Override
public boolean isLoading() {
return player.isLoading();
@@ -646,6 +701,26 @@ public class SimpleExoPlayer implements ExoPlayer {
return player.isCurrentWindowSeekable();
}
+ @Override
+ public boolean isPlayingAd() {
+ return player.isPlayingAd();
+ }
+
+ @Override
+ public int getCurrentAdGroupIndex() {
+ return player.getCurrentAdGroupIndex();
+ }
+
+ @Override
+ public int getCurrentAdIndexInAdGroup() {
+ return player.getCurrentAdIndexInAdGroup();
+ }
+
+ @Override
+ public long getContentPosition() {
+ return player.getContentPosition();
+ }
+
// Internal methods.
private void removeSurfaceCallbacks() {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
index eb3966ae4d..414c0804ad 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
@@ -15,18 +15,24 @@
*/
package com.google.android.exoplayer2;
+import android.util.Pair;
+import com.google.android.exoplayer2.util.Assertions;
+
/**
- * A representation of media currently available for playback.
- *
- * Timeline instances are immutable. For cases where the available media is changing dynamically
- * (e.g. live streams) a timeline provides a snapshot of the media currently available.
+ * A flexible representation of the structure of media. A timeline is able to represent the
+ * structure of a wide variety of media, from simple cases like a single media file through to
+ * complex compositions of media such as playlists and streams with inserted ads. Instances are
+ * immutable. For cases where media is changing dynamically (e.g. live streams), a timeline provides
+ * a snapshot of the current state.
*
* A timeline consists of related {@link Period}s and {@link Window}s. A period defines a single
- * logical piece of media, for example a media file. A window spans one or more periods, defining
- * the region within those periods that's currently available for playback along with additional
- * information such as whether seeking is supported within the window. Each window defines a default
- * position, which is the position from which playback will start when the player starts playing the
- * window. The following examples illustrate timelines for various use cases.
+ * logical piece of media, for example a media file. It may also define groups of ads inserted into
+ * the media, along with information about whether those ads have been loaded and played. A window
+ * spans one or more periods, defining the region within those periods that's currently available
+ * for playback along with additional information such as whether seeking is supported within the
+ * window. Each window defines a default position, which is the position from which playback will
+ * start when the player starts playing the window. The following examples illustrate timelines for
+ * various use cases.
*
*
* This case arises when a live stream is explicitly divided into separate periods, for example at
- * content and advert boundaries. This case is similar to the Live stream
- * with limited availability case, except that the window may span more than one period.
- * Multiple periods are also possible in the indefinite availability case.
+ * content boundaries. This case is similar to the Live stream with limited
+ * availability case, except that the window may span more than one period. Multiple periods are
+ * also possible in the indefinite availability case.
*
- *
On-demand pre-roll followed by live stream
+ *
On-demand stream followed by live stream
*
- *
*
* This case is the concatenation of the Single media file or on-demand
* stream and Live stream with multiple periods cases. When playback
- * of the pre-roll ends, playback of the live stream will start from its default position near the
- * live edge.
+ * of the on-demand stream ends, playback of the live stream will start from its default position
+ * near the live edge.
+ *
+ *
On-demand stream with mid-roll ads
+ *
+ *
+ *
+ * This case includes mid-roll ad groups, which are defined as part of the timeline's single period.
+ * The period can be queried for information about the ad groups and the ads they contain.
*/
public abstract class Timeline {
- /**
- * An empty timeline.
- */
- public static final Timeline EMPTY = new Timeline() {
-
- @Override
- public int getWindowCount() {
- return 0;
- }
-
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- throw new IndexOutOfBoundsException();
- }
-
- @Override
- public int getPeriodCount() {
- return 0;
- }
-
- @Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- throw new IndexOutOfBoundsException();
- }
-
- @Override
- public int getIndexOfPeriod(Object uid) {
- return C.INDEX_UNSET;
- }
-
- };
-
- /**
- * Returns whether the timeline is empty.
- */
- public final boolean isEmpty() {
- return getWindowCount() == 0;
- }
-
- /**
- * Returns the number of windows in the timeline.
- */
- public abstract int getWindowCount();
-
- /**
- * Populates a {@link Window} with data for the window at the specified index. Does not populate
- * {@link Window#id}.
- *
- * @param windowIndex The index of the window.
- * @param window The {@link Window} to populate. Must not be null.
- * @return The populated {@link Window}, for convenience.
- */
- public final Window getWindow(int windowIndex, Window window) {
- return getWindow(windowIndex, window, false);
- }
-
- /**
- * Populates a {@link Window} with data for the window at the specified index.
- *
- * @param windowIndex The index of the window.
- * @param window The {@link Window} to populate. Must not be null.
- * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
- * null. The caller should pass false for efficiency reasons unless the field is required.
- * @return The populated {@link Window}, for convenience.
- */
- public Window getWindow(int windowIndex, Window window, boolean setIds) {
- return getWindow(windowIndex, window, setIds, 0);
- }
-
- /**
- * Populates a {@link Window} with data for the window at the specified index.
- *
- * @param windowIndex The index of the window.
- * @param window The {@link Window} to populate. Must not be null.
- * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
- * null. The caller should pass false for efficiency reasons unless the field is required.
- * @param defaultPositionProjectionUs A duration into the future that the populated window's
- * default start position should be projected.
- * @return The populated {@link Window}, for convenience.
- */
- public abstract Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs);
-
- /**
- * Returns the number of periods in the timeline.
- */
- public abstract int getPeriodCount();
-
- /**
- * Populates a {@link Period} with data for the period at the specified index. Does not populate
- * {@link Period#id} and {@link Period#uid}.
- *
- * @param periodIndex The index of the period.
- * @param period The {@link Period} to populate. Must not be null.
- * @return The populated {@link Period}, for convenience.
- */
- public final Period getPeriod(int periodIndex, Period period) {
- return getPeriod(periodIndex, period, false);
- }
-
- /**
- * Populates a {@link Period} with data for the period at the specified index.
- *
- * @param periodIndex The index of the period.
- * @param period The {@link Period} to populate. Must not be null.
- * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false,
- * the fields will be set to null. The caller should pass false for efficiency reasons unless
- * the fields are required.
- * @return The populated {@link Period}, for convenience.
- */
- public abstract Period getPeriod(int periodIndex, Period period, boolean setIds);
-
- /**
- * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET}
- * if the period is not in the timeline.
- *
- * @param uid A unique identifier for a period.
- * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found.
- */
- public abstract int getIndexOfPeriod(Object uid);
-
/**
* Holds information about a window in a {@link Timeline}. A window defines a region of media
* currently available for playback along with additional information such as whether seeking is
- * supported within the window. See {@link Timeline} for more details. The figure below shows some
- * of the information defined by a window, as well as how this information relates to
- * corresponding {@link Period}s in the timeline.
+ * supported within the window. The figure below shows some of the information defined by a
+ * window, as well as how this information relates to corresponding {@link Period}s in the
+ * timeline.
*
*
*
@@ -354,9 +246,11 @@ public abstract class Timeline {
/**
* Holds information about a period in a {@link Timeline}. A period defines a single logical piece
- * of media, for example a a media file. See {@link Timeline} for more details. The figure below
- * shows some of the information defined by a period, as well as how this information relates to a
- * corresponding {@link Window} in the timeline.
+ * of media, for example a media file. It may also define groups of ads inserted into the media,
+ * along with information about whether those ads have been loaded and played.
+ *
+ * The figure below shows some of the information defined by a period, as well as how this
+ * information relates to a corresponding {@link Window} in the timeline.
*
*
*
@@ -383,24 +277,71 @@ public abstract class Timeline {
*/
public long durationUs;
- /**
- * Whether this period contains an ad.
- */
- public boolean isAd;
-
private long positionInWindowUs;
+ private long[] adGroupTimesUs;
+ private int[] adCounts;
+ private int[] adsLoadedCounts;
+ private int[] adsPlayedCounts;
+ private long[][] adDurationsUs;
+ private long adResumePositionUs;
/**
* Sets the data held by this period.
+ *
+ * @param id An identifier for the period. Not necessarily unique.
+ * @param uid A unique identifier for the period.
+ * @param windowIndex The index of the window to which this period belongs.
+ * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if
+ * unknown.
+ * @param positionInWindowUs The position of the start of this period relative to the start of
+ * the window to which it belongs, in milliseconds. May be negative if the start of the
+ * period is not within the window.
+ * @return This period, for convenience.
*/
public Period set(Object id, Object uid, int windowIndex, long durationUs,
- long positionInWindowUs, boolean isAd) {
+ long positionInWindowUs) {
+ return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null,
+ null, C.TIME_UNSET);
+ }
+
+ /**
+ * Sets the data held by this period.
+ *
+ * @param id An identifier for the period. Not necessarily unique.
+ * @param uid A unique identifier for the period.
+ * @param windowIndex The index of the window to which this period belongs.
+ * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if
+ * unknown.
+ * @param positionInWindowUs The position of the start of this period relative to the start of
+ * the window to which it belongs, in milliseconds. May be negative if the start of the
+ * period is not within the window.
+ * @param adGroupTimesUs The times of ad groups relative to the start of the period, in
+ * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
+ * the period has a postroll ad.
+ * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET}
+ * if the number of ads is not yet known.
+ * @param adsLoadedCounts The number of ads loaded so far in each ad group.
+ * @param adsPlayedCounts The number of ads played so far in each ad group.
+ * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element
+ * may be {@link C#TIME_UNSET} if the duration is not yet known.
+ * @param adResumePositionUs The position offset in the first unplayed ad at which to begin
+ * playback, in microseconds.
+ * @return This period, for convenience.
+ */
+ public Period set(Object id, Object uid, int windowIndex, long durationUs,
+ long positionInWindowUs, long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts,
+ int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs) {
this.id = id;
this.uid = uid;
this.windowIndex = windowIndex;
this.durationUs = durationUs;
this.positionInWindowUs = positionInWindowUs;
- this.isAd = isAd;
+ this.adGroupTimesUs = adGroupTimesUs;
+ this.adCounts = adCounts;
+ this.adsLoadedCounts = adsLoadedCounts;
+ this.adsPlayedCounts = adsPlayedCounts;
+ this.adDurationsUs = adDurationsUs;
+ this.adResumePositionUs = adResumePositionUs;
return this;
}
@@ -436,6 +377,403 @@ public abstract class Timeline {
return positionInWindowUs;
}
+ /**
+ * Returns the number of ad groups in the period.
+ */
+ public int getAdGroupCount() {
+ return adGroupTimesUs == null ? 0 : adGroupTimesUs.length;
+ }
+
+ /**
+ * Returns the time of the ad group at index {@code adGroupIndex} in the period, in
+ * microseconds.
+ *
+ * @param adGroupIndex The ad group index.
+ * @return The time of the ad group at the index, in microseconds.
+ */
+ public long getAdGroupTimeUs(int adGroupIndex) {
+ return adGroupTimesUs[adGroupIndex];
+ }
+
+ /**
+ * Returns the number of ads that have been played in the specified ad group in the period.
+ *
+ * @param adGroupIndex The ad group index.
+ * @return The number of ads that have been played.
+ */
+ public int getPlayedAdCount(int adGroupIndex) {
+ return adsPlayedCounts[adGroupIndex];
+ }
+
+ /**
+ * Returns whether the ad group at index {@code adGroupIndex} has been played.
+ *
+ * @param adGroupIndex The ad group index.
+ * @return Whether the ad group at index {@code adGroupIndex} has been played.
+ */
+ public boolean hasPlayedAdGroup(int adGroupIndex) {
+ return adCounts[adGroupIndex] != C.INDEX_UNSET
+ && adsPlayedCounts[adGroupIndex] == adCounts[adGroupIndex];
+ }
+
+ /**
+ * Returns the index of the ad group at or before {@code positionUs}, if that ad group is
+ * unplayed. Returns {@link C#INDEX_UNSET} if the ad group before {@code positionUs} has been
+ * played, or if there is no such ad group.
+ *
+ * @param positionUs The position at or before which to find an ad group, in microseconds.
+ * @return The index of the ad group, or {@link C#INDEX_UNSET}.
+ */
+ public int getAdGroupIndexForPositionUs(long positionUs) {
+ if (adGroupTimesUs == null) {
+ return C.INDEX_UNSET;
+ }
+ // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
+ // In practice we expect there to be few ad groups so the search shouldn't be expensive.
+ int index = adGroupTimesUs.length - 1;
+ while (index >= 0 && (adGroupTimesUs[index] == C.TIME_END_OF_SOURCE
+ || adGroupTimesUs[index] > positionUs)) {
+ index--;
+ }
+ return index >= 0 && !hasPlayedAdGroup(index) ? index : C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns the index of the next unplayed ad group after {@code positionUs}. Returns
+ * {@link C#INDEX_UNSET} if there is no such ad group.
+ *
+ * @param positionUs The position after which to find an ad group, in microseconds.
+ * @return The index of the ad group, or {@link C#INDEX_UNSET}.
+ */
+ public int getAdGroupIndexAfterPositionUs(long positionUs) {
+ if (adGroupTimesUs == null) {
+ return C.INDEX_UNSET;
+ }
+ // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
+ // In practice we expect there to be few ad groups so the search shouldn't be expensive.
+ int index = 0;
+ while (index < adGroupTimesUs.length && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE
+ && (positionUs >= adGroupTimesUs[index] || hasPlayedAdGroup(index))) {
+ index++;
+ }
+ return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns the number of ads in the ad group at index {@code adGroupIndex}, or
+ * {@link C#LENGTH_UNSET} if not yet known.
+ *
+ * @param adGroupIndex The ad group index.
+ * @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known.
+ */
+ public int getAdCountInAdGroup(int adGroupIndex) {
+ return adCounts[adGroupIndex];
+ }
+
+ /**
+ * Returns whether the URL for the specified ad is known.
+ *
+ * @param adGroupIndex The ad group index.
+ * @param adIndexInAdGroup The ad index in the ad group.
+ * @return Whether the URL for the specified ad is known.
+ */
+ public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) {
+ return adIndexInAdGroup < adsLoadedCounts[adGroupIndex];
+ }
+
+ /**
+ * Returns the duration of the ad at index {@code adIndexInAdGroup} in the ad group at
+ * {@code adGroupIndex}, in microseconds, or {@link C#TIME_UNSET} if not yet known.
+ *
+ * @param adGroupIndex The ad group index.
+ * @param adIndexInAdGroup The ad index in the ad group.
+ * @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known.
+ */
+ public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) {
+ if (adIndexInAdGroup >= adDurationsUs[adGroupIndex].length) {
+ return C.TIME_UNSET;
+ }
+ return adDurationsUs[adGroupIndex][adIndexInAdGroup];
+ }
+
+ /**
+ * Returns the position offset in the first unplayed ad at which to begin playback, in
+ * microseconds.
+ */
+ public long getAdResumePositionUs() {
+ return adResumePositionUs;
+ }
+
}
+ /**
+ * An empty timeline.
+ */
+ public static final Timeline EMPTY = new Timeline() {
+
+ @Override
+ public int getWindowCount() {
+ return 0;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 0;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return C.INDEX_UNSET;
+ }
+
+ };
+
+ /**
+ * Returns whether the timeline is empty.
+ */
+ public final boolean isEmpty() {
+ return getWindowCount() == 0;
+ }
+
+ /**
+ * Returns the number of windows in the timeline.
+ */
+ public abstract int getWindowCount();
+
+ /**
+ * Returns the index of the window after the window at index {@code windowIndex} depending on the
+ * {@code repeatMode}.
+ *
+ * @param windowIndex Index of a window in the timeline.
+ * @param repeatMode A repeat mode.
+ * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window.
+ */
+ public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ switch (repeatMode) {
+ case Player.REPEAT_MODE_OFF:
+ return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1;
+ case Player.REPEAT_MODE_ONE:
+ return windowIndex;
+ case Player.REPEAT_MODE_ALL:
+ return windowIndex == getWindowCount() - 1 ? 0 : windowIndex + 1;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Returns the index of the window before the window at index {@code windowIndex} depending on the
+ * {@code repeatMode}.
+ *
+ * @param windowIndex Index of a window in the timeline.
+ * @param repeatMode A repeat mode.
+ * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window.
+ */
+ public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ switch (repeatMode) {
+ case Player.REPEAT_MODE_OFF:
+ return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1;
+ case Player.REPEAT_MODE_ONE:
+ return windowIndex;
+ case Player.REPEAT_MODE_ALL:
+ return windowIndex == 0 ? getWindowCount() - 1 : windowIndex - 1;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Returns whether the given window is the last window of the timeline depending on the
+ * {@code repeatMode}.
+ *
+ * @param windowIndex A window index.
+ * @param repeatMode A repeat mode.
+ * @return Whether the window of the given index is the last window of the timeline.
+ */
+ public final boolean isLastWindow(int windowIndex, @Player.RepeatMode int repeatMode) {
+ return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns whether the given window is the first window of the timeline depending on the
+ * {@code repeatMode}.
+ *
+ * @param windowIndex A window index.
+ * @param repeatMode A repeat mode.
+ * @return Whether the window of the given index is the first window of the timeline.
+ */
+ public final boolean isFirstWindow(int windowIndex, @Player.RepeatMode int repeatMode) {
+ return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET;
+ }
+
+ /**
+ * Populates a {@link Window} with data for the window at the specified index. Does not populate
+ * {@link Window#id}.
+ *
+ * @param windowIndex The index of the window.
+ * @param window The {@link Window} to populate. Must not be null.
+ * @return The populated {@link Window}, for convenience.
+ */
+ public final Window getWindow(int windowIndex, Window window) {
+ return getWindow(windowIndex, window, false);
+ }
+
+ /**
+ * Populates a {@link Window} with data for the window at the specified index.
+ *
+ * @param windowIndex The index of the window.
+ * @param window The {@link Window} to populate. Must not be null.
+ * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
+ * null. The caller should pass false for efficiency reasons unless the field is required.
+ * @return The populated {@link Window}, for convenience.
+ */
+ public Window getWindow(int windowIndex, Window window, boolean setIds) {
+ return getWindow(windowIndex, window, setIds, 0);
+ }
+
+ /**
+ * Populates a {@link Window} with data for the window at the specified index.
+ *
+ * @param windowIndex The index of the window.
+ * @param window The {@link Window} to populate. Must not be null.
+ * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
+ * null. The caller should pass false for efficiency reasons unless the field is required.
+ * @param defaultPositionProjectionUs A duration into the future that the populated window's
+ * default start position should be projected.
+ * @return The populated {@link Window}, for convenience.
+ */
+ public abstract Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs);
+
+ /**
+ * Returns the number of periods in the timeline.
+ */
+ public abstract int getPeriodCount();
+
+ /**
+ * Returns the index of the period after the period at index {@code periodIndex} depending on the
+ * {@code repeatMode}.
+ *
+ * @param periodIndex Index of a period in the timeline.
+ * @param period A {@link Period} to be used internally. Must not be null.
+ * @param window A {@link Window} to be used internally. Must not be null.
+ * @param repeatMode A repeat mode.
+ * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period.
+ */
+ public final int getNextPeriodIndex(int periodIndex, Period period, Window window,
+ @Player.RepeatMode int repeatMode) {
+ int windowIndex = getPeriod(periodIndex, period).windowIndex;
+ if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) {
+ int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode);
+ if (nextWindowIndex == C.INDEX_UNSET) {
+ return C.INDEX_UNSET;
+ }
+ return getWindow(nextWindowIndex, window).firstPeriodIndex;
+ }
+ return periodIndex + 1;
+ }
+
+ /**
+ * Returns whether the given period is the last period of the timeline depending on the
+ * {@code repeatMode}.
+ *
+ * @param periodIndex A period index.
+ * @param period A {@link Period} to be used internally. Must not be null.
+ * @param window A {@link Window} to be used internally. Must not be null.
+ * @param repeatMode A repeat mode.
+ * @return Whether the period of the given index is the last period of the timeline.
+ */
+ public final boolean isLastPeriod(int periodIndex, Period period, Window window,
+ @Player.RepeatMode int repeatMode) {
+ return getNextPeriodIndex(periodIndex, period, window, repeatMode) == C.INDEX_UNSET;
+ }
+
+ /**
+ * Populates a {@link Period} with data for the period at the specified index. Does not populate
+ * {@link Period#id} and {@link Period#uid}.
+ *
+ * @param periodIndex The index of the period.
+ * @param period The {@link Period} to populate. Must not be null.
+ * @return The populated {@link Period}, for convenience.
+ */
+ public final Period getPeriod(int periodIndex, Period period) {
+ return getPeriod(periodIndex, period, false);
+ }
+
+ /**
+ * Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position
+ * projection.
+ */
+ public final Pair getPeriodPosition(Window window, Period period, int windowIndex,
+ long windowPositionUs) {
+ return getPeriodPosition(window, period, windowIndex, windowPositionUs, 0);
+ }
+
+ /**
+ * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs).
+ *
+ * @param window A {@link Window} that may be overwritten.
+ * @param period A {@link Period} that may be overwritten.
+ * @param windowIndex The window index.
+ * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default
+ * start position.
+ * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the
+ * duration into the future by which the window's position should be projected.
+ * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs}
+ * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's
+ * position could not be projected by {@code defaultPositionProjectionUs}.
+ */
+ public final Pair getPeriodPosition(Window window, Period period, int windowIndex,
+ long windowPositionUs, long defaultPositionProjectionUs) {
+ Assertions.checkIndex(windowIndex, 0, getWindowCount());
+ getWindow(windowIndex, window, false, defaultPositionProjectionUs);
+ if (windowPositionUs == C.TIME_UNSET) {
+ windowPositionUs = window.getDefaultPositionUs();
+ if (windowPositionUs == C.TIME_UNSET) {
+ return null;
+ }
+ }
+ int periodIndex = window.firstPeriodIndex;
+ long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
+ long periodDurationUs = getPeriod(periodIndex, period).getDurationUs();
+ while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
+ && periodIndex < window.lastPeriodIndex) {
+ periodPositionUs -= periodDurationUs;
+ periodDurationUs = getPeriod(++periodIndex, period).getDurationUs();
+ }
+ return Pair.create(periodIndex, periodPositionUs);
+ }
+
+ /**
+ * Populates a {@link Period} with data for the period at the specified index.
+ *
+ * @param periodIndex The index of the period.
+ * @param period The {@link Period} to populate. Must not be null.
+ * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false,
+ * the fields will be set to null. The caller should pass false for efficiency reasons unless
+ * the fields are required.
+ * @return The populated {@link Period}, for convenience.
+ */
+ public abstract Period getPeriod(int periodIndex, Period period, boolean setIds);
+
+ /**
+ * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET}
+ * if the period is not in the timeline.
+ *
+ * @param uid A unique identifier for a period.
+ * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found.
+ */
+ public abstract int getIndexOfPeriod(Object uid);
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
new file mode 100644
index 0000000000..337200da8f
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import android.annotation.TargetApi;
+import com.google.android.exoplayer2.C;
+
+/**
+ * Attributes for audio playback, which configure the underlying platform
+ * {@link android.media.AudioTrack}.
+ *
+ * To set the audio attributes, create an instance using the {@link Builder} and either pass it to
+ * {@link com.google.android.exoplayer2.SimpleExoPlayer#setAudioAttributes(AudioAttributes)} or
+ * send a message of type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to the audio renderers.
+ *
+ * This class is based on {@link android.media.AudioAttributes}, but can be used on all supported
+ * API versions.
+ */
+public final class AudioAttributes {
+
+ public static final AudioAttributes DEFAULT = new Builder().build();
+
+ /**
+ * Builder for {@link AudioAttributes}.
+ */
+ public static final class Builder {
+
+ @C.AudioContentType
+ private int contentType;
+ @C.AudioFlags
+ private int flags;
+ @C.AudioUsage
+ private int usage;
+
+ /**
+ * Creates a new builder for {@link AudioAttributes}.
+ *
+ * By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is
+ * {@link C#USAGE_MEDIA}, and no flags are set.
+ */
+ public Builder() {
+ contentType = C.CONTENT_TYPE_UNKNOWN;
+ flags = 0;
+ usage = C.USAGE_MEDIA;
+ }
+
+ /**
+ * @see android.media.AudioAttributes.Builder#setContentType(int)
+ */
+ public Builder setContentType(@C.AudioContentType int contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
+ /**
+ * @see android.media.AudioAttributes.Builder#setFlags(int)
+ */
+ public Builder setFlags(@C.AudioFlags int flags) {
+ this.flags = flags;
+ return this;
+ }
+
+ /**
+ * @see android.media.AudioAttributes.Builder#setUsage(int)
+ */
+ public Builder setUsage(@C.AudioUsage int usage) {
+ this.usage = usage;
+ return this;
+ }
+
+ /**
+ * Creates an {@link AudioAttributes} instance from this builder.
+ */
+ public AudioAttributes build() {
+ return new AudioAttributes(contentType, flags, usage);
+ }
+
+ }
+
+ @C.AudioContentType
+ public final int contentType;
+ @C.AudioFlags
+ public final int flags;
+ @C.AudioUsage
+ public final int usage;
+
+ private android.media.AudioAttributes audioAttributesV21;
+
+ private AudioAttributes(@C.AudioContentType int contentType, @C.AudioFlags int flags,
+ @C.AudioUsage int usage) {
+ this.contentType = contentType;
+ this.flags = flags;
+ this.usage = usage;
+ }
+
+ @TargetApi(21)
+ /* package */ android.media.AudioAttributes getAudioAttributesV21() {
+ if (audioAttributesV21 == null) {
+ audioAttributesV21 = new android.media.AudioAttributes.Builder()
+ .setContentType(contentType)
+ .setFlags(flags)
+ .setUsage(usage)
+ .build();
+ }
+ return audioAttributesV21;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ AudioAttributes other = (AudioAttributes) obj;
+ return this.contentType == other.contentType && this.flags == other.flags
+ && this.usage == other.usage;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + contentType;
+ result = 31 * result + flags;
+ result = 31 * result + usage;
+ return result;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
index 92838e34b0..79cb26bf39 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.audio;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
-import android.media.AudioAttributes;
import android.media.AudioFormat;
+import android.media.AudioManager;
import android.media.AudioTimestamp;
import android.os.ConditionVariable;
import android.os.SystemClock;
@@ -40,9 +40,9 @@ import java.util.LinkedList;
*
* Before starting playback, specify the input format by calling
* {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)},
- * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()}
- * to configure audio playback. These methods may be called after writing data to the track, in
- * which case it will be reinitialized as required.
+ * {@link #setAudioAttributes(AudioAttributes)}, {@link #enableTunnelingV21(int)} and
+ * {@link #disableTunneling()} to configure audio playback. These methods may be called after
+ * writing data to the track, in which case it will be reinitialized as required.
*
* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}
* when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
@@ -299,8 +299,7 @@ public final class AudioTrack {
private int encoding;
@C.Encoding
private int outputEncoding;
- @C.StreamType
- private int streamType;
+ private AudioAttributes audioAttributes;
private boolean passthrough;
private int bufferSize;
private long bufferSizeUs;
@@ -384,7 +383,7 @@ public final class AudioTrack {
playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
volume = 1.0f;
startMediaTimeState = START_NOT_SET;
- streamType = C.STREAM_TYPE_DEFAULT;
+ audioAttributes = AudioAttributes.DEFAULT;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
playbackParameters = PlaybackParameters.DEFAULT;
drainingAudioProcessorIndex = C.INDEX_UNSET;
@@ -634,19 +633,7 @@ public final class AudioTrack {
// initialization of the audio track to fail.
releasingConditionVariable.block();
- if (tunneling) {
- audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding,
- bufferSize, audioSessionId);
- } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
- audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
- outputEncoding, bufferSize, MODE_STREAM);
- } else {
- // Re-attach to the same audio session.
- audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
- outputEncoding, bufferSize, MODE_STREAM, audioSessionId);
- }
- checkAudioTrackInitialized();
-
+ audioTrack = initializeAudioTrack();
int audioSessionId = audioTrack.getAudioSessionId();
if (enablePreV21AudioSessionWorkaround) {
if (Util.SDK_INT < 21) {
@@ -657,12 +644,7 @@ public final class AudioTrack {
releaseKeepSessionIdAudioTrack();
}
if (keepSessionIdAudioTrack == null) {
- int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
- int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
- @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
- int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
- keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate,
- channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId);
+ keepSessionIdAudioTrack = initializeKeepSessionIdAudioTrack(audioSessionId);
}
}
}
@@ -1021,23 +1003,23 @@ public final class AudioTrack {
}
/**
- * Sets the stream type for audio track. If the stream type has changed and if the audio track
+ * Sets the attributes for audio playback. If the attributes have changed and if the audio track
* is not configured for use with tunneling, then the audio track is reset and the audio session
* id is cleared.
*
- * If the audio track is configured for use with tunneling then the stream type is ignored, the
- * audio track is not reset and the audio session id is not cleared. The passed stream type will
- * be used if the audio track is later re-configured into non-tunneled mode.
+ * If the audio track is configured for use with tunneling then the audio attributes are ignored.
+ * The audio track is not reset and the audio session id is not cleared. The passed attributes
+ * will be used if the audio track is later re-configured into non-tunneled mode.
*
- * @param streamType The {@link C.StreamType} to use for audio output.
+ * @param audioAttributes The attributes for audio playback.
*/
- public void setStreamType(@C.StreamType int streamType) {
- if (this.streamType == streamType) {
+ public void setAudioAttributes(AudioAttributes audioAttributes) {
+ if (this.audioAttributes.equals(audioAttributes)) {
return;
}
- this.streamType = streamType;
+ this.audioAttributes = audioAttributes;
if (tunneling) {
- // The stream type is ignored in tunneling mode, so no need to reset.
+ // The audio attributes are ignored in tunneling mode, so no need to reset.
return;
}
reset();
@@ -1333,31 +1315,6 @@ public final class AudioTrack {
}
}
- /**
- * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this
- * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an
- * exception is thrown.
- *
- * @throws InitializationException If {@link #audioTrack} has not been successfully initialized.
- */
- private void checkAudioTrackInitialized() throws InitializationException {
- int state = audioTrack.getState();
- if (state == STATE_INITIALIZED) {
- return;
- }
- // The track is not successfully initialized. Release and null the track.
- try {
- audioTrack.release();
- } catch (Exception e) {
- // The track has already failed to initialize, so it wouldn't be that surprising if release
- // were to fail too. Swallow the exception.
- } finally {
- audioTrack = null;
- }
-
- throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
- }
-
private boolean isInitialized() {
return audioTrack != null;
}
@@ -1408,24 +1365,65 @@ public final class AudioTrack {
&& audioTrack.getPlaybackHeadPosition() == 0;
}
- /**
- * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback.
- */
+ private android.media.AudioTrack initializeAudioTrack() throws InitializationException {
+ android.media.AudioTrack audioTrack;
+ if (Util.SDK_INT >= 21) {
+ audioTrack = createAudioTrackV21();
+ } else {
+ int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage);
+ if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
+ audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
+ outputEncoding, bufferSize, MODE_STREAM);
+ } else {
+ // Re-attach to the same audio session.
+ audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
+ outputEncoding, bufferSize, MODE_STREAM, audioSessionId);
+ }
+ }
+
+ int state = audioTrack.getState();
+ if (state != STATE_INITIALIZED) {
+ try {
+ audioTrack.release();
+ } catch (Exception e) {
+ // The track has already failed to initialize, so it wouldn't be that surprising if release
+ // were to fail too. Swallow the exception.
+ }
+ throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
+ }
+ return audioTrack;
+ }
+
@TargetApi(21)
- private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate,
- int channelConfig, int encoding, int bufferSize, int sessionId) {
- AudioAttributes attributesBuilder = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_MEDIA)
- .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
- .setFlags(AudioAttributes.FLAG_HW_AV_SYNC)
- .build();
+ private android.media.AudioTrack createAudioTrackV21() {
+ android.media.AudioAttributes attributes;
+ if (tunneling) {
+ attributes = new android.media.AudioAttributes.Builder()
+ .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)
+ .setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC)
+ .setUsage(android.media.AudioAttributes.USAGE_MEDIA)
+ .build();
+ } else {
+ attributes = audioAttributes.getAudioAttributesV21();
+ }
AudioFormat format = new AudioFormat.Builder()
.setChannelMask(channelConfig)
- .setEncoding(encoding)
+ .setEncoding(outputEncoding)
.setSampleRate(sampleRate)
.build();
- return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM,
- sessionId);
+ int audioSessionId = this.audioSessionId != C.AUDIO_SESSION_ID_UNSET ? this.audioSessionId
+ : AudioManager.AUDIO_SESSION_ID_GENERATE;
+ return new android.media.AudioTrack(attributes, format, bufferSize, MODE_STREAM,
+ audioSessionId);
+ }
+
+ private android.media.AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) {
+ int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
+ int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+ @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
+ int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
+ return new android.media.AudioTrack(C.STREAM_TYPE_DEFAULT, sampleRate, channelConfig, encoding,
+ bufferSize, MODE_STATIC, audioSessionId);
}
@C.Encoding
@@ -1465,7 +1463,7 @@ public final class AudioTrack {
@TargetApi(21)
private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack,
ByteBuffer buffer, int size, long presentationTimeUs) {
- // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed.
+ // TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed.
// if (Util.SDK_INT >= 23) {
// // The underlying platform AudioTrack writes AV sync headers directly.
// return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
index 48c7462b03..4d97c292ac 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -399,9 +399,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
case C.MSG_SET_VOLUME:
audioTrack.setVolume((Float) message);
break;
- case C.MSG_SET_STREAM_TYPE:
- @C.StreamType int streamType = (Integer) message;
- audioTrack.setStreamType(streamType);
+ case C.MSG_SET_AUDIO_ATTRIBUTES:
+ AudioAttributes audioAttributes = (AudioAttributes) message;
+ audioTrack.setAudioAttributes(audioAttributes);
break;
default:
super.handleMessage(messageType, message);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
index ddb870f6ff..c4a55eeb02 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
@@ -376,15 +377,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null) {
+ if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
- return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
- && (bufferEncrypted || !playClearSamplesWithoutKeys);
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
private void processEndOfStream() throws ExoPlaybackException {
@@ -514,13 +514,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
drmSession = pendingDrmSession;
ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) {
- @DrmSession.State int drmSessionState = drmSession.getState();
- if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
- } else if (drmSessionState == DrmSession.STATE_OPENED
- || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
- mediaCrypto = drmSession.getMediaCrypto();
- } else {
+ mediaCrypto = drmSession.getMediaCrypto();
+ if (mediaCrypto == null) {
+ DrmSessionException drmError = drmSession.getError();
+ if (drmError != null) {
+ throw ExoPlaybackException.createForRenderer(drmError, getIndex());
+ }
// The drm session isn't open yet.
return;
}
@@ -595,9 +594,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
case C.MSG_SET_VOLUME:
audioTrack.setVolume((Float) message);
break;
- case C.MSG_SET_STREAM_TYPE:
- @C.StreamType int streamType = (Integer) message;
- audioTrack.setStreamType(streamType);
+ case C.MSG_SET_AUDIO_ATTRIBUTES:
+ AudioAttributes audioAttributes = (AudioAttributes) message;
+ audioTrack.setAudioAttributes(audioAttributes);
break;
default:
super.handleMessage(messageType, message);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
index 0d143cdf49..ec17de8d74 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
@@ -52,11 +52,11 @@ public final class CryptoInfo {
/**
* @see android.media.MediaCodec.CryptoInfo.Pattern
*/
- public int patternBlocksToEncrypt;
+ public int encryptedBlocks;
/**
* @see android.media.MediaCodec.CryptoInfo.Pattern
*/
- public int patternBlocksToSkip;
+ public int clearBlocks;
private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
private final PatternHolderV24 patternHolder;
@@ -70,28 +70,20 @@ public final class CryptoInfo {
* @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int)
*/
public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
- byte[] key, byte[] iv, @C.CryptoMode int mode) {
+ byte[] key, byte[] iv, @C.CryptoMode int mode, int encryptedBlocks, int clearBlocks) {
this.numSubSamples = numSubSamples;
this.numBytesOfClearData = numBytesOfClearData;
this.numBytesOfEncryptedData = numBytesOfEncryptedData;
this.key = key;
this.iv = iv;
this.mode = mode;
- patternBlocksToEncrypt = 0;
- patternBlocksToSkip = 0;
+ this.encryptedBlocks = encryptedBlocks;
+ this.clearBlocks = clearBlocks;
if (Util.SDK_INT >= 16) {
updateFrameworkCryptoInfoV16();
}
}
- public void setPattern(int patternBlocksToEncrypt, int patternBlocksToSkip) {
- this.patternBlocksToEncrypt = patternBlocksToEncrypt;
- this.patternBlocksToSkip = patternBlocksToSkip;
- if (Util.SDK_INT >= 24) {
- patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip);
- }
- }
-
/**
* Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
*
@@ -122,7 +114,7 @@ public final class CryptoInfo {
frameworkCryptoInfo.iv = iv;
frameworkCryptoInfo.mode = mode;
if (Util.SDK_INT >= 24) {
- patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip);
+ patternHolder.set(encryptedBlocks, clearBlocks);
}
}
@@ -137,8 +129,8 @@ public final class CryptoInfo {
pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0);
}
- private void set(int blocksToEncrypt, int blocksToSkip) {
- pattern.set(blocksToEncrypt, blocksToSkip);
+ private void set(int encryptedBlocks, int clearBlocks) {
+ pattern.set(encryptedBlocks, clearBlocks);
frameworkCryptoInfo.setPattern(pattern);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
index 6fc149ba32..cafbe6e8f7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -25,6 +25,7 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
@@ -222,7 +223,6 @@ public class DefaultDrmSessionManager implements DrmSe
this.eventHandler = eventHandler;
this.eventListener = eventListener;
mediaDrm.setOnEventListener(new MediaDrmEventListener());
- state = STATE_CLOSED;
mode = MODE_PLAYBACK;
}
@@ -307,6 +307,26 @@ public class DefaultDrmSessionManager implements DrmSe
// DrmSessionManager implementation.
+ @Override
+ public boolean canAcquireSession(@NonNull DrmInitData drmInitData) {
+ SchemeData schemeData = drmInitData.get(uuid);
+ if (schemeData == null) {
+ // No data for this manager's scheme.
+ return false;
+ }
+ String schemeType = schemeData.type;
+ if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) {
+ // If there is no scheme information, assume patternless AES-CTR.
+ return true;
+ } else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)
+ || C.CENC_TYPE_cens.equals(schemeType)) {
+ // AES-CBC and pattern encryption are supported on API 24 onwards.
+ return Util.SDK_INT >= 24;
+ }
+ // Unknown schemes, assume one of them is supported.
+ return true;
+ }
+
@Override
public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);
@@ -358,7 +378,7 @@ public class DefaultDrmSessionManager implements DrmSe
if (--openCount != 0) {
return;
}
- state = STATE_CLOSED;
+ state = STATE_RELEASED;
provisioningInProgress = false;
mediaDrmHandler.removeCallbacksAndMessages(null);
postResponseHandler.removeCallbacksAndMessages(null);
@@ -384,35 +404,19 @@ public class DefaultDrmSessionManager implements DrmSe
return state;
}
- @Override
- public final T getMediaCrypto() {
- if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
- throw new IllegalStateException();
- }
- return mediaCrypto;
- }
-
- @Override
- public boolean requiresSecureDecoderComponent(String mimeType) {
- if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
- throw new IllegalStateException();
- }
- return mediaCrypto.requiresSecureDecoderComponent(mimeType);
- }
-
@Override
public final DrmSessionException getError() {
return state == STATE_ERROR ? lastException : null;
}
+ @Override
+ public final T getMediaCrypto() {
+ return mediaCrypto;
+ }
+
@Override
public Map queryKeyStatus() {
- // User may call this method rightfully even if state == STATE_ERROR. So only check if there is
- // a sessionId
- if (sessionId == null) {
- throw new IllegalStateException();
- }
- return mediaDrm.queryKeyStatus(sessionId);
+ return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId);
}
@Override
@@ -513,6 +517,8 @@ public class DefaultDrmSessionManager implements DrmSe
}
break;
case MODE_RELEASE:
+ // It's not necessary to restore the key (and open a session to do that) before releasing it
+ // but this serves as a good sanity/fast-failure check.
if (restoreKeys()) {
postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
index 5126628dd9..9fa6547a00 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.util.Assertions;
@@ -102,6 +103,33 @@ public final class DrmInitData implements Comparator, Parcelable {
return schemeDatas[index];
}
+ /**
+ * Returns a copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated
+ * to have the specified scheme type.
+ *
+ * @param schemeType A protection scheme type. May be null.
+ * @return A copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated
+ * to have the specified scheme type.
+ */
+ public DrmInitData copyWithSchemeType(@Nullable String schemeType) {
+ boolean isCopyRequired = false;
+ for (SchemeData schemeData : schemeDatas) {
+ if (!Util.areEqual(schemeData.type, schemeType)) {
+ isCopyRequired = true;
+ break;
+ }
+ }
+ if (isCopyRequired) {
+ SchemeData[] schemeDatas = new SchemeData[this.schemeDatas.length];
+ for (int i = 0; i < schemeDatas.length; i++) {
+ schemeDatas[i] = this.schemeDatas[i].copyWithSchemeType(schemeType);
+ }
+ return new DrmInitData(schemeDatas);
+ } else {
+ return this;
+ }
+ }
+
@Override
public int hashCode() {
if (hashCode == 0) {
@@ -167,6 +195,10 @@ public final class DrmInitData implements Comparator, Parcelable {
* applies to all schemes).
*/
private final UUID uuid;
+ /**
+ * The protection scheme type, or null if not applicable or unknown.
+ */
+ @Nullable public final String type;
/**
* The mimeType of {@link #data}.
*/
@@ -183,22 +215,26 @@ public final class DrmInitData implements Comparator, Parcelable {
/**
* @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
* universal (i.e. applies to all schemes).
+ * @param type The type of the protection scheme, or null if not applicable or unknown.
* @param mimeType The mimeType of the initialization data.
* @param data The initialization data.
*/
- public SchemeData(UUID uuid, String mimeType, byte[] data) {
- this(uuid, mimeType, data, false);
+ public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data) {
+ this(uuid, type, mimeType, data, false);
}
/**
* @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
* universal (i.e. applies to all schemes).
+ * @param type The type of the protection scheme, or null if not applicable or unknown.
* @param mimeType The mimeType of the initialization data.
* @param data The initialization data.
* @param requiresSecureDecryption Whether secure decryption is required.
*/
- public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) {
+ public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data,
+ boolean requiresSecureDecryption) {
this.uuid = Assertions.checkNotNull(uuid);
+ this.type = type;
this.mimeType = Assertions.checkNotNull(mimeType);
this.data = Assertions.checkNotNull(data);
this.requiresSecureDecryption = requiresSecureDecryption;
@@ -206,6 +242,7 @@ public final class DrmInitData implements Comparator, Parcelable {
/* package */ SchemeData(Parcel in) {
uuid = new UUID(in.readLong(), in.readLong());
+ type = in.readString();
mimeType = in.readString();
data = in.createByteArray();
requiresSecureDecryption = in.readByte() != 0;
@@ -221,6 +258,19 @@ public final class DrmInitData implements Comparator, Parcelable {
return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid);
}
+ /**
+ * Returns a copy of the {@link SchemeData} instance with the given scheme type.
+ *
+ * @param type A protection scheme type.
+ * @return A copy of the {@link SchemeData} instance with the given scheme type.
+ */
+ public SchemeData copyWithSchemeType(String type) {
+ if (Util.areEqual(this.type, type)) {
+ return this;
+ }
+ return new SchemeData(uuid, type, mimeType, data, requiresSecureDecryption);
+ }
+
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SchemeData)) {
@@ -231,13 +281,14 @@ public final class DrmInitData implements Comparator, Parcelable {
}
SchemeData other = (SchemeData) obj;
return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid)
- && Arrays.equals(data, other.data);
+ && Util.areEqual(type, other.type) && Arrays.equals(data, other.data);
}
@Override
public int hashCode() {
if (hashCode == 0) {
int result = uuid.hashCode();
+ result = 31 * result + (type == null ? 0 : type.hashCode());
result = 31 * result + mimeType.hashCode();
result = 31 * result + Arrays.hashCode(data);
hashCode = result;
@@ -256,6 +307,7 @@ public final class DrmInitData implements Comparator, Parcelable {
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(uuid.getMostSignificantBits());
dest.writeLong(uuid.getLeastSignificantBits());
+ dest.writeString(type);
dest.writeString(mimeType);
dest.writeByteArray(data);
dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0));
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java
index cd694396b7..0c17b102fd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java
@@ -41,16 +41,16 @@ public interface DrmSession {
* The state of the DRM session.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STATE_ERROR, STATE_CLOSED, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
- @interface State {}
+ @IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
+ public @interface State {}
+ /**
+ * The session has been released.
+ */
+ int STATE_RELEASED = 0;
/**
* The session has encountered an error. {@link #getError()} can be used to retrieve the cause.
*/
- int STATE_ERROR = 0;
- /**
- * The session is closed.
- */
- int STATE_CLOSED = 1;
+ int STATE_ERROR = 1;
/**
* The session is being opened.
*/
@@ -65,66 +65,40 @@ public interface DrmSession {
int STATE_OPENED_WITH_KEYS = 4;
/**
- * Returns the current state of the session.
- *
- * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
- * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
+ * Returns the current state of the session, which is one of {@link #STATE_ERROR},
+ * {@link #STATE_RELEASED}, {@link #STATE_OPENING}, {@link #STATE_OPENED} and
+ * {@link #STATE_OPENED_WITH_KEYS}.
*/
@State int getState();
- /**
- * Returns a {@link ExoMediaCrypto} for the open session.
- *
- * This method may be called when the session is in the following states:
- * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
- *
- * @return A {@link ExoMediaCrypto} for the open session.
- * @throws IllegalStateException If called when a session isn't opened.
- */
- T getMediaCrypto();
-
- /**
- * Whether the session requires a secure decoder for the specified mime type.
- *
- * Normally this method should return
- * {@link ExoMediaCrypto#requiresSecureDecoderComponent(String)}, however in some cases
- * implementations may wish to modify the return value (i.e. to force a secure decoder even when
- * one is not required).
- *
- * This method may be called when the session is in the following states:
- * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
- *
- * @return Whether the open session requires a secure decoder for the specified mime type.
- * @throws IllegalStateException If called when a session isn't opened.
- */
- boolean requiresSecureDecoderComponent(String mimeType);
-
/**
* Returns the cause of the error state.
- *
- * This method may be called when the session is in any state.
- *
- * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
*/
DrmSessionException getError();
/**
- * Returns an informative description of the key status for the session. The status is in the form
- * of {name, value} pairs.
- *
- *
Since DRM license policies vary by vendor, the specific status field names are determined by
+ * Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has
+ * been opened or after it's been released.
+ */
+ T getMediaCrypto();
+
+ /**
+ * Returns a map describing the key status for the session, or null if called before the session
+ * has been opened or after it's been released.
+ *
+ * Since DRM license policies vary by vendor, the specific status field names are determined by
* each DRM vendor. Refer to your DRM provider documentation for definitions of the field names
* for a particular DRM engine plugin.
*
- * @return A map of key status.
- * @throws IllegalStateException If called when the session isn't opened.
+ * @return A map describing the key status for the session, or null if called before the session
+ * has been opened or after it's been released.
* @see MediaDrm#queryKeyStatus(byte[])
*/
Map queryKeyStatus();
/**
- * Returns the key set id of the offline license loaded into this session, if there is one. Null
- * otherwise.
+ * Returns the key set id of the offline license loaded into this session, or null if there isn't
+ * one.
*/
byte[] getOfflineLicenseKeySetId();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
index 8e63fbfaae..e4b7059860 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
@@ -24,6 +24,16 @@ import android.os.Looper;
@TargetApi(16)
public interface DrmSessionManager {
+ /**
+ * Returns whether the manager is capable of acquiring a session for the given
+ * {@link DrmInitData}.
+ *
+ * @param drmInitData DRM initialization data.
+ * @return Whether the manager is capable of acquiring a session for the given
+ * {@link DrmInitData}.
+ */
+ boolean canAcquireSession(DrmInitData drmInitData);
+
/**
* Acquires a {@link DrmSession} for the specified {@link DrmInitData}. The {@link DrmSession}
* must be returned to {@link #releaseSession(DrmSession)} when it is no longer required.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
index dd441a022f..5bee85f449 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
@@ -26,9 +26,12 @@ import com.google.android.exoplayer2.util.Assertions;
public final class FrameworkMediaCrypto implements ExoMediaCrypto {
private final MediaCrypto mediaCrypto;
+ private final boolean forceAllowInsecureDecoderComponents;
- /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto) {
+ /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto,
+ boolean forceAllowInsecureDecoderComponents) {
this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
+ this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
}
public MediaCrypto getWrappedMediaCrypto() {
@@ -37,7 +40,8 @@ public final class FrameworkMediaCrypto implements ExoMediaCrypto {
@Override
public boolean requiresSecureDecoderComponent(String mimeType) {
- return mediaCrypto.requiresSecureDecoderComponent(mimeType);
+ return !forceAllowInsecureDecoderComponents
+ && mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
index e6887af6da..ed4494559a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -24,7 +24,9 @@ import android.media.NotProvisionedException;
import android.media.ResourceBusyException;
import android.media.UnsupportedSchemeException;
import android.support.annotation.NonNull;
+import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@@ -163,7 +165,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm PLAYREADY_KEY_REQUEST_PROPERTIES;
- static {
- PLAYREADY_KEY_REQUEST_PROPERTIES = new HashMap<>();
- PLAYREADY_KEY_REQUEST_PROPERTIES.put("Content-Type", "text/xml");
- PLAYREADY_KEY_REQUEST_PROPERTIES.put("SOAPAction",
- "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense");
- }
-
private final HttpDataSource.Factory dataSourceFactory;
private final String defaultUrl;
private final Map keyRequestProperties;
@@ -124,10 +116,15 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
url = defaultUrl;
}
Map requestProperties = new HashMap<>();
- requestProperties.put("Content-Type", "application/octet-stream");
+ // Add standard request properties for supported schemes.
+ String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml"
+ : (C.CLEARKEY_UUID.equals(uuid) ? "application/json" : "application/octet-stream");
+ requestProperties.put("Content-Type", contentType);
if (C.PLAYREADY_UUID.equals(uuid)) {
- requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
+ requestProperties.put("SOAPAction",
+ "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense");
}
+ // Add additional request properties.
synchronized (keyRequestProperties) {
requestProperties.putAll(keyRequestProperties);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java
index e5d014f102..45c38b3609 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java
@@ -34,14 +34,16 @@ public final class WidevineUtil {
/**
* Returns license and playback durations remaining in seconds.
*
- * @return A {@link Pair} consisting of the remaining license and playback durations in seconds.
- * @throws IllegalStateException If called when a session isn't opened.
- * @param drmSession
+ * @param drmSession The drm session to query.
+ * @return A {@link Pair} consisting of the remaining license and playback durations in seconds,
+ * or null if called before the session has been opened or after it's been released.
*/
public static Pair getLicenseDurationRemainingSec(DrmSession> drmSession) {
Map keyStatus = drmSession.queryKeyStatus();
- return new Pair<>(
- getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
+ if (keyStatus == null) {
+ return null;
+ }
+ return new Pair<>(getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
index c47a91b176..ccc5c0eb3e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
@@ -117,7 +117,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
/**
* Sets the mode for {@link TsExtractor} instances created by the factory.
*
- * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory).
+ * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory)
* @param mode The mode to use.
* @return The factory, for convenience.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
deleted file mode 100644
index 1c9a148226..0000000000
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
+++ /dev/null
@@ -1,997 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.extractor;
-
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.FormatHolder;
-import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-import com.google.android.exoplayer2.upstream.Allocation;
-import com.google.android.exoplayer2.upstream.Allocator;
-import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.ParsableByteArray;
-import com.google.android.exoplayer2.util.Util;
-import java.io.EOFException;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * A {@link TrackOutput} that buffers extracted samples in a queue and allows for consumption from
- * that queue.
- */
-public final class DefaultTrackOutput implements TrackOutput {
-
- /**
- * A listener for changes to the upstream format.
- */
- public interface UpstreamFormatChangedListener {
-
- /**
- * Called on the loading thread when an upstream format change occurs.
- *
- * @param format The new upstream format.
- */
- void onUpstreamFormatChanged(Format format);
-
- }
-
- private static final int INITIAL_SCRATCH_SIZE = 32;
-
- private static final int STATE_ENABLED = 0;
- private static final int STATE_ENABLED_WRITING = 1;
- private static final int STATE_DISABLED = 2;
-
- private final Allocator allocator;
- private final int allocationLength;
-
- private final InfoQueue infoQueue;
- private final LinkedBlockingDeque dataQueue;
- private final BufferExtrasHolder extrasHolder;
- private final ParsableByteArray scratch;
- private final AtomicInteger state;
-
- // Accessed only by the consuming thread.
- private long totalBytesDropped;
- private Format downstreamFormat;
-
- // Accessed only by the loading thread (or the consuming thread when there is no loading thread).
- private boolean pendingFormatAdjustment;
- private Format lastUnadjustedFormat;
- private long sampleOffsetUs;
- private long totalBytesWritten;
- private Allocation lastAllocation;
- private int lastAllocationOffset;
- private boolean pendingSplice;
- private UpstreamFormatChangedListener upstreamFormatChangeListener;
-
- /**
- * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
- */
- public DefaultTrackOutput(Allocator allocator) {
- this.allocator = allocator;
- allocationLength = allocator.getIndividualAllocationLength();
- infoQueue = new InfoQueue();
- dataQueue = new LinkedBlockingDeque<>();
- extrasHolder = new BufferExtrasHolder();
- scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
- state = new AtomicInteger();
- lastAllocationOffset = allocationLength;
- }
-
- // Called by the consuming thread, but only when there is no loading thread.
-
- /**
- * Resets the output.
- *
- * @param enable Whether the output should be enabled. False if it should be disabled.
- */
- public void reset(boolean enable) {
- int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED);
- clearSampleData();
- infoQueue.resetLargestParsedTimestamps();
- if (previousState == STATE_DISABLED) {
- downstreamFormat = null;
- }
- }
-
- /**
- * Sets a source identifier for subsequent samples.
- *
- * @param sourceId The source identifier.
- */
- public void sourceId(int sourceId) {
- infoQueue.sourceId(sourceId);
- }
-
- /**
- * Indicates that samples subsequently queued to the buffer should be spliced into those already
- * queued.
- */
- public void splice() {
- pendingSplice = true;
- }
-
- /**
- * Returns the current absolute write index.
- */
- public int getWriteIndex() {
- return infoQueue.getWriteIndex();
- }
-
- /**
- * Discards samples from the write side of the buffer.
- *
- * @param discardFromIndex The absolute index of the first sample to be discarded.
- */
- public void discardUpstreamSamples(int discardFromIndex) {
- totalBytesWritten = infoQueue.discardUpstreamSamples(discardFromIndex);
- dropUpstreamFrom(totalBytesWritten);
- }
-
- /**
- * Discards data from the write side of the buffer. Data is discarded from the specified absolute
- * position. Any allocations that are fully discarded are returned to the allocator.
- *
- * @param absolutePosition The absolute position (inclusive) from which to discard data.
- */
- private void dropUpstreamFrom(long absolutePosition) {
- int relativePosition = (int) (absolutePosition - totalBytesDropped);
- // Calculate the index of the allocation containing the position, and the offset within it.
- int allocationIndex = relativePosition / allocationLength;
- int allocationOffset = relativePosition % allocationLength;
- // We want to discard any allocations after the one at allocationIdnex.
- int allocationDiscardCount = dataQueue.size() - allocationIndex - 1;
- if (allocationOffset == 0) {
- // If the allocation at allocationIndex is empty, we should discard that one too.
- allocationDiscardCount++;
- }
- // Discard the allocations.
- for (int i = 0; i < allocationDiscardCount; i++) {
- allocator.release(dataQueue.removeLast());
- }
- // Update lastAllocation and lastAllocationOffset to reflect the new position.
- lastAllocation = dataQueue.peekLast();
- lastAllocationOffset = allocationOffset == 0 ? allocationLength : allocationOffset;
- }
-
- // Called by the consuming thread.
-
- /**
- * Disables buffering of sample data and metadata.
- */
- public void disable() {
- if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) {
- clearSampleData();
- }
- }
-
- /**
- * Returns whether the buffer is empty.
- */
- public boolean isEmpty() {
- return infoQueue.isEmpty();
- }
-
- /**
- * Returns the current absolute read index.
- */
- public int getReadIndex() {
- return infoQueue.getReadIndex();
- }
-
- /**
- * Peeks the source id of the next sample, or the current upstream source id if the buffer is
- * empty.
- *
- * @return The source id.
- */
- public int peekSourceId() {
- return infoQueue.peekSourceId();
- }
-
- /**
- * Returns the upstream {@link Format} in which samples are being queued.
- */
- public Format getUpstreamFormat() {
- return infoQueue.getUpstreamFormat();
- }
-
- /**
- * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
- *
- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
- * considered as having been queued. Samples that were dequeued from the front of the queue are
- * considered as having been queued.
- *
- * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
- * samples have been queued.
- */
- public long getLargestQueuedTimestampUs() {
- return infoQueue.getLargestQueuedTimestampUs();
- }
-
- /**
- * Skips all samples currently in the buffer.
- */
- public void skipAll() {
- long nextOffset = infoQueue.skipAll();
- if (nextOffset != C.POSITION_UNSET) {
- dropDownstreamTo(nextOffset);
- }
- }
-
- /**
- * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
- * contains a keyframe with a timestamp of {@code timeUs} or earlier. If
- * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
- * falls within the buffer.
- *
- * @param timeUs The seek time.
- * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
- * of the buffer.
- * @return Whether the skip was successful.
- */
- public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
- long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
- if (nextOffset == C.POSITION_UNSET) {
- return false;
- }
- dropDownstreamTo(nextOffset);
- return true;
- }
-
- /**
- * Attempts to read from the queue.
- *
- * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
- * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
- * end of the stream. If the end of the stream has been reached, the
- * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
- * @param formatRequired Whether the caller requires that the format of the stream be read even if
- * it's not changing. A sample will never be read if set to true, however it is still possible
- * for the end of stream or nothing to be read.
- * @param loadingFinished True if an empty queue should be considered the end of the stream.
- * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
- * be set if the buffer's timestamp is less than this value.
- * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
- * {@link C#RESULT_BUFFER_READ}.
- */
- public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
- boolean loadingFinished, long decodeOnlyUntilUs) {
- int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
- downstreamFormat, extrasHolder);
- switch (result) {
- case C.RESULT_FORMAT_READ:
- downstreamFormat = formatHolder.format;
- return C.RESULT_FORMAT_READ;
- case C.RESULT_BUFFER_READ:
- if (!buffer.isEndOfStream()) {
- if (buffer.timeUs < decodeOnlyUntilUs) {
- buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
- }
- // Read encryption data if the sample is encrypted.
- if (buffer.isEncrypted()) {
- readEncryptionData(buffer, extrasHolder);
- }
- // Write the sample data into the holder.
- buffer.ensureSpaceForWrite(extrasHolder.size);
- readData(extrasHolder.offset, buffer.data, extrasHolder.size);
- // Advance the read head.
- dropDownstreamTo(extrasHolder.nextOffset);
- }
- return C.RESULT_BUFFER_READ;
- case C.RESULT_NOTHING_READ:
- return C.RESULT_NOTHING_READ;
- default:
- throw new IllegalStateException();
- }
- }
-
- /**
- * Reads encryption data for the current sample.
- *
- * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and
- * {@link BufferExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The
- * same value is added to {@link BufferExtrasHolder#offset}.
- *
- * @param buffer The buffer into which the encryption data should be written.
- * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
- */
- private void readEncryptionData(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) {
- long offset = extrasHolder.offset;
-
- // Read the signal byte.
- scratch.reset(1);
- readData(offset, scratch.data, 1);
- offset++;
- byte signalByte = scratch.data[0];
- boolean subsampleEncryption = (signalByte & 0x80) != 0;
- int ivSize = signalByte & 0x7F;
-
- // Read the initialization vector.
- if (buffer.cryptoInfo.iv == null) {
- buffer.cryptoInfo.iv = new byte[16];
- }
- readData(offset, buffer.cryptoInfo.iv, ivSize);
- offset += ivSize;
-
- // Read the subsample count, if present.
- int subsampleCount;
- if (subsampleEncryption) {
- scratch.reset(2);
- readData(offset, scratch.data, 2);
- offset += 2;
- subsampleCount = scratch.readUnsignedShort();
- } else {
- subsampleCount = 1;
- }
-
- // Write the clear and encrypted subsample sizes.
- int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData;
- if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
- clearDataSizes = new int[subsampleCount];
- }
- int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData;
- if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
- encryptedDataSizes = new int[subsampleCount];
- }
- if (subsampleEncryption) {
- int subsampleDataLength = 6 * subsampleCount;
- scratch.reset(subsampleDataLength);
- readData(offset, scratch.data, subsampleDataLength);
- offset += subsampleDataLength;
- scratch.setPosition(0);
- for (int i = 0; i < subsampleCount; i++) {
- clearDataSizes[i] = scratch.readUnsignedShort();
- encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
- }
- } else {
- clearDataSizes[0] = 0;
- encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
- }
-
- // Populate the cryptoInfo.
- buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,
- extrasHolder.encryptionKeyId, buffer.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR);
-
- // Adjust the offset and size to take into account the bytes read.
- int bytesRead = (int) (offset - extrasHolder.offset);
- extrasHolder.offset += bytesRead;
- extrasHolder.size -= bytesRead;
- }
-
- /**
- * Reads data from the front of the rolling buffer.
- *
- * @param absolutePosition The absolute position from which data should be read.
- * @param target The buffer into which data should be written.
- * @param length The number of bytes to read.
- */
- private void readData(long absolutePosition, ByteBuffer target, int length) {
- int remaining = length;
- while (remaining > 0) {
- dropDownstreamTo(absolutePosition);
- int positionInAllocation = (int) (absolutePosition - totalBytesDropped);
- int toCopy = Math.min(remaining, allocationLength - positionInAllocation);
- Allocation allocation = dataQueue.peek();
- target.put(allocation.data, allocation.translateOffset(positionInAllocation), toCopy);
- absolutePosition += toCopy;
- remaining -= toCopy;
- }
- }
-
- /**
- * Reads data from the front of the rolling buffer.
- *
- * @param absolutePosition The absolute position from which data should be read.
- * @param target The array into which data should be written.
- * @param length The number of bytes to read.
- */
- private void readData(long absolutePosition, byte[] target, int length) {
- int bytesRead = 0;
- while (bytesRead < length) {
- dropDownstreamTo(absolutePosition);
- int positionInAllocation = (int) (absolutePosition - totalBytesDropped);
- int toCopy = Math.min(length - bytesRead, allocationLength - positionInAllocation);
- Allocation allocation = dataQueue.peek();
- System.arraycopy(allocation.data, allocation.translateOffset(positionInAllocation), target,
- bytesRead, toCopy);
- absolutePosition += toCopy;
- bytesRead += toCopy;
- }
- }
-
- /**
- * Discard any allocations that hold data prior to the specified absolute position, returning
- * them to the allocator.
- *
- * @param absolutePosition The absolute position up to which allocations can be discarded.
- */
- private void dropDownstreamTo(long absolutePosition) {
- int relativePosition = (int) (absolutePosition - totalBytesDropped);
- int allocationIndex = relativePosition / allocationLength;
- for (int i = 0; i < allocationIndex; i++) {
- allocator.release(dataQueue.remove());
- totalBytesDropped += allocationLength;
- }
- }
-
- // Called by the loading thread.
-
- /**
- * Sets a listener to be notified of changes to the upstream format.
- *
- * @param listener The listener.
- */
- public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) {
- upstreamFormatChangeListener = listener;
- }
-
- /**
- * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
- * subsequently queued to the buffer.
- *
- * @param sampleOffsetUs The timestamp offset in microseconds.
- */
- public void setSampleOffsetUs(long sampleOffsetUs) {
- if (this.sampleOffsetUs != sampleOffsetUs) {
- this.sampleOffsetUs = sampleOffsetUs;
- pendingFormatAdjustment = true;
- }
- }
-
- @Override
- public void format(Format format) {
- Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
- boolean formatChanged = infoQueue.format(adjustedFormat);
- lastUnadjustedFormat = format;
- pendingFormatAdjustment = false;
- if (upstreamFormatChangeListener != null && formatChanged) {
- upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
- }
- }
-
- @Override
- public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
- throws IOException, InterruptedException {
- if (!startWriteOperation()) {
- int bytesSkipped = input.skip(length);
- if (bytesSkipped == C.RESULT_END_OF_INPUT) {
- if (allowEndOfInput) {
- return C.RESULT_END_OF_INPUT;
- }
- throw new EOFException();
- }
- return bytesSkipped;
- }
- try {
- length = prepareForAppend(length);
- int bytesAppended = input.read(lastAllocation.data,
- lastAllocation.translateOffset(lastAllocationOffset), length);
- if (bytesAppended == C.RESULT_END_OF_INPUT) {
- if (allowEndOfInput) {
- return C.RESULT_END_OF_INPUT;
- }
- throw new EOFException();
- }
- lastAllocationOffset += bytesAppended;
- totalBytesWritten += bytesAppended;
- return bytesAppended;
- } finally {
- endWriteOperation();
- }
- }
-
- @Override
- public void sampleData(ParsableByteArray buffer, int length) {
- if (!startWriteOperation()) {
- buffer.skipBytes(length);
- return;
- }
- while (length > 0) {
- int thisAppendLength = prepareForAppend(length);
- buffer.readBytes(lastAllocation.data, lastAllocation.translateOffset(lastAllocationOffset),
- thisAppendLength);
- lastAllocationOffset += thisAppendLength;
- totalBytesWritten += thisAppendLength;
- length -= thisAppendLength;
- }
- endWriteOperation();
- }
-
- @Override
- public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
- byte[] encryptionKey) {
- if (pendingFormatAdjustment) {
- format(lastUnadjustedFormat);
- }
- if (!startWriteOperation()) {
- infoQueue.commitSampleTimestamp(timeUs);
- return;
- }
- try {
- if (pendingSplice) {
- if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) {
- return;
- }
- pendingSplice = false;
- }
- timeUs += sampleOffsetUs;
- long absoluteOffset = totalBytesWritten - size - offset;
- infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey);
- } finally {
- endWriteOperation();
- }
- }
-
- // Private methods.
-
- private boolean startWriteOperation() {
- return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING);
- }
-
- private void endWriteOperation() {
- if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) {
- clearSampleData();
- }
- }
-
- private void clearSampleData() {
- infoQueue.clearSampleData();
- allocator.release(dataQueue.toArray(new Allocation[dataQueue.size()]));
- dataQueue.clear();
- allocator.trim();
- totalBytesDropped = 0;
- totalBytesWritten = 0;
- lastAllocation = null;
- lastAllocationOffset = allocationLength;
- }
-
- /**
- * Prepares the rolling sample buffer for an append of up to {@code length} bytes, returning the
- * number of bytes that can actually be appended.
- */
- private int prepareForAppend(int length) {
- if (lastAllocationOffset == allocationLength) {
- lastAllocationOffset = 0;
- lastAllocation = allocator.allocate();
- dataQueue.add(lastAllocation);
- }
- return Math.min(length, allocationLength - lastAllocationOffset);
- }
-
- /**
- * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}.
- *
- * @param format The {@link Format} to adjust.
- * @param sampleOffsetUs The offset to apply.
- * @return The adjusted {@link Format}.
- */
- private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) {
- if (format == null) {
- return null;
- }
- if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
- format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
- }
- return format;
- }
-
- /**
- * Holds information about the samples in the rolling buffer.
- */
- private static final class InfoQueue {
-
- private static final int SAMPLE_CAPACITY_INCREMENT = 1000;
-
- private int capacity;
-
- private int[] sourceIds;
- private long[] offsets;
- private int[] sizes;
- private int[] flags;
- private long[] timesUs;
- private byte[][] encryptionKeys;
- private Format[] formats;
-
- private int queueSize;
- private int absoluteReadIndex;
- private int relativeReadIndex;
- private int relativeWriteIndex;
-
- private long largestDequeuedTimestampUs;
- private long largestQueuedTimestampUs;
- private boolean upstreamKeyframeRequired;
- private boolean upstreamFormatRequired;
- private Format upstreamFormat;
- private int upstreamSourceId;
-
- public InfoQueue() {
- capacity = SAMPLE_CAPACITY_INCREMENT;
- sourceIds = new int[capacity];
- offsets = new long[capacity];
- timesUs = new long[capacity];
- flags = new int[capacity];
- sizes = new int[capacity];
- encryptionKeys = new byte[capacity][];
- formats = new Format[capacity];
- largestDequeuedTimestampUs = Long.MIN_VALUE;
- largestQueuedTimestampUs = Long.MIN_VALUE;
- upstreamFormatRequired = true;
- upstreamKeyframeRequired = true;
- }
-
- public void clearSampleData() {
- absoluteReadIndex = 0;
- relativeReadIndex = 0;
- relativeWriteIndex = 0;
- queueSize = 0;
- upstreamKeyframeRequired = true;
- }
-
- // Called by the consuming thread, but only when there is no loading thread.
-
- public void resetLargestParsedTimestamps() {
- largestDequeuedTimestampUs = Long.MIN_VALUE;
- largestQueuedTimestampUs = Long.MIN_VALUE;
- }
-
- /**
- * Returns the current absolute write index.
- */
- public int getWriteIndex() {
- return absoluteReadIndex + queueSize;
- }
-
- /**
- * Discards samples from the write side of the buffer.
- *
- * @param discardFromIndex The absolute index of the first sample to be discarded.
- * @return The reduced total number of bytes written, after the samples have been discarded.
- */
- public long discardUpstreamSamples(int discardFromIndex) {
- int discardCount = getWriteIndex() - discardFromIndex;
- Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize);
-
- if (discardCount == 0) {
- if (absoluteReadIndex == 0) {
- // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue.
- return 0;
- }
- int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1;
- return offsets[lastWriteIndex] + sizes[lastWriteIndex];
- }
-
- queueSize -= discardCount;
- relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity;
- // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are
- // always less than the timestamp of the keyframe itself, and of subsequent frames.
- largestQueuedTimestampUs = Long.MIN_VALUE;
- for (int i = queueSize - 1; i >= 0; i--) {
- int sampleIndex = (relativeReadIndex + i) % capacity;
- largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]);
- if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
- break;
- }
- }
- return offsets[relativeWriteIndex];
- }
-
- public void sourceId(int sourceId) {
- upstreamSourceId = sourceId;
- }
-
- // Called by the consuming thread.
-
- /**
- * Returns the current absolute read index.
- */
- public int getReadIndex() {
- return absoluteReadIndex;
- }
-
- /**
- * Peeks the source id of the next sample, or the current upstream source id if the queue is
- * empty.
- */
- public int peekSourceId() {
- return queueSize == 0 ? upstreamSourceId : sourceIds[relativeReadIndex];
- }
-
- /**
- * Returns whether the queue is empty.
- */
- public synchronized boolean isEmpty() {
- return queueSize == 0;
- }
-
- /**
- * Returns the upstream {@link Format} in which samples are being queued.
- */
- public synchronized Format getUpstreamFormat() {
- return upstreamFormatRequired ? null : upstreamFormat;
- }
-
- /**
- * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
- *
- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
- * considered as having been queued. Samples that were dequeued from the front of the queue are
- * considered as having been queued.
- *
- * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
- * samples have been queued.
- */
- public synchronized long getLargestQueuedTimestampUs() {
- return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs);
- }
-
- /**
- * Attempts to read from the queue.
- *
- * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
- * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
- * end of the stream. If a sample is read then the buffer is populated with information
- * about the sample, but not its data. The size and absolute position of the data in the
- * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
- * and the absolute position of the first byte that may still be required after the current
- * sample has been read. May be null if the caller requires that the format of the stream be
- * read even if it's not changing.
- * @param formatRequired Whether the caller requires that the format of the stream be read even
- * if it's not changing. A sample will never be read if set to true, however it is still
- * possible for the end of stream or nothing to be read.
- * @param loadingFinished True if an empty queue should be considered the end of the stream.
- * @param downstreamFormat The current downstream {@link Format}. If the format of the next
- * sample is different to the current downstream format then a format will be read.
- * @param extrasHolder The holder into which extra sample information should be written.
- * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
- * or {@link C#RESULT_BUFFER_READ}.
- */
- @SuppressWarnings("ReferenceEquality")
- public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
- boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
- BufferExtrasHolder extrasHolder) {
- if (queueSize == 0) {
- if (loadingFinished) {
- buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
- return C.RESULT_BUFFER_READ;
- } else if (upstreamFormat != null
- && (formatRequired || upstreamFormat != downstreamFormat)) {
- formatHolder.format = upstreamFormat;
- return C.RESULT_FORMAT_READ;
- } else {
- return C.RESULT_NOTHING_READ;
- }
- }
-
- if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {
- formatHolder.format = formats[relativeReadIndex];
- return C.RESULT_FORMAT_READ;
- }
-
- if (buffer.isFlagsOnly()) {
- return C.RESULT_NOTHING_READ;
- }
-
- buffer.timeUs = timesUs[relativeReadIndex];
- buffer.setFlags(flags[relativeReadIndex]);
- extrasHolder.size = sizes[relativeReadIndex];
- extrasHolder.offset = offsets[relativeReadIndex];
- extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex];
-
- largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs);
- queueSize--;
- relativeReadIndex++;
- absoluteReadIndex++;
- if (relativeReadIndex == capacity) {
- // Wrap around.
- relativeReadIndex = 0;
- }
-
- extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex]
- : extrasHolder.offset + extrasHolder.size;
- return C.RESULT_BUFFER_READ;
- }
-
- /**
- * Skips all samples in the buffer.
- *
- * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no
- * dropping of data is required.
- */
- public synchronized long skipAll() {
- if (queueSize == 0) {
- return C.POSITION_UNSET;
- }
-
- int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity;
- relativeReadIndex = (relativeReadIndex + queueSize) % capacity;
- absoluteReadIndex += queueSize;
- queueSize = 0;
- return offsets[lastSampleIndex] + sizes[lastSampleIndex];
- }
-
- /**
- * Attempts to locate the keyframe before or at the specified time. If
- * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
- * falls within the buffer.
- *
- * @param timeUs The seek time.
- * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
- * of the buffer.
- * @return The offset of the keyframe's data if the keyframe was present.
- * {@link C#POSITION_UNSET} otherwise.
- */
- public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
- if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
- return C.POSITION_UNSET;
- }
-
- if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
- return C.POSITION_UNSET;
- }
-
- // This could be optimized to use a binary search, however in practice callers to this method
- // often pass times near to the start of the buffer. Hence it's unclear whether switching to
- // a binary search would yield any real benefit.
- int sampleCount = 0;
- int sampleCountToKeyframe = -1;
- int searchIndex = relativeReadIndex;
- while (searchIndex != relativeWriteIndex) {
- if (timesUs[searchIndex] > timeUs) {
- // We've gone too far.
- break;
- } else if ((flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
- // We've found a keyframe, and we're still before the seek position.
- sampleCountToKeyframe = sampleCount;
- }
- searchIndex = (searchIndex + 1) % capacity;
- sampleCount++;
- }
-
- if (sampleCountToKeyframe == -1) {
- return C.POSITION_UNSET;
- }
-
- relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity;
- absoluteReadIndex += sampleCountToKeyframe;
- queueSize -= sampleCountToKeyframe;
- return offsets[relativeReadIndex];
- }
-
- // Called by the loading thread.
-
- public synchronized boolean format(Format format) {
- if (format == null) {
- upstreamFormatRequired = true;
- return false;
- }
- upstreamFormatRequired = false;
- if (Util.areEqual(format, upstreamFormat)) {
- // Suppress changes between equal formats so we can use referential equality in readData.
- return false;
- } else {
- upstreamFormat = format;
- return true;
- }
- }
-
- public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset,
- int size, byte[] encryptionKey) {
- if (upstreamKeyframeRequired) {
- if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
- return;
- }
- upstreamKeyframeRequired = false;
- }
- Assertions.checkState(!upstreamFormatRequired);
- commitSampleTimestamp(timeUs);
- timesUs[relativeWriteIndex] = timeUs;
- offsets[relativeWriteIndex] = offset;
- sizes[relativeWriteIndex] = size;
- flags[relativeWriteIndex] = sampleFlags;
- encryptionKeys[relativeWriteIndex] = encryptionKey;
- formats[relativeWriteIndex] = upstreamFormat;
- sourceIds[relativeWriteIndex] = upstreamSourceId;
- // Increment the write index.
- queueSize++;
- if (queueSize == capacity) {
- // Increase the capacity.
- int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
- int[] newSourceIds = new int[newCapacity];
- long[] newOffsets = new long[newCapacity];
- long[] newTimesUs = new long[newCapacity];
- int[] newFlags = new int[newCapacity];
- int[] newSizes = new int[newCapacity];
- byte[][] newEncryptionKeys = new byte[newCapacity][];
- Format[] newFormats = new Format[newCapacity];
- int beforeWrap = capacity - relativeReadIndex;
- System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap);
- System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap);
- System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap);
- System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap);
- System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap);
- System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap);
- System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap);
- int afterWrap = relativeReadIndex;
- System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
- System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
- System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
- System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
- System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap);
- System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap);
- System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
- offsets = newOffsets;
- timesUs = newTimesUs;
- flags = newFlags;
- sizes = newSizes;
- encryptionKeys = newEncryptionKeys;
- formats = newFormats;
- sourceIds = newSourceIds;
- relativeReadIndex = 0;
- relativeWriteIndex = capacity;
- queueSize = capacity;
- capacity = newCapacity;
- } else {
- relativeWriteIndex++;
- if (relativeWriteIndex == capacity) {
- // Wrap around.
- relativeWriteIndex = 0;
- }
- }
- }
-
- public synchronized void commitSampleTimestamp(long timeUs) {
- largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
- }
-
- /**
- * Attempts to discard samples from the tail of the queue to allow samples starting from the
- * specified timestamp to be spliced in.
- *
- * @param timeUs The timestamp at which the splice occurs.
- * @return Whether the splice was successful.
- */
- public synchronized boolean attemptSplice(long timeUs) {
- if (largestDequeuedTimestampUs >= timeUs) {
- return false;
- }
- int retainCount = queueSize;
- while (retainCount > 0
- && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) {
- retainCount--;
- }
- discardUpstreamSamples(absoluteReadIndex + retainCount);
- return true;
- }
-
- }
-
- /**
- * Holds additional buffer information not held by {@link DecoderInputBuffer}.
- */
- private static final class BufferExtrasHolder {
-
- public int size;
- public long offset;
- public long nextOffset;
- public byte[] encryptionKeyId;
-
- }
-
-}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
index 61f97887be..c023b0de95 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
@@ -51,7 +51,7 @@ public final class DummyTrackOutput implements TrackOutput {
@Override
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
- byte[] encryptionKey) {
+ CryptoData cryptoData) {
// Do nothing.
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java
index de3dfd5266..7a2bc15da9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java
@@ -63,7 +63,8 @@ public interface Extractor {
void init(ExtractorOutput output);
/**
- * Extracts data read from a provided {@link ExtractorInput}.
+ * Extracts data read from a provided {@link ExtractorInput}. Must not be called before
+ * {@link #init(ExtractorOutput)}.
*
* A single call to this method will block until some progress has been made, but will not block
* for longer than this. Hence each call will consume only a small amount of input data.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
index c4dee4b6a7..a12a0315a4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
@@ -20,12 +20,78 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException;
import java.io.IOException;
+import java.util.Arrays;
/**
* Receives track level data extracted by an {@link Extractor}.
*/
public interface TrackOutput {
+ /**
+ * Holds data required to decrypt a sample.
+ */
+ final class CryptoData {
+
+ /**
+ * The encryption mode used for the sample.
+ */
+ @C.CryptoMode public final int cryptoMode;
+
+ /**
+ * The encryption key associated with the sample. Its contents must not be modified.
+ */
+ public final byte[] encryptionKey;
+
+ /**
+ * The number of encrypted blocks in the encryption pattern, 0 if pattern encryption does not
+ * apply.
+ */
+ public final int encryptedBlocks;
+
+ /**
+ * The number of clear blocks in the encryption pattern, 0 if pattern encryption does not
+ * apply.
+ */
+ public final int clearBlocks;
+
+ /**
+ * @param cryptoMode See {@link #cryptoMode}.
+ * @param encryptionKey See {@link #encryptionKey}.
+ * @param encryptedBlocks See {@link #encryptedBlocks}.
+ * @param clearBlocks See {@link #clearBlocks}.
+ */
+ public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey, int encryptedBlocks,
+ int clearBlocks) {
+ this.cryptoMode = cryptoMode;
+ this.encryptionKey = encryptionKey;
+ this.encryptedBlocks = encryptedBlocks;
+ this.clearBlocks = clearBlocks;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ CryptoData other = (CryptoData) obj;
+ return cryptoMode == other.cryptoMode && encryptedBlocks == other.encryptedBlocks
+ && clearBlocks == other.clearBlocks && Arrays.equals(encryptionKey, other.encryptionKey);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = cryptoMode;
+ result = 31 * result + Arrays.hashCode(encryptionKey);
+ result = 31 * result + encryptedBlocks;
+ result = 31 * result + clearBlocks;
+ return result;
+ }
+
+ }
+
/**
* Called when the {@link Format} of the track has been extracted from the stream.
*
@@ -70,9 +136,9 @@ public interface TrackOutput {
* {@link #sampleData(ExtractorInput, int, boolean)} or
* {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample
* whose metadata is being passed.
- * @param encryptionKey The encryption key associated with the sample. May be null.
+ * @param encryptionData The encryption data required to decrypt the sample. May be null.
*/
void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
- byte[] encryptionKey);
+ CryptoData encryptionData);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
index 2313392fcf..6c4eb033ce 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -120,6 +120,7 @@ public final class MatroskaExtractor implements Extractor {
private static final String CODEC_ID_ACM = "A_MS/ACM";
private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT";
private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8";
+ private static final String CODEC_ID_ASS = "S_TEXT/ASS";
private static final String CODEC_ID_VOBSUB = "S_VOBSUB";
private static final String CODEC_ID_PGS = "S_HDMV/PGS";
private static final String CODEC_ID_DVBSUB = "S_DVBSUB";
@@ -226,21 +227,62 @@ public final class MatroskaExtractor implements Extractor {
private static final byte[] SUBRIP_PREFIX = new byte[] {49, 10, 48, 48, 58, 48, 48, 58, 48, 48,
44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 10};
/**
- * A special end timecode indicating that a subtitle should be displayed until the next subtitle,
- * or until the end of the media in the case of the last subtitle.
+ * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}.
+ */
+ private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;
+ /**
+ * A special end timecode indicating that a subrip subtitle should be displayed until the next
+ * subtitle, or until the end of the media in the case of the last subtitle.
*
* Equivalent to the UTF-8 string: " ".
*/
private static final byte[] SUBRIP_TIMECODE_EMPTY =
new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32};
/**
- * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}.
+ * The value by which to divide a time in microseconds to convert it to the unit of the last value
+ * in a subrip timecode (milliseconds).
*/
- private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;
+ private static long SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000;
/**
- * The length in bytes of a timecode in a subrip prefix.
+ * The format of a subrip timecode.
*/
- private static final int SUBRIP_TIMECODE_LENGTH = 12;
+ private static final String SUBRIP_TIMECODE_FORMAT = "%02d:%02d:%02d,%03d";
+
+ /**
+ * Matroska specific format line for SSA subtitles.
+ */
+ private static final byte[] SSA_DIALOGUE_FORMAT = Util.getUtf8Bytes("Format: Start, End, "
+ + "ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
+ /**
+ * A template for the prefix that must be added to each SSA sample. The 10 byte end timecode
+ * starting at {@link #SSA_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be
+ * replaced with the duration of the subtitle.
+ *
+ * Equivalent to the UTF-8 string: "Dialogue: 0:00:00:00,0:00:00:00,".
+ */
+ private static final byte[] SSA_PREFIX = new byte[] {68, 105, 97, 108, 111, 103, 117, 101, 58, 32,
+ 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44};
+ /**
+ * The byte offset of the end timecode in {@link #SSA_PREFIX}.
+ */
+ private static final int SSA_PREFIX_END_TIMECODE_OFFSET = 21;
+ /**
+ * The value by which to divide a time in microseconds to convert it to the unit of the last value
+ * in an SSA timecode (1/100ths of a second).
+ */
+ private static long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000;
+ /**
+ * A special end timecode indicating that an SSA subtitle should be displayed until the next
+ * subtitle, or until the end of the media in the case of the last subtitle.
+ *
+ * Equivalent to the UTF-8 string: " ".
+ */
+ private static final byte[] SSA_TIMECODE_EMPTY =
+ new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32};
+ /**
+ * The format of an SSA timecode.
+ */
+ private static final String SSA_TIMECODE_FORMAT = "%01d:%02d:%02d:%02d";
/**
* The length in bytes of a WAVEFORMATEX structure.
@@ -271,7 +313,7 @@ public final class MatroskaExtractor implements Extractor {
private final ParsableByteArray vorbisNumPageSamples;
private final ParsableByteArray seekEntryIdBytes;
private final ParsableByteArray sampleStrippedBytes;
- private final ParsableByteArray subripSample;
+ private final ParsableByteArray subtitleSample;
private final ParsableByteArray encryptionInitializationVector;
private final ParsableByteArray encryptionSubsampleData;
private ByteBuffer encryptionSubsampleDataBuffer;
@@ -349,7 +391,7 @@ public final class MatroskaExtractor implements Extractor {
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
sampleStrippedBytes = new ParsableByteArray();
- subripSample = new ParsableByteArray();
+ subtitleSample = new ParsableByteArray();
encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE);
encryptionSubsampleData = new ParsableByteArray();
}
@@ -583,11 +625,11 @@ public final class MatroskaExtractor implements Extractor {
break;
case ID_CONTENT_ENCODING:
if (currentTrack.hasContentEncryption) {
- if (currentTrack.encryptionKeyId == null) {
+ if (currentTrack.cryptoData == null) {
throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
}
- currentTrack.drmInitData = new DrmInitData(
- new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId));
+ currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, null,
+ MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey));
}
break;
case ID_CONTENT_ENCODINGS:
@@ -891,8 +933,10 @@ public final class MatroskaExtractor implements Extractor {
input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize);
break;
case ID_CONTENT_ENCRYPTION_KEY_ID:
- currentTrack.encryptionKeyId = new byte[contentSize];
- input.readFully(currentTrack.encryptionKeyId, 0, contentSize);
+ byte[] encryptionKey = new byte[contentSize];
+ input.readFully(encryptionKey, 0, contentSize);
+ currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey,
+ 0, 0); // We assume patternless AES-CTR.
break;
case ID_SIMPLE_BLOCK:
case ID_BLOCK:
@@ -1014,7 +1058,7 @@ public final class MatroskaExtractor implements Extractor {
// For SimpleBlock, we have metadata for each sample here.
while (blockLacingSampleIndex < blockLacingSampleCount) {
writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]);
- long sampleTimeUs = this.blockTimeUs
+ long sampleTimeUs = blockTimeUs
+ (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000;
commitSampleToOutput(track, sampleTimeUs);
blockLacingSampleIndex++;
@@ -1034,9 +1078,13 @@ public final class MatroskaExtractor implements Extractor {
private void commitSampleToOutput(Track track, long timeUs) {
if (CODEC_ID_SUBRIP.equals(track.codecId)) {
- writeSubripSample(track);
+ commitSubtitleSample(track, SUBRIP_TIMECODE_FORMAT, SUBRIP_PREFIX_END_TIMECODE_OFFSET,
+ SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, SUBRIP_TIMECODE_EMPTY);
+ } else if (CODEC_ID_ASS.equals(track.codecId)) {
+ commitSubtitleSample(track, SSA_TIMECODE_FORMAT, SSA_PREFIX_END_TIMECODE_OFFSET,
+ SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, SSA_TIMECODE_EMPTY);
}
- track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.encryptionKeyId);
+ track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData);
sampleRead = true;
resetSample();
}
@@ -1074,17 +1122,10 @@ public final class MatroskaExtractor implements Extractor {
private void writeSampleData(ExtractorInput input, Track track, int size)
throws IOException, InterruptedException {
if (CODEC_ID_SUBRIP.equals(track.codecId)) {
- int sizeWithPrefix = SUBRIP_PREFIX.length + size;
- if (subripSample.capacity() < sizeWithPrefix) {
- // Initialize subripSample to contain the required prefix and have space to hold a subtitle
- // twice as long as this one.
- subripSample.data = Arrays.copyOf(SUBRIP_PREFIX, sizeWithPrefix + size);
- }
- input.readFully(subripSample.data, SUBRIP_PREFIX.length, size);
- subripSample.setPosition(0);
- subripSample.setLimit(sizeWithPrefix);
- // Defer writing the data to the track output. We need to modify the sample data by setting
- // the correct end timecode, which we might not have yet.
+ writeSubtitleSampleData(input, SUBRIP_PREFIX, size);
+ return;
+ } else if (CODEC_ID_ASS.equals(track.codecId)) {
+ writeSubtitleSampleData(input, SSA_PREFIX, size);
return;
}
@@ -1228,31 +1269,50 @@ public final class MatroskaExtractor implements Extractor {
}
}
- private void writeSubripSample(Track track) {
- setSubripSampleEndTimecode(subripSample.data, blockDurationUs);
- // Note: If we ever want to support DRM protected subtitles then we'll need to output the
- // appropriate encryption data here.
- track.output.sampleData(subripSample, subripSample.limit());
- sampleBytesWritten += subripSample.limit();
+ private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size)
+ throws IOException, InterruptedException {
+ int sizeWithPrefix = samplePrefix.length + size;
+ if (subtitleSample.capacity() < sizeWithPrefix) {
+ // Initialize subripSample to contain the required prefix and have space to hold a subtitle
+ // twice as long as this one.
+ subtitleSample.data = Arrays.copyOf(samplePrefix, sizeWithPrefix + size);
+ } else {
+ System.arraycopy(samplePrefix, 0, subtitleSample.data, 0, samplePrefix.length);
+ }
+ input.readFully(subtitleSample.data, samplePrefix.length, size);
+ subtitleSample.reset(sizeWithPrefix);
+ // Defer writing the data to the track output. We need to modify the sample data by setting
+ // the correct end timecode, which we might not have yet.
}
- private static void setSubripSampleEndTimecode(byte[] subripSampleData, long timeUs) {
+ private void commitSubtitleSample(Track track, String timecodeFormat, int endTimecodeOffset,
+ long lastTimecodeValueScalingFactor, byte[] emptyTimecode) {
+ setSampleDuration(subtitleSample.data, blockDurationUs, timecodeFormat, endTimecodeOffset,
+ lastTimecodeValueScalingFactor, emptyTimecode);
+ // Note: If we ever want to support DRM protected subtitles then we'll need to output the
+ // appropriate encryption data here.
+ track.output.sampleData(subtitleSample, subtitleSample.limit());
+ sampleBytesWritten += subtitleSample.limit();
+ }
+
+ private static void setSampleDuration(byte[] subripSampleData, long durationUs,
+ String timecodeFormat, int endTimecodeOffset, long lastTimecodeValueScalingFactor,
+ byte[] emptyTimecode) {
byte[] timeCodeData;
- if (timeUs == C.TIME_UNSET) {
- timeCodeData = SUBRIP_TIMECODE_EMPTY;
+ if (durationUs == C.TIME_UNSET) {
+ timeCodeData = emptyTimecode;
} else {
- int hours = (int) (timeUs / 3600000000L);
- timeUs -= (hours * 3600000000L);
- int minutes = (int) (timeUs / 60000000);
- timeUs -= (minutes * 60000000);
- int seconds = (int) (timeUs / 1000000);
- timeUs -= (seconds * 1000000);
- int milliseconds = (int) (timeUs / 1000);
- timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, "%02d:%02d:%02d,%03d", hours,
- minutes, seconds, milliseconds));
+ int hours = (int) (durationUs / (3600 * C.MICROS_PER_SECOND));
+ durationUs -= (hours * 3600 * C.MICROS_PER_SECOND);
+ int minutes = (int) (durationUs / (60 * C.MICROS_PER_SECOND));
+ durationUs -= (minutes * 60 * C.MICROS_PER_SECOND);
+ int seconds = (int) (durationUs / C.MICROS_PER_SECOND);
+ durationUs -= (seconds * C.MICROS_PER_SECOND);
+ int lastValue = (int) (durationUs / lastTimecodeValueScalingFactor);
+ timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes,
+ seconds, lastValue));
}
- System.arraycopy(timeCodeData, 0, subripSampleData, SUBRIP_PREFIX_END_TIMECODE_OFFSET,
- SUBRIP_TIMECODE_LENGTH);
+ System.arraycopy(timeCodeData, 0, subripSampleData, endTimecodeOffset, emptyTimecode.length);
}
/**
@@ -1383,6 +1443,7 @@ public final class MatroskaExtractor implements Extractor {
|| CODEC_ID_ACM.equals(codecId)
|| CODEC_ID_PCM_INT_LIT.equals(codecId)
|| CODEC_ID_SUBRIP.equals(codecId)
+ || CODEC_ID_ASS.equals(codecId)
|| CODEC_ID_VOBSUB.equals(codecId)
|| CODEC_ID_PGS.equals(codecId)
|| CODEC_ID_DVBSUB.equals(codecId);
@@ -1473,7 +1534,7 @@ public final class MatroskaExtractor implements Extractor {
public int defaultSampleDurationNs;
public boolean hasContentEncryption;
public byte[] sampleStrippedBytes;
- public byte[] encryptionKeyId;
+ public TrackOutput.CryptoData cryptoData;
public byte[] codecPrivate;
public DrmInitData drmInitData;
@@ -1648,6 +1709,9 @@ public final class MatroskaExtractor implements Extractor {
case CODEC_ID_SUBRIP:
mimeType = MimeTypes.APPLICATION_SUBRIP;
break;
+ case CODEC_ID_ASS:
+ mimeType = MimeTypes.TEXT_SSA;
+ break;
case CODEC_ID_VOBSUB:
mimeType = MimeTypes.APPLICATION_VOBSUB;
initializationData = Collections.singletonList(codecPrivate);
@@ -1698,8 +1762,16 @@ public final class MatroskaExtractor implements Extractor {
drmInitData);
} else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
type = C.TRACK_TYPE_TEXT;
+ format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, selectionFlags,
+ language, drmInitData);
+ } else if (MimeTypes.TEXT_SSA.equals(mimeType)) {
+ type = C.TRACK_TYPE_TEXT;
+ initializationData = new ArrayList<>(2);
+ initializationData.add(SSA_DIALOGUE_FORMAT);
+ initializationData.add(codecPrivate);
format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
- Format.NO_VALUE, selectionFlags, language, drmInitData);
+ Format.NO_VALUE, selectionFlags, language, Format.NO_VALUE, drmInitData,
+ Format.OFFSET_SAMPLE_RELATIVE, initializationData);
} else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
|| MimeTypes.APPLICATION_PGS.equals(mimeType)
|| MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
index 474ba65d86..f7e3e846e9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -615,10 +615,10 @@ import java.util.List;
|| childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp
|| childAtomType == Atom.TYPE_c608) {
parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
- language, drmInitData, out);
+ language, out);
} else if (childAtomType == Atom.TYPE_camm) {
out.format = Format.createSampleFormat(Integer.toString(trackId),
- MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData);
+ MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null);
}
stsd.setPosition(childStartPosition + childAtomSize);
}
@@ -626,8 +626,7 @@ import java.util.List;
}
private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position,
- int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out)
- throws ParserException {
+ int atomSize, int trackId, String language, StsdData out) throws ParserException {
parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
// Default values.
@@ -658,8 +657,7 @@ import java.util.List;
}
out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
- Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs,
- initializationData);
+ Format.NO_VALUE, 0, language, Format.NO_VALUE, null, subsampleOffsetUs, initializationData);
}
private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position,
@@ -676,9 +674,20 @@ import java.util.List;
int childPosition = parent.getPosition();
if (atomType == Atom.TYPE_encv) {
- atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex);
+ Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData(
+ parent, position, size);
+ if (sampleEntryEncryptionData != null) {
+ atomType = sampleEntryEncryptionData.first;
+ drmInitData = drmInitData == null ? null
+ : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);
+ out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;
+ }
parent.setPosition(childPosition);
}
+ // TODO: Uncomment when [Internal: b/63092960] is fixed.
+ // else {
+ // drmInitData = null;
+ // }
List initializationData = null;
String mimeType = null;
@@ -845,9 +854,20 @@ import java.util.List;
int childPosition = parent.getPosition();
if (atomType == Atom.TYPE_enca) {
- atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex);
+ Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData(
+ parent, position, size);
+ if (sampleEntryEncryptionData != null) {
+ atomType = sampleEntryEncryptionData.first;
+ drmInitData = drmInitData == null ? null
+ : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);
+ out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;
+ }
parent.setPosition(childPosition);
}
+ // TODO: Uncomment when [Internal: b/63092960] is fixed.
+ // else {
+ // drmInitData = null;
+ // }
// If the atom type determines a MIME type, set it immediately.
String mimeType = null;
@@ -1023,11 +1043,12 @@ import java.util.List;
}
/**
- * Parses encryption data from an audio/video sample entry, populating {@code out} and returning
- * the unencrypted atom type, or 0 if no common encryption sinf atom was present.
+ * Parses encryption data from an audio/video sample entry, returning a pair consisting of the
+ * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common
+ * encryption sinf atom was present.
*/
- private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position,
- int size, StsdData out, int entryIndex) {
+ private static Pair parseSampleEntryEncryptionData(
+ ParsableByteArray parent, int position, int size) {
int childPosition = parent.getPosition();
while (childPosition - position < size) {
parent.setPosition(childPosition);
@@ -1038,22 +1059,20 @@ import java.util.List;
Pair result = parseSinfFromParent(parent, childPosition,
childAtomSize);
if (result != null) {
- out.trackEncryptionBoxes[entryIndex] = result.second;
- return result.first;
+ return result;
}
}
childPosition += childAtomSize;
}
- // This enca/encv box does not have a data format so return an invalid atom type.
- return 0;
+ return null;
}
private static Pair parseSinfFromParent(ParsableByteArray parent,
int position, int size) {
int childPosition = position + Atom.HEADER_SIZE;
-
- boolean isCencScheme = false;
- TrackEncryptionBox trackEncryptionBox = null;
+ int schemeInformationBoxPosition = C.POSITION_UNSET;
+ int schemeInformationBoxSize = 0;
+ String schemeType = null;
Integer dataFormat = null;
while (childPosition - position < size) {
parent.setPosition(childPosition);
@@ -1063,36 +1082,60 @@ import java.util.List;
dataFormat = parent.readInt();
} else if (childAtomType == Atom.TYPE_schm) {
parent.skipBytes(4);
- isCencScheme = parent.readInt() == TYPE_cenc;
+ // scheme_type field. Defined in ISO/IEC 23001-7:2016, section 4.1.
+ schemeType = parent.readString(4);
} else if (childAtomType == Atom.TYPE_schi) {
- trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize);
+ schemeInformationBoxPosition = childPosition;
+ schemeInformationBoxSize = childAtomSize;
}
childPosition += childAtomSize;
}
- if (isCencScheme) {
+ if (schemeType != null) {
Assertions.checkArgument(dataFormat != null, "frma atom is mandatory");
- Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory");
- return Pair.create(dataFormat, trackEncryptionBox);
+ Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET,
+ "schi atom is mandatory");
+ TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition,
+ schemeInformationBoxSize, schemeType);
+ Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory");
+ return Pair.create(dataFormat, encryptionBox);
} else {
return null;
}
}
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
- int size) {
+ int size, String schemeType) {
int childPosition = position + Atom.HEADER_SIZE;
while (childPosition - position < size) {
parent.setPosition(childPosition);
int childAtomSize = parent.readInt();
int childAtomType = parent.readInt();
if (childAtomType == Atom.TYPE_tenc) {
- parent.skipBytes(6);
- boolean defaultIsEncrypted = parent.readUnsignedByte() == 1;
- int defaultInitVectorSize = parent.readUnsignedByte();
+ int fullAtom = parent.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ parent.skipBytes(1); // reserved = 0.
+ int defaultCryptByteBlock = 0;
+ int defaultSkipByteBlock = 0;
+ if (version == 0) {
+ parent.skipBytes(1); // reserved = 0.
+ } else /* version 1 or greater */ {
+ int patternByte = parent.readUnsignedByte();
+ defaultCryptByteBlock = (patternByte & 0xF0) >> 4;
+ defaultSkipByteBlock = patternByte & 0x0F;
+ }
+ boolean defaultIsProtected = parent.readUnsignedByte() == 1;
+ int defaultPerSampleIvSize = parent.readUnsignedByte();
byte[] defaultKeyId = new byte[16];
parent.readBytes(defaultKeyId, 0, defaultKeyId.length);
- return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId);
+ byte[] constantIv = null;
+ if (defaultIsProtected && defaultPerSampleIvSize == 0) {
+ int constantIvSize = parent.readUnsignedByte();
+ constantIv = new byte[constantIvSize];
+ parent.readBytes(constantIv, 0, constantIvSize);
+ }
+ return new TrackEncryptionBox(defaultIsProtected, schemeType, defaultPerSampleIvSize,
+ defaultKeyId, defaultCryptByteBlock, defaultSkipByteBlock, constantIv);
}
childPosition += childAtomSize;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
index a228a9b775..6b2077ef76 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -128,6 +128,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private final ParsableByteArray nalPrefix;
private final ParsableByteArray nalBuffer;
private final ParsableByteArray encryptionSignalByte;
+ private final ParsableByteArray defaultInitializationVector;
// Adjusts sample timestamps.
private final TimestampAdjuster timestampAdjuster;
@@ -197,6 +198,7 @@ public final class FragmentedMp4Extractor implements Extractor {
nalPrefix = new ParsableByteArray(5);
nalBuffer = new ParsableByteArray();
encryptionSignalByte = new ParsableByteArray(1);
+ defaultInitializationVector = new ParsableByteArray();
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<>();
pendingMetadataSampleInfos = new LinkedList<>();
@@ -462,8 +464,8 @@ public final class FragmentedMp4Extractor implements Extractor {
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) {
TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1,
C.TRACK_TYPE_TEXT);
- cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
- null, Format.NO_VALUE, 0, null, null));
+ cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0,
+ null));
cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput};
}
}
@@ -559,11 +561,12 @@ public final class FragmentedMp4Extractor implements Extractor {
parseTruns(traf, trackBundle, decodeTime, flags);
+ TrackEncryptionBox encryptionBox = trackBundle.track
+ .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
+
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) {
- TrackEncryptionBox trackEncryptionBox = trackBundle.track
- .sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex];
- parseSaiz(trackEncryptionBox, saiz.data, fragment);
+ parseSaiz(encryptionBox, saiz.data, fragment);
}
LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio);
@@ -579,7 +582,8 @@ public final class FragmentedMp4Extractor implements Extractor {
LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp);
LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd);
if (sbgp != null && sgpd != null) {
- parseSgpd(sbgp.data, sgpd.data, fragment);
+ parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null,
+ fragment);
}
int leafChildrenSize = traf.leafChildren.size();
@@ -811,7 +815,7 @@ public final class FragmentedMp4Extractor implements Extractor {
// here, because unsigned integers will still be parsed correctly (unless their top bit is
// set, which is never true in practice because sample offsets are always small).
int sampleOffset = trun.readInt();
- sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
+ sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000L) / timescale);
} else {
sampleCompositionTimeOffsetTable[i] = 0;
}
@@ -868,8 +872,8 @@ public final class FragmentedMp4Extractor implements Extractor {
out.fillEncryptionData(senc);
}
- private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, TrackFragment out)
- throws ParserException {
+ private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType,
+ TrackFragment out) throws ParserException {
sbgp.setPosition(Atom.HEADER_SIZE);
int sbgpFullAtom = sbgp.readInt();
if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) {
@@ -877,9 +881,9 @@ public final class FragmentedMp4Extractor implements Extractor {
return;
}
if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) {
- sbgp.skipBytes(4);
+ sbgp.skipBytes(4); // default_length.
}
- if (sbgp.readInt() != 1) {
+ if (sbgp.readInt() != 1) { // entry_count.
throw new ParserException("Entry count in sbgp != 1 (unsupported).");
}
@@ -892,25 +896,35 @@ public final class FragmentedMp4Extractor implements Extractor {
int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom);
if (sgpdVersion == 1) {
if (sgpd.readUnsignedInt() == 0) {
- throw new ParserException("Variable length decription in sgpd found (unsupported)");
+ throw new ParserException("Variable length description in sgpd found (unsupported)");
}
} else if (sgpdVersion >= 2) {
- sgpd.skipBytes(4);
+ sgpd.skipBytes(4); // default_sample_description_index.
}
- if (sgpd.readUnsignedInt() != 1) {
+ if (sgpd.readUnsignedInt() != 1) { // entry_count.
throw new ParserException("Entry count in sgpd != 1 (unsupported).");
}
// CencSampleEncryptionInformationGroupEntry
- sgpd.skipBytes(2);
+ sgpd.skipBytes(1); // reserved = 0.
+ int patternByte = sgpd.readUnsignedByte();
+ int cryptByteBlock = (patternByte & 0xF0) >> 4;
+ int skipByteBlock = patternByte & 0x0F;
boolean isProtected = sgpd.readUnsignedByte() == 1;
if (!isProtected) {
return;
}
- int initVectorSize = sgpd.readUnsignedByte();
+ int perSampleIvSize = sgpd.readUnsignedByte();
byte[] keyId = new byte[16];
sgpd.readBytes(keyId, 0, keyId.length);
+ byte[] constantIv = null;
+ if (isProtected && perSampleIvSize == 0) {
+ int constantIvSize = sgpd.readUnsignedByte();
+ constantIv = new byte[constantIvSize];
+ sgpd.readBytes(constantIv, 0, constantIvSize);
+ }
out.definesEncryptionData = true;
- out.trackEncryptionBox = new TrackEncryptionBox(isProtected, initVectorSize, keyId);
+ out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, perSampleIvSize, keyId,
+ cryptByteBlock, skipByteBlock, constantIv);
}
/**
@@ -1122,19 +1136,24 @@ public final class FragmentedMp4Extractor implements Extractor {
}
long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L;
- @C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0)
- | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0);
- int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;
- byte[] encryptionKey = null;
- if (fragment.definesEncryptionData) {
- encryptionKey = fragment.trackEncryptionBox != null
- ? fragment.trackEncryptionBox.keyId
- : track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId;
- }
if (timestampAdjuster != null) {
sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
}
- output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
+
+ @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex]
+ ? C.BUFFER_FLAG_KEY_FRAME : 0;
+
+ // Encryption data.
+ TrackOutput.CryptoData cryptoData = null;
+ if (fragment.definesEncryptionData) {
+ sampleFlags |= C.BUFFER_FLAG_ENCRYPTED;
+ TrackEncryptionBox encryptionBox = fragment.trackEncryptionBox != null
+ ? fragment.trackEncryptionBox
+ : track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
+ cryptoData = encryptionBox.cryptoData;
+ }
+
+ output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData);
while (!pendingMetadataSampleInfos.isEmpty()) {
MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
@@ -1190,12 +1209,24 @@ public final class FragmentedMp4Extractor implements Extractor {
*/
private int appendSampleEncryptionData(TrackBundle trackBundle) {
TrackFragment trackFragment = trackBundle.fragment;
- ParsableByteArray sampleEncryptionData = trackFragment.sampleEncryptionData;
int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex;
TrackEncryptionBox encryptionBox = trackFragment.trackEncryptionBox != null
? trackFragment.trackEncryptionBox
- : trackBundle.track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];
- int vectorSize = encryptionBox.initializationVectorSize;
+ : trackBundle.track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex);
+
+ ParsableByteArray initializationVectorData;
+ int vectorSize;
+ if (encryptionBox.initializationVectorSize != 0) {
+ initializationVectorData = trackFragment.sampleEncryptionData;
+ vectorSize = encryptionBox.initializationVectorSize;
+ } else {
+ // The default initialization vector should be used.
+ byte[] initVectorData = encryptionBox.defaultInitializationVector;
+ defaultInitializationVector.reset(initVectorData, initVectorData.length);
+ initializationVectorData = defaultInitializationVector;
+ vectorSize = initVectorData.length;
+ }
+
boolean subsampleEncryption = trackFragment
.sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex];
@@ -1205,20 +1236,20 @@ public final class FragmentedMp4Extractor implements Extractor {
TrackOutput output = trackBundle.output;
output.sampleData(encryptionSignalByte, 1);
// Write the vector.
- output.sampleData(sampleEncryptionData, vectorSize);
+ output.sampleData(initializationVectorData, vectorSize);
// If we don't have subsample encryption data, we're done.
if (!subsampleEncryption) {
return 1 + vectorSize;
}
// Write the subsample encryption data.
- int subsampleCount = sampleEncryptionData.readUnsignedShort();
- sampleEncryptionData.skipBytes(-2);
+ ParsableByteArray subsampleEncryptionData = trackFragment.sampleEncryptionData;
+ int subsampleCount = subsampleEncryptionData.readUnsignedShort();
+ subsampleEncryptionData.skipBytes(-2);
int subsampleDataLength = 2 + 6 * subsampleCount;
- output.sampleData(sampleEncryptionData, subsampleDataLength);
+ output.sampleData(subsampleEncryptionData, subsampleDataLength);
return 1 + vectorSize + subsampleDataLength;
}
-
/** Returns DrmInitData from leaf atoms. */
private static DrmInitData getDrmInitDataFromAtoms(List leafChildren) {
ArrayList schemeDatas = null;
@@ -1234,7 +1265,7 @@ public final class FragmentedMp4Extractor implements Extractor {
if (uuid == null) {
Log.w(TAG, "Skipped pssh atom (failed to extract uuid)");
} else {
- schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData));
+ schemeDatas.add(new SchemeData(uuid, null, MimeTypes.VIDEO_MP4, psshData));
}
}
}
@@ -1308,8 +1339,12 @@ public final class FragmentedMp4Extractor implements Extractor {
}
public void updateDrmInitData(DrmInitData drmInitData) {
- output.format(track.format.copyWithDrmInitData(drmInitData));
+ TrackEncryptionBox encryptionBox =
+ track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
+ String schemeType = encryptionBox != null ? encryptionBox.schemeType : null;
+ output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType)));
}
+
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java
index f1c4e99ec1..7ac3158794 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp4;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import java.lang.annotation.Retention;
@@ -77,11 +78,6 @@ public final class Track {
*/
@Transformation public final int sampleTransformation;
- /**
- * Track encryption boxes for the different track sample descriptions. Entries may be null.
- */
- public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes;
-
/**
* Durations of edit list segments in the movie timescale. Null if there is no edit list.
*/
@@ -98,9 +94,11 @@ public final class Track {
*/
public final int nalUnitLengthFieldLength;
+ @Nullable private final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes;
+
public Track(int id, int type, long timescale, long movieTimescale, long durationUs,
Format format, @Transformation int sampleTransformation,
- TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
+ @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
long[] editListDurations, long[] editListMediaTimes) {
this.id = id;
this.type = type;
@@ -115,4 +113,16 @@ public final class Track {
this.editListMediaTimes = editListMediaTimes;
}
+ /**
+ * Returns the {@link TrackEncryptionBox} for the given sample description index.
+ *
+ * @param sampleDescriptionIndex The given sample description index
+ * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no
+ * such entry exists.
+ */
+ public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) {
+ return sampleDescriptionEncryptionBoxes == null ? null
+ : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
index dde03a8507..d39aae0c5f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
@@ -15,37 +15,86 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.Assertions;
+
/**
* Encapsulates information parsed from a track encryption (tenc) box or sample group description
* (sgpd) box in an MP4 stream.
*/
public final class TrackEncryptionBox {
+ private static final String TAG = "TrackEncryptionBox";
+
/**
* Indicates the encryption state of the samples in the sample group.
*/
public final boolean isEncrypted;
+ /**
+ * The protection scheme type, as defined by the 'schm' box, or null if unknown.
+ */
+ @Nullable public final String schemeType;
+
+ /**
+ * A {@link TrackOutput.CryptoData} instance containing the encryption information from this
+ * {@link TrackEncryptionBox}.
+ */
+ public final TrackOutput.CryptoData cryptoData;
+
/**
* The initialization vector size in bytes for the samples in the corresponding sample group.
*/
public final int initializationVectorSize;
/**
- * The key identifier for the samples in the corresponding sample group.
+ * If {@link #initializationVectorSize} is 0, holds the default initialization vector as defined
+ * in the track encryption box or sample group description box. Null otherwise.
*/
- public final byte[] keyId;
+ public final byte[] defaultInitializationVector;
/**
- * @param isEncrypted Indicates the encryption state of the samples in the sample group.
- * @param initializationVectorSize The initialization vector size in bytes for the samples in the
- * corresponding sample group.
- * @param keyId The key identifier for the samples in the corresponding sample group.
+ * @param isEncrypted See {@link #isEncrypted}.
+ * @param schemeType See {@link #schemeType}.
+ * @param initializationVectorSize See {@link #initializationVectorSize}.
+ * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}.
+ * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}.
+ * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}.
+ * @param defaultInitializationVector See {@link #defaultInitializationVector}.
*/
- public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) {
+ public TrackEncryptionBox(boolean isEncrypted, @Nullable String schemeType,
+ int initializationVectorSize, byte[] keyId, int defaultEncryptedBlocks,
+ int defaultClearBlocks, @Nullable byte[] defaultInitializationVector) {
+ Assertions.checkArgument(initializationVectorSize == 0 ^ defaultInitializationVector == null);
this.isEncrypted = isEncrypted;
+ this.schemeType = schemeType;
this.initializationVectorSize = initializationVectorSize;
- this.keyId = keyId;
+ this.defaultInitializationVector = defaultInitializationVector;
+ cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId,
+ defaultEncryptedBlocks, defaultClearBlocks);
+ }
+
+ @C.CryptoMode
+ private static int schemeToCryptoMode(@Nullable String schemeType) {
+ if (schemeType == null) {
+ // If unknown, assume cenc.
+ return C.CRYPTO_MODE_AES_CTR;
+ }
+ switch (schemeType) {
+ case C.CENC_TYPE_cenc:
+ case C.CENC_TYPE_cens:
+ return C.CRYPTO_MODE_AES_CTR;
+ case C.CENC_TYPE_cbc1:
+ case C.CENC_TYPE_cbcs:
+ return C.CRYPTO_MODE_AES_CBC;
+ default:
+ Log.w(TAG, "Unsupported protection scheme type '" + schemeType + "'. Assuming AES-CTR "
+ + "crypto mode.");
+ return C.CRYPTO_MODE_AES_CTR;
+ }
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
index cc3c5de311..54e168c665 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
@@ -45,30 +45,14 @@ public class OggExtractor implements Extractor {
private static final int MAX_VERIFICATION_BYTES = 8;
+ private ExtractorOutput output;
private StreamReader streamReader;
+ private boolean streamReaderInitialized;
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
try {
- OggPageHeader header = new OggPageHeader();
- if (!header.populate(input, true) || (header.type & 0x02) != 0x02) {
- return false;
- }
-
- int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES);
- ParsableByteArray scratch = new ParsableByteArray(length);
- input.peekFully(scratch.data, 0, length);
-
- if (FlacReader.verifyBitstreamType(resetPosition(scratch))) {
- streamReader = new FlacReader();
- } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) {
- streamReader = new VorbisReader();
- } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) {
- streamReader = new OpusReader();
- } else {
- return false;
- }
- return true;
+ return sniffInternal(input);
} catch (ParserException e) {
return false;
}
@@ -76,15 +60,14 @@ public class OggExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
- TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
- output.endTracks();
- // TODO: fix the case if sniff() isn't called
- streamReader.init(output, trackOutput);
+ this.output = output;
}
@Override
public void seek(long position, long timeUs) {
- streamReader.seek(position, timeUs);
+ if (streamReader != null) {
+ streamReader.seek(position, timeUs);
+ }
}
@Override
@@ -95,12 +78,41 @@ public class OggExtractor implements Extractor {
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
+ if (streamReader == null) {
+ if (!sniffInternal(input)) {
+ throw new ParserException("Failed to determine bitstream type");
+ }
+ input.resetPeekPosition();
+ }
+ if (!streamReaderInitialized) {
+ TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
+ output.endTracks();
+ streamReader.init(output, trackOutput);
+ streamReaderInitialized = true;
+ }
return streamReader.read(input, seekPosition);
}
- //@VisibleForTesting
- /* package */ StreamReader getStreamReader() {
- return streamReader;
+ private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException {
+ OggPageHeader header = new OggPageHeader();
+ if (!header.populate(input, true) || (header.type & 0x02) != 0x02) {
+ return false;
+ }
+
+ int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES);
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.peekFully(scratch.data, 0, length);
+
+ if (FlacReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new FlacReader();
+ } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new VorbisReader();
+ } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new OpusReader();
+ } else {
+ return false;
+ }
+ return true;
}
private static ParsableByteArray resetPosition(ParsableByteArray scratch) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
index c203b0c6bd..d136468faa 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
@@ -41,7 +41,8 @@ import java.io.IOException;
OggSeeker oggSeeker;
}
- private OggPacket oggPacket;
+ private final OggPacket oggPacket;
+
private TrackOutput trackOutput;
private ExtractorOutput extractorOutput;
private OggSeeker oggSeeker;
@@ -55,11 +56,13 @@ import java.io.IOException;
private boolean seekMapSet;
private boolean formatSet;
+ public StreamReader() {
+ oggPacket = new OggPacket();
+ }
+
void init(ExtractorOutput output, TrackOutput trackOutput) {
this.extractorOutput = output;
this.trackOutput = trackOutput;
- this.oggPacket = new OggPacket();
-
reset(true);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
index 9c9536beec..8bab6b7ed1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
@@ -56,9 +56,9 @@ public final class Ac3Extractor implements Extractor {
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
private final long firstSampleTimestampUs;
+ private final Ac3Reader reader;
private final ParsableByteArray sampleData;
- private Ac3Reader reader;
private boolean startedPacket;
public Ac3Extractor() {
@@ -67,6 +67,7 @@ public final class Ac3Extractor implements Extractor {
public Ac3Extractor(long firstSampleTimestampUs) {
this.firstSampleTimestampUs = firstSampleTimestampUs;
+ reader = new Ac3Reader();
sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE);
}
@@ -117,7 +118,6 @@ public final class Ac3Extractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
- reader = new Ac3Reader(); // TODO: Add support for embedded ID3.
reader.createTracks(output, new TrackIdGenerator(0, 1));
output.endTracks();
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
index f7dadd51b2..a1851aa0ea 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
@@ -55,10 +55,9 @@ public final class AdtsExtractor implements Extractor {
private static final int MAX_SNIFF_BYTES = 8 * 1024;
private final long firstSampleTimestampUs;
+ private final AdtsReader reader;
private final ParsableByteArray packetBuffer;
- // Accessed only by the loading thread.
- private AdtsReader reader;
private boolean startedPacket;
public AdtsExtractor() {
@@ -67,6 +66,7 @@ public final class AdtsExtractor implements Extractor {
public AdtsExtractor(long firstSampleTimestampUs) {
this.firstSampleTimestampUs = firstSampleTimestampUs;
+ reader = new AdtsReader(true);
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
}
@@ -127,7 +127,6 @@ public final class AdtsExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
- reader = new AdtsReader(true);
reader.createTracks(output, new TrackIdGenerator(0, 1));
output.endTracks();
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
index 4bc7f11c1a..40cfd7f8d9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
@@ -78,7 +78,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
this.flags = flags;
if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) {
closedCaptionFormats = Collections.singletonList(Format.createTextSampleFormat(null,
- MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null));
+ MimeTypes.APPLICATION_CEA608, 0, null));
}
this.closedCaptionFormats = closedCaptionFormats;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
index 7266f847c4..a3502a3242 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
@@ -51,17 +51,17 @@ public final class H262Reader implements ElementaryStreamReader {
// State that should be reset on seek.
private final boolean[] prefixFlags;
private final CsdBuffer csdBuffer;
- private boolean foundFirstFrameInGroup;
private long totalBytesWritten;
+ private boolean startedFirstSample;
// Per packet state that gets reset at the start of each packet.
private long pesTimeUs;
- private boolean pesPtsUsAvailable;
- // Per sample state that gets reset at the start of each frame.
- private boolean isKeyframe;
- private long framePosition;
- private long frameTimeUs;
+ // Per sample state that gets reset at the start of each sample.
+ private long samplePosition;
+ private long sampleTimeUs;
+ private boolean sampleIsKeyframe;
+ private boolean sampleHasPicture;
public H262Reader() {
prefixFlags = new boolean[4];
@@ -72,9 +72,8 @@ public final class H262Reader implements ElementaryStreamReader {
public void seek() {
NalUnitUtil.clearPrefixFlags(prefixFlags);
csdBuffer.reset();
- pesPtsUsAvailable = false;
- foundFirstFrameInGroup = false;
totalBytesWritten = 0;
+ startedFirstSample = false;
}
@Override
@@ -86,10 +85,7 @@ public final class H262Reader implements ElementaryStreamReader {
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
- pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET;
- if (pesPtsUsAvailable) {
- this.pesTimeUs = pesTimeUs;
- }
+ this.pesTimeUs = pesTimeUs;
}
@Override
@@ -102,9 +98,8 @@ public final class H262Reader implements ElementaryStreamReader {
totalBytesWritten += data.bytesLeft();
output.sampleData(data, data.bytesLeft());
- int searchOffset = offset;
while (true) {
- int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, searchOffset, limit, prefixFlags);
+ int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
if (startCodeOffset == limit) {
// We've scanned to the end of the data without finding another start code.
@@ -125,7 +120,7 @@ public final class H262Reader implements ElementaryStreamReader {
csdBuffer.onData(dataArray, offset, startCodeOffset);
}
// This is the number of bytes belonging to the next start code that have already been
- // passed to csdDataTargetBuffer.
+ // passed to csdBuffer.
int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0;
if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) {
// The csd data is complete, so we can decode and output the media format.
@@ -136,27 +131,29 @@ public final class H262Reader implements ElementaryStreamReader {
}
}
- if (hasOutputFormat && (startCodeValue == START_GROUP || startCodeValue == START_PICTURE)) {
+ if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) {
int bytesWrittenPastStartCode = limit - startCodeOffset;
- if (foundFirstFrameInGroup) {
- @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
- int size = (int) (totalBytesWritten - framePosition) - bytesWrittenPastStartCode;
- output.sampleMetadata(frameTimeUs, flags, size, bytesWrittenPastStartCode, null);
- isKeyframe = false;
+ if (startedFirstSample && sampleHasPicture && hasOutputFormat) {
+ // Output the sample.
+ @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode;
+ output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null);
}
- if (startCodeValue == START_GROUP) {
- foundFirstFrameInGroup = false;
- isKeyframe = true;
- } else /* startCodeValue == START_PICTURE */ {
- frameTimeUs = pesPtsUsAvailable ? pesTimeUs : (frameTimeUs + frameDurationUs);
- framePosition = totalBytesWritten - bytesWrittenPastStartCode;
- pesPtsUsAvailable = false;
- foundFirstFrameInGroup = true;
+ if (!startedFirstSample || sampleHasPicture) {
+ // Start the next sample.
+ samplePosition = totalBytesWritten - bytesWrittenPastStartCode;
+ sampleTimeUs = pesTimeUs != C.TIME_UNSET ? pesTimeUs
+ : (startedFirstSample ? (sampleTimeUs + frameDurationUs) : 0);
+ sampleIsKeyframe = false;
+ pesTimeUs = C.TIME_UNSET;
+ startedFirstSample = true;
}
+ sampleHasPicture = startCodeValue == START_PICTURE;
+ } else if (startCodeValue == START_GROUP) {
+ sampleIsKeyframe = true;
}
- offset = startCodeOffset;
- searchOffset = offset + 3;
+ offset = startCodeOffset + 3;
}
}
@@ -221,6 +218,8 @@ public final class H262Reader implements ElementaryStreamReader {
private static final class CsdBuffer {
+ private static final byte[] START_CODE = new byte[] {0, 0, 1};
+
private boolean isFilling;
public int length;
@@ -244,24 +243,25 @@ public final class H262Reader implements ElementaryStreamReader {
* Called when a start code is encountered in the stream.
*
* @param startCodeValue The start code value.
- * @param bytesAlreadyPassed The number of bytes of the start code that have already been
- * passed to {@link #onData(byte[], int, int)}, or 0.
+ * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to
+ * {@link #onData(byte[], int, int)}, or 0.
* @return Whether the csd data is now complete. If true is returned, neither
- * this method or {@link #onData(byte[], int, int)} should be called again without an
+ * this method nor {@link #onData(byte[], int, int)} should be called again without an
* interleaving call to {@link #reset()}.
*/
public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) {
if (isFilling) {
+ length -= bytesAlreadyPassed;
if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) {
sequenceExtensionPosition = length;
} else {
- length -= bytesAlreadyPassed;
isFilling = false;
return true;
}
} else if (startCodeValue == START_SEQUENCE_HEADER) {
isFilling = true;
}
+ onData(START_CODE, 0, START_CODE.length);
return false;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
index 1149856649..2929b8a076 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
@@ -105,8 +105,8 @@ public final class TsExtractor implements Extractor {
private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3");
private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC");
- private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2
- private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT;
+ private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50;
+ private static final int SNIFF_TS_PACKET_COUNT = 5;
@Mode private final int mode;
private final List timestampAdjusters;
@@ -174,10 +174,10 @@ public final class TsExtractor implements Extractor {
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
byte[] buffer = tsPacketBuffer.data;
- input.peekFully(buffer, 0, BUFFER_SIZE);
+ input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT);
for (int j = 0; j < TS_PACKET_SIZE; j++) {
for (int i = 0; true; i++) {
- if (i == BUFFER_PACKET_COUNT) {
+ if (i == SNIFF_TS_PACKET_COUNT) {
input.skipFully(j);
return true;
}
@@ -216,7 +216,8 @@ public final class TsExtractor implements Extractor {
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
byte[] data = tsPacketBuffer.data;
- // Shift bytes to the start of the buffer if there isn't enough space left at the end
+
+ // Shift bytes to the start of the buffer if there isn't enough space left at the end.
if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) {
int bytesLeft = tsPacketBuffer.bytesLeft();
if (bytesLeft > 0) {
@@ -224,7 +225,8 @@ public final class TsExtractor implements Extractor {
}
tsPacketBuffer.reset(data, bytesLeft);
}
- // Read more bytes until there is at least one packet size
+
+ // Read more bytes until we have at least one packet.
while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) {
int limit = tsPacketBuffer.limit();
int read = input.read(data, limit, BUFFER_SIZE - limit);
@@ -234,8 +236,7 @@ public final class TsExtractor implements Extractor {
tsPacketBuffer.setLimit(limit + read);
}
- // Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of
- // the header.
+ // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
final int limit = tsPacketBuffer.limit();
int position = tsPacketBuffer.getPosition();
while (position < limit && data[position] != TS_SYNC_BYTE) {
@@ -554,5 +555,4 @@ public final class TsExtractor implements Extractor {
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
index a7c237edb2..2e5b04f4a9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -61,6 +61,14 @@ public final class MediaCodecInfo {
*/
public final boolean tunneling;
+ /**
+ * Whether the decoder is secure.
+ *
+ * @see CodecCapabilities#isFeatureSupported(String)
+ * @see CodecCapabilities#FEATURE_SecurePlayback
+ */
+ public final boolean secure;
+
private final String mimeType;
private final CodecCapabilities capabilities;
@@ -71,7 +79,7 @@ public final class MediaCodecInfo {
* @return The created instance.
*/
public static MediaCodecInfo newPassthroughInstance(String name) {
- return new MediaCodecInfo(name, null, null, false);
+ return new MediaCodecInfo(name, null, null, false, false);
}
/**
@@ -84,7 +92,7 @@ public final class MediaCodecInfo {
*/
public static MediaCodecInfo newInstance(String name, String mimeType,
CodecCapabilities capabilities) {
- return new MediaCodecInfo(name, mimeType, capabilities, false);
+ return new MediaCodecInfo(name, mimeType, capabilities, false, false);
}
/**
@@ -94,20 +102,22 @@ public final class MediaCodecInfo {
* @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}.
+ * @param forceSecure Whether {@link #secure} should be forced to {@code true}.
* @return The created instance.
*/
public static MediaCodecInfo newInstance(String name, String mimeType,
- CodecCapabilities capabilities, boolean forceDisableAdaptive) {
- return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive);
+ CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) {
+ return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive, forceSecure);
}
private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities,
- boolean forceDisableAdaptive) {
+ boolean forceDisableAdaptive, boolean forceSecure) {
this.name = Assertions.checkNotNull(name);
this.mimeType = mimeType;
this.capabilities = capabilities;
adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);
tunneling = capabilities != null && isTunneling(capabilities);
+ secure = forceSecure || (capabilities != null && isSecure(capabilities));
}
/**
@@ -176,12 +186,12 @@ public final class MediaCodecInfo {
logNoSupport("sizeAndRate.vCaps");
return false;
}
- if (!areSizeAndRateSupported(videoCapabilities, width, height, frameRate)) {
+ if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) {
// Capabilities are known to be inaccurately reported for vertical resolutions on some devices
// (b/31387661). If the video is vertical and the capabilities indicate support if the width
// and height are swapped, we assume that the vertical resolution is also supported.
if (width >= height
- || !areSizeAndRateSupported(videoCapabilities, height, width, frameRate)) {
+ || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) {
logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate);
return false;
}
@@ -326,14 +336,6 @@ public final class MediaCodecInfo {
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
}
- @TargetApi(21)
- private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width,
- int height, double frameRate) {
- return frameRate == Format.NO_VALUE || frameRate <= 0
- ? capabilities.isSizeSupported(width, height)
- : capabilities.areSizeAndRateSupported(width, height, frameRate);
- }
-
private static boolean isTunneling(CodecCapabilities capabilities) {
return Util.SDK_INT >= 21 && isTunnelingV21(capabilities);
}
@@ -343,4 +345,21 @@ public final class MediaCodecInfo {
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback);
}
+ private static boolean isSecure(CodecCapabilities capabilities) {
+ return Util.SDK_INT >= 21 && isSecureV21(capabilities);
+ }
+
+ @TargetApi(21)
+ private static boolean isSecureV21(CodecCapabilities capabilities) {
+ return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback);
+ }
+
+ @TargetApi(21)
+ private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width,
+ int height, double frameRate) {
+ return frameRate == Format.NO_VALUE || frameRate <= 0
+ ? capabilities.isSizeSupported(width, height)
+ : capabilities.areSizeAndRateSupported(width, height, frameRate);
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index f0e23ebfeb..01229c1104 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -23,6 +23,7 @@ import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Looper;
import android.os.SystemClock;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
@@ -31,7 +32,9 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
@@ -175,10 +178,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private final MediaCodec.BufferInfo outputBufferInfo;
private Format format;
- private MediaCodec codec;
private DrmSession drmSession;
private DrmSession pendingDrmSession;
- private boolean codecIsAdaptive;
+ private MediaCodec codec;
+ private MediaCodecInfo codecInfo;
private boolean codecNeedsDiscardToSpsWorkaround;
private boolean codecNeedsFlushWorkaround;
private boolean codecNeedsAdaptationWorkaround;
@@ -237,14 +240,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
@Override
- public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
+ public final int supportsMixedMimeTypeAdaptation() {
return ADAPTIVE_NOT_SEAMLESS;
}
@Override
public final int supportsFormat(Format format) throws ExoPlaybackException {
try {
- return supportsFormat(mediaCodecSelector, format);
+ int formatSupport = supportsFormat(mediaCodecSelector, format);
+ if ((formatSupport & FORMAT_SUPPORT_MASK) > FORMAT_UNSUPPORTED_DRM
+ && !isDrmSchemeSupported(drmSessionManager, format.drmInitData)) {
+ // The renderer advertises higher support than FORMAT_UNSUPPORTED_DRM but the DRM scheme is
+ // not supported. The format support is truncated to reflect this.
+ formatSupport = (formatSupport & ~FORMAT_SUPPORT_MASK) | FORMAT_UNSUPPORTED_DRM;
+ }
+ return formatSupport;
} catch (DecoderQueryException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
@@ -291,55 +301,60 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@SuppressWarnings("deprecation")
protected final void maybeInitCodec() throws ExoPlaybackException {
- if (!shouldInitCodec()) {
+ if (codec != null || format == null) {
+ // We have a codec already, or we don't have a format with which to instantiate one.
return;
}
drmSession = pendingDrmSession;
String mimeType = format.sampleMimeType;
- MediaCrypto mediaCrypto = null;
+ MediaCrypto wrappedMediaCrypto = null;
boolean drmSessionRequiresSecureDecoder = false;
if (drmSession != null) {
- @DrmSession.State int drmSessionState = drmSession.getState();
- if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
- } else if (drmSessionState == DrmSession.STATE_OPENED
- || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
- mediaCrypto = drmSession.getMediaCrypto().getWrappedMediaCrypto();
- drmSessionRequiresSecureDecoder = drmSession.requiresSecureDecoderComponent(mimeType);
- } else {
+ FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
+ if (mediaCrypto == null) {
+ DrmSessionException drmError = drmSession.getError();
+ if (drmError != null) {
+ throw ExoPlaybackException.createForRenderer(drmError, getIndex());
+ }
// The drm session isn't open yet.
return;
}
+ wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
+ drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
- MediaCodecInfo decoderInfo = null;
- try {
- decoderInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder);
- if (decoderInfo == null && drmSessionRequiresSecureDecoder) {
- // The drm session indicates that a secure decoder is required, but the device does not have
- // one. Assuming that supportsFormat indicated support for the media being played, we know
- // that it does not require a secure output path. Most CDM implementations allow playback to
- // proceed with a non-secure decoder in this case, so we try our luck.
- decoderInfo = getDecoderInfo(mediaCodecSelector, format, false);
- if (decoderInfo != null) {
- Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but "
- + "no secure decoder available. Trying to proceed with " + decoderInfo.name + ".");
+ if (codecInfo == null) {
+ try {
+ codecInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder);
+ if (codecInfo == null && drmSessionRequiresSecureDecoder) {
+ // The drm session indicates that a secure decoder is required, but the device does not
+ // have one. Assuming that supportsFormat indicated support for the media being played, we
+ // know that it does not require a secure output path. Most CDM implementations allow
+ // playback to proceed with a non-secure decoder in this case, so we try our luck.
+ codecInfo = getDecoderInfo(mediaCodecSelector, format, false);
+ if (codecInfo != null) {
+ Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but "
+ + "no secure decoder available. Trying to proceed with " + codecInfo.name + ".");
+ }
}
+ } catch (DecoderQueryException e) {
+ throwDecoderInitError(new DecoderInitializationException(format, e,
+ drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR));
+ }
+
+ if (codecInfo == null) {
+ throwDecoderInitError(new DecoderInitializationException(format, null,
+ drmSessionRequiresSecureDecoder,
+ DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
}
- } catch (DecoderQueryException e) {
- throwDecoderInitError(new DecoderInitializationException(format, e,
- drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR));
}
- if (decoderInfo == null) {
- throwDecoderInitError(new DecoderInitializationException(format, null,
- drmSessionRequiresSecureDecoder,
- DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
+ if (!shouldInitCodec(codecInfo)) {
+ return;
}
- String codecName = decoderInfo.name;
- codecIsAdaptive = decoderInfo.adaptive;
+ String codecName = codecInfo.name;
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName);
@@ -353,7 +368,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codec = MediaCodec.createByCodecName(codecName);
TraceUtil.endSection();
TraceUtil.beginSection("configureCodec");
- configureCodec(decoderInfo, codec, format, mediaCrypto);
+ configureCodec(codecInfo, codec, format, wrappedMediaCrypto);
TraceUtil.endSection();
TraceUtil.beginSection("startCodec");
codec.start();
@@ -380,14 +395,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
- protected boolean shouldInitCodec() {
- return codec == null && format != null;
+ protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
+ return true;
}
protected final MediaCodec getCodec() {
return codec;
}
+ protected final MediaCodecInfo getCodecInfo() {
+ return codecInfo;
+ }
+
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
decoderCounters = new DecoderCounters();
@@ -426,31 +445,31 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
protected void releaseCodec() {
+ codecHotswapDeadlineMs = C.TIME_UNSET;
+ inputIndex = C.INDEX_UNSET;
+ outputIndex = C.INDEX_UNSET;
+ waitingForKeys = false;
+ shouldSkipOutputBuffer = false;
+ decodeOnlyPresentationTimestamps.clear();
+ inputBuffers = null;
+ outputBuffers = null;
+ codecInfo = null;
+ codecReconfigured = false;
+ codecReceivedBuffers = false;
+ codecNeedsDiscardToSpsWorkaround = false;
+ codecNeedsFlushWorkaround = false;
+ codecNeedsAdaptationWorkaround = false;
+ codecNeedsEosPropagationWorkaround = false;
+ codecNeedsEosFlushWorkaround = false;
+ codecNeedsMonoChannelCountWorkaround = false;
+ codecNeedsAdaptationWorkaroundBuffer = false;
+ shouldSkipAdaptationWorkaroundOutputBuffer = false;
+ codecReceivedEos = false;
+ codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+ codecReinitializationState = REINITIALIZATION_STATE_NONE;
+ buffer.data = null;
if (codec != null) {
- codecHotswapDeadlineMs = C.TIME_UNSET;
- inputIndex = C.INDEX_UNSET;
- outputIndex = C.INDEX_UNSET;
- waitingForKeys = false;
- shouldSkipOutputBuffer = false;
- decodeOnlyPresentationTimestamps.clear();
- inputBuffers = null;
- outputBuffers = null;
- codecReconfigured = false;
- codecReceivedBuffers = false;
- codecIsAdaptive = false;
- codecNeedsDiscardToSpsWorkaround = false;
- codecNeedsFlushWorkaround = false;
- codecNeedsAdaptationWorkaround = false;
- codecNeedsEosPropagationWorkaround = false;
- codecNeedsEosFlushWorkaround = false;
- codecNeedsMonoChannelCountWorkaround = false;
- codecNeedsAdaptationWorkaroundBuffer = false;
- shouldSkipAdaptationWorkaroundOutputBuffer = false;
- codecReceivedEos = false;
- codecReconfigurationState = RECONFIGURATION_STATE_NONE;
- codecReinitializationState = REINITIALIZATION_STATE_NONE;
decoderCounters.decoderReleaseCount++;
- buffer.data = null;
try {
codec.stop();
} finally {
@@ -727,15 +746,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null) {
+ if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
- return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
- && (bufferEncrypted || !playClearSamplesWithoutKeys);
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
/**
@@ -781,7 +799,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
if (pendingDrmSession == drmSession && codec != null
- && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) {
+ && canReconfigureCodec(codec, codecInfo.adaptive, oldFormat, format)) {
codecReconfigured = true;
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround
@@ -1065,6 +1083,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return false;
}
+ /**
+ * Returns whether the encryption scheme is supported, or true if {@code drmInitData} is null.
+ *
+ * @param drmSessionManager The drm session manager associated with the renderer.
+ * @param drmInitData {@link DrmInitData} of the format to check for support.
+ * @return Whether the encryption scheme is supported, or true if {@code drmInitData} is null.
+ */
+ private static boolean isDrmSchemeSupported(DrmSessionManager drmSessionManager,
+ @Nullable DrmInitData drmInitData) {
+ if (drmInitData == null) {
+ // Content is unencrypted.
+ return true;
+ } else if (drmSessionManager == null) {
+ // Content is encrypted, but no drm session manager is available.
+ return false;
+ }
+ return drmSessionManager.canAcquireSession(drmInitData);
+ }
+
/**
* Returns whether the decoder is known to fail when flushed.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
index bb946d76f9..1823c3a7ff 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
@@ -55,8 +55,7 @@ public interface MediaCodecSelector {
/**
* Selects a decoder to instantiate for audio passthrough.
*
- * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
- * exists.
+ * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
index 392162f607..d3f3dae344 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -101,7 +101,7 @@ public final class MediaCodecUtil {
/**
* Returns information about a decoder suitable for audio passthrough.
- **
+ *
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
* exists.
*/
@@ -233,10 +233,10 @@ public final class MediaCodecUtil {
if ((secureDecodersExplicit && key.secure == secure)
|| (!secureDecodersExplicit && !key.secure)) {
decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities,
- forceDisableAdaptive));
+ forceDisableAdaptive, false));
} else if (!secureDecodersExplicit && secure) {
decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType,
- capabilities, forceDisableAdaptive));
+ capabilities, forceDisableAdaptive, true));
// It only makes sense to have one synthesized secure decoder, return immediately.
return decoderInfos;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
index 70b2d8aab9..7ff426e2df 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
@@ -104,7 +104,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
}
@Override
- protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
decoder = decoderFactory.createDecoder(formats[0]);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
index df3353fb18..6b2e5c3675 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
@@ -483,13 +483,8 @@ public final class Id3Decoder implements MetadataDecoder {
int ownerEndIndex = indexOfZeroByte(data, 0);
String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1");
- byte[] privateData;
int privateDataStartIndex = ownerEndIndex + 1;
- if (privateDataStartIndex < data.length) {
- privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length);
- } else {
- privateData = new byte[0];
- }
+ byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length);
return new PrivFrame(owner, privateData);
}
@@ -516,7 +511,7 @@ public final class Id3Decoder implements MetadataDecoder {
descriptionEndIndex - descriptionStartIndex, charset);
int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
- byte[] objectData = Arrays.copyOfRange(data, objectDataStartIndex, data.length);
+ byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length);
return new GeobFrame(mimeType, filename, description, objectData);
}
@@ -553,7 +548,7 @@ public final class Id3Decoder implements MetadataDecoder {
descriptionEndIndex - descriptionStartIndex, charset);
int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
- byte[] pictureData = Arrays.copyOfRange(data, pictureDataStartIndex, data.length);
+ byte[] pictureData = copyOfRangeIfValid(data, pictureDataStartIndex, data.length);
return new ApicFrame(mimeType, description, pictureType, pictureData);
}
@@ -749,6 +744,22 @@ public final class Id3Decoder implements MetadataDecoder {
? 1 : 2;
}
+ /**
+ * Copies the specified range of an array, or returns a zero length array if the range is invalid.
+ *
+ * @param data The array from which to copy.
+ * @param from The start of the range to copy (inclusive).
+ * @param to The end of the range to copy (exclusive).
+ * @return The copied data, or a zero length array if the range is invalid.
+ */
+ private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) {
+ if (to <= from) {
+ // Invalid or zero length range.
+ return new byte[0];
+ }
+ return Arrays.copyOfRange(data, from, to);
+ }
+
private static final class Id3Header {
private final int majorVersion;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
new file mode 100644
index 0000000000..42ac938677
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+
+/**
+ * Abstract base class for the concatenation of one or more {@link Timeline}s.
+ */
+/* package */ abstract class AbstractConcatenatedTimeline extends Timeline {
+
+ private final int childCount;
+
+ public AbstractConcatenatedTimeline(int childCount) {
+ this.childCount = childCount;
+ }
+
+ @Override
+ public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ int childIndex = getChildIndexByWindowIndex(windowIndex);
+ int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
+ int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex(
+ windowIndex - firstWindowIndexInChild,
+ repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode);
+ if (nextWindowIndexInChild != C.INDEX_UNSET) {
+ return firstWindowIndexInChild + nextWindowIndexInChild;
+ } else {
+ int nextChildIndex = childIndex + 1;
+ if (nextChildIndex < childCount) {
+ return getFirstWindowIndexByChildIndex(nextChildIndex);
+ } else if (repeatMode == Player.REPEAT_MODE_ALL) {
+ return 0;
+ } else {
+ return C.INDEX_UNSET;
+ }
+ }
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ int childIndex = getChildIndexByWindowIndex(windowIndex);
+ int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
+ int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex(
+ windowIndex - firstWindowIndexInChild,
+ repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode);
+ if (previousWindowIndexInChild != C.INDEX_UNSET) {
+ return firstWindowIndexInChild + previousWindowIndexInChild;
+ } else {
+ if (firstWindowIndexInChild > 0) {
+ return firstWindowIndexInChild - 1;
+ } else if (repeatMode == Player.REPEAT_MODE_ALL) {
+ return getWindowCount() - 1;
+ } else {
+ return C.INDEX_UNSET;
+ }
+ }
+ }
+
+ @Override
+ public final Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ int childIndex = getChildIndexByWindowIndex(windowIndex);
+ int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
+ int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
+ getTimelineByChildIndex(childIndex).getWindow(windowIndex - firstWindowIndexInChild, window,
+ setIds, defaultPositionProjectionUs);
+ window.firstPeriodIndex += firstPeriodIndexInChild;
+ window.lastPeriodIndex += firstPeriodIndexInChild;
+ return window;
+ }
+
+ @Override
+ public final Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ int childIndex = getChildIndexByPeriodIndex(periodIndex);
+ int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
+ int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
+ getTimelineByChildIndex(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period,
+ setIds);
+ period.windowIndex += firstWindowIndexInChild;
+ if (setIds) {
+ period.uid = Pair.create(getChildUidByChildIndex(childIndex), period.uid);
+ }
+ return period;
+ }
+
+ @Override
+ public final int getIndexOfPeriod(Object uid) {
+ if (!(uid instanceof Pair)) {
+ return C.INDEX_UNSET;
+ }
+ Pair, ?> childUidAndPeriodUid = (Pair, ?>) uid;
+ Object childUid = childUidAndPeriodUid.first;
+ Object periodUid = childUidAndPeriodUid.second;
+ int childIndex = getChildIndexByChildUid(childUid);
+ if (childIndex == C.INDEX_UNSET) {
+ return C.INDEX_UNSET;
+ }
+ int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid);
+ return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET
+ : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild;
+ }
+
+ /**
+ * Returns the index of the child timeline containing the given period index.
+ *
+ * @param periodIndex A valid period index within the bounds of the timeline.
+ */
+ protected abstract int getChildIndexByPeriodIndex(int periodIndex);
+
+ /**
+ * Returns the index of the child timeline containing the given window index.
+ *
+ * @param windowIndex A valid window index within the bounds of the timeline.
+ */
+ protected abstract int getChildIndexByWindowIndex(int windowIndex);
+
+ /**
+ * Returns the index of the child timeline with the given UID or {@link C#INDEX_UNSET} if not
+ * found.
+ *
+ * @param childUid A child UID.
+ * @return Index of child timeline or {@link C#INDEX_UNSET} if UID was not found.
+ */
+ protected abstract int getChildIndexByChildUid(Object childUid);
+
+ /**
+ * Returns the child timeline for the child with the given index.
+ *
+ * @param childIndex A valid child index within the bounds of the timeline.
+ */
+ protected abstract Timeline getTimelineByChildIndex(int childIndex);
+
+ /**
+ * Returns the first period index belonging to the child timeline with the given index.
+ *
+ * @param childIndex A valid child index within the bounds of the timeline.
+ */
+ protected abstract int getFirstPeriodIndexByChildIndex(int childIndex);
+
+ /**
+ * Returns the first window index belonging to the child timeline with the given index.
+ *
+ * @param childIndex A valid child index within the bounds of the timeline.
+ */
+ protected abstract int getFirstWindowIndexByChildIndex(int childIndex);
+
+ /**
+ * Returns the UID of the child timeline with the given index.
+ *
+ * @param childIndex A valid child index within the bounds of the timeline.
+ */
+ protected abstract Object getChildUidByChildIndex(int childIndex);
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
index e14930c7b8..12f58d9a21 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
@@ -16,10 +16,12 @@
package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
/**
@@ -45,14 +47,20 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
*
* The clipping start/end positions must be specified by calling {@link #setClipping(long, long)}
* on the playback thread before preparation completes.
+ *
+ * If the start point is guaranteed to be a key frame, pass {@code false} to {@code
+ * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when the period is
+ * first read from.
*
* @param mediaPeriod The media period to clip.
+ * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled.
*/
- public ClippingMediaPeriod(MediaPeriod mediaPeriod) {
+ public ClippingMediaPeriod(MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity) {
this.mediaPeriod = mediaPeriod;
startUs = C.TIME_UNSET;
endUs = C.TIME_UNSET;
sampleStreams = new ClippingSampleStream[0];
+ pendingInitialDiscontinuity = enableInitialDiscontinuity;
}
/**
@@ -68,9 +76,9 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
@Override
- public void prepare(MediaPeriod.Callback callback) {
+ public void prepare(MediaPeriod.Callback callback, long positionUs) {
this.callback = callback;
- mediaPeriod.prepare(this);
+ mediaPeriod.prepare(this, startUs + positionUs);
}
@Override
@@ -94,6 +102,9 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags,
internalStreams, streamResetFlags, positionUs + startUs);
+ if (pendingInitialDiscontinuity) {
+ pendingInitialDiscontinuity = startUs != 0 && shouldKeepInitialDiscontinuity(selections);
+ }
Assertions.checkState(enablePositionUs == positionUs + startUs
|| (enablePositionUs >= startUs
&& (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs)));
@@ -179,6 +190,15 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET);
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ callback.onContinueLoadingRequested(this);
+ }
+
+ private static boolean shouldKeepInitialDiscontinuity(TrackSelection[] selections) {
// If the clipping start position is non-zero, the clipping sample streams will adjust
// timestamps on buffers they read from the unclipped sample streams. These adjusted buffer
// timestamps can be negative, because sample streams provide buffers starting at a key-frame,
@@ -186,13 +206,17 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
// negative timestamp, its offset timestamp can jump backwards compared to the last timestamp
// read in the previous period. Renderer implementations may not allow this, so we signal a
// discontinuity which resets the renderers before they read the clipping sample stream.
- pendingInitialDiscontinuity = startUs != 0;
- callback.onPrepared(this);
- }
-
- @Override
- public void onContinueLoadingRequested(MediaPeriod source) {
- callback.onContinueLoadingRequested(this);
+ // However, for audio-only track selections we assume to have random access seek behaviour and
+ // do not need an initial discontinuity to reset the renderer.
+ for (TrackSelection trackSelection : selections) {
+ if (trackSelection != null) {
+ Format selectedFormat = trackSelection.getSelectedFormat();
+ if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) {
+ return true;
+ }
+ }
+ }
+ return false;
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
index be15a07726..32c4eb6c73 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
@@ -33,6 +34,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
private final MediaSource mediaSource;
private final long startUs;
private final long endUs;
+ private final boolean enableInitialDiscontinuity;
private final ArrayList mediaPeriods;
private MediaSource.Listener sourceListener;
@@ -49,10 +51,31 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
* from the specified start point up to the end of the source.
*/
public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) {
+ this(mediaSource, startPositionUs, endPositionUs, true);
+ }
+
+ /**
+ * Creates a new clipping source that wraps the specified source.
+ *
+ * If the start point is guaranteed to be a key frame, pass {@code false} to
+ * {@code enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period
+ * is first read from.
+ *
+ * @param mediaSource The single-period, non-dynamic source to wrap.
+ * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to
+ * start providing samples, in microseconds.
+ * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop
+ * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples
+ * from the specified start point up to the end of the source.
+ * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled.
+ */
+ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs,
+ boolean enableInitialDiscontinuity) {
Assertions.checkArgument(startPositionUs >= 0);
this.mediaSource = Assertions.checkNotNull(mediaSource);
startUs = startPositionUs;
endUs = endPositionUs;
+ this.enableInitialDiscontinuity = enableInitialDiscontinuity;
mediaPeriods = new ArrayList<>();
}
@@ -68,9 +91,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod(
- mediaSource.createPeriod(index, allocator, startUs + positionUs));
+ mediaSource.createPeriod(id, allocator), enableInitialDiscontinuity);
mediaPeriods.add(mediaPeriod);
mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs);
return mediaPeriod;
@@ -142,6 +165,16 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
return 1;
}
+ @Override
+ public int getNextWindowIndex(int windowIndex, @RepeatMode int repeatMode) {
+ return timeline.getNextWindowIndex(windowIndex, repeatMode);
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, @RepeatMode int repeatMode) {
+ return timeline.getPreviousWindowIndex(windowIndex, repeatMode);
+ }
+
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
index c28a016581..343d4f0bbe 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
@@ -29,7 +29,19 @@ public final class CompositeSequenceableLoader implements SequenceableLoader {
}
@Override
- public long getNextLoadPositionUs() {
+ public final long getBufferedPositionUs() {
+ long bufferedPositionUs = Long.MAX_VALUE;
+ for (SequenceableLoader loader : loaders) {
+ long loaderBufferedPositionUs = loader.getBufferedPositionUs();
+ if (loaderBufferedPositionUs != C.TIME_END_OF_SOURCE) {
+ bufferedPositionUs = Math.min(bufferedPositionUs, loaderBufferedPositionUs);
+ }
+ }
+ return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+ }
+
+ @Override
+ public final long getNextLoadPositionUs() {
long nextLoadPositionUs = Long.MAX_VALUE;
for (SequenceableLoader loader : loaders) {
long loaderNextLoadPositionUs = loader.getNextLoadPositionUs();
@@ -41,7 +53,7 @@ public final class CompositeSequenceableLoader implements SequenceableLoader {
}
@Override
- public boolean continueLoading(long positionUs) {
+ public final boolean continueLoading(long positionUs) {
boolean madeProgress = false;
boolean madeProgressThisIteration;
do {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
index 9fc499f251..5d2bbcc33e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -15,9 +15,9 @@
*/
package com.google.android.exoplayer2.source;
-import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
@@ -38,6 +38,7 @@ public final class ConcatenatingMediaSource implements MediaSource {
private final Object[] manifests;
private final Map sourceIndexByMediaPeriod;
private final boolean[] duplicateFlags;
+ private final boolean isRepeatOneAtomic;
private Listener listener;
private ConcatenatedTimeline timeline;
@@ -47,7 +48,21 @@ public final class ConcatenatingMediaSource implements MediaSource {
* {@link MediaSource} instance to be present more than once in the array.
*/
public ConcatenatingMediaSource(MediaSource... mediaSources) {
+ this(false, mediaSources);
+ }
+
+ /**
+ * @param isRepeatOneAtomic Whether the concatenated media source shall be treated as atomic
+ * (i.e., repeated in its entirety) when repeat mode is set to {@code Player.REPEAT_MODE_ONE}.
+ * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same
+ * {@link MediaSource} instance to be present more than once in the array.
+ */
+ public ConcatenatingMediaSource(boolean isRepeatOneAtomic, MediaSource... mediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ Assertions.checkNotNull(mediaSource);
+ }
this.mediaSources = mediaSources;
+ this.isRepeatOneAtomic = isRepeatOneAtomic;
timelines = new Timeline[mediaSources.length];
manifests = new Object[mediaSources.length];
sourceIndexByMediaPeriod = new HashMap<>();
@@ -80,11 +95,11 @@ public final class ConcatenatingMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- int sourceIndex = timeline.getSourceIndexForPeriod(index);
- int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex);
- MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator,
- positionUs);
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex);
+ MediaPeriodId periodIdInSource =
+ new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex));
+ MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator);
sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex);
return mediaPeriod;
}
@@ -123,7 +138,7 @@ public final class ConcatenatingMediaSource implements MediaSource {
return;
}
}
- timeline = new ConcatenatedTimeline(timelines.clone());
+ timeline = new ConcatenatedTimeline(timelines.clone(), isRepeatOneAtomic);
listener.onSourceInfoRefreshed(timeline, manifests.clone());
}
@@ -144,13 +159,15 @@ public final class ConcatenatingMediaSource implements MediaSource {
/**
* A {@link Timeline} that is the concatenation of one or more {@link Timeline}s.
*/
- private static final class ConcatenatedTimeline extends Timeline {
+ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {
private final Timeline[] timelines;
private final int[] sourcePeriodOffsets;
private final int[] sourceWindowOffsets;
+ private final boolean isRepeatOneAtomic;
- public ConcatenatedTimeline(Timeline[] timelines) {
+ public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) {
+ super(timelines.length);
int[] sourcePeriodOffsets = new int[timelines.length];
int[] sourceWindowOffsets = new int[timelines.length];
long periodCount = 0;
@@ -167,6 +184,7 @@ public final class ConcatenatingMediaSource implements MediaSource {
this.timelines = timelines;
this.sourcePeriodOffsets = sourcePeriodOffsets;
this.sourceWindowOffsets = sourceWindowOffsets;
+ this.isRepeatOneAtomic = isRepeatOneAtomic;
}
@Override
@@ -174,70 +192,63 @@ public final class ConcatenatingMediaSource implements MediaSource {
return sourceWindowOffsets[sourceWindowOffsets.length - 1];
}
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- int sourceIndex = getSourceIndexForWindow(windowIndex);
- int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
- int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
- timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds,
- defaultPositionProjectionUs);
- window.firstPeriodIndex += firstPeriodIndexInSource;
- window.lastPeriodIndex += firstPeriodIndexInSource;
- return window;
- }
-
@Override
public int getPeriodCount() {
return sourcePeriodOffsets[sourcePeriodOffsets.length - 1];
}
@Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- int sourceIndex = getSourceIndexForPeriod(periodIndex);
- int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
- int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
- timelines[sourceIndex].getPeriod(periodIndex - firstPeriodIndexInSource, period, setIds);
- period.windowIndex += firstWindowIndexInSource;
- if (setIds) {
- period.uid = Pair.create(sourceIndex, period.uid);
+ public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) {
+ repeatMode = Player.REPEAT_MODE_ALL;
}
- return period;
+ return super.getNextWindowIndex(windowIndex, repeatMode);
}
@Override
- public int getIndexOfPeriod(Object uid) {
- if (!(uid instanceof Pair)) {
- return C.INDEX_UNSET;
+ public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) {
+ repeatMode = Player.REPEAT_MODE_ALL;
}
- Pair, ?> sourceIndexAndPeriodId = (Pair, ?>) uid;
- if (!(sourceIndexAndPeriodId.first instanceof Integer)) {
- return C.INDEX_UNSET;
- }
- int sourceIndex = (Integer) sourceIndexAndPeriodId.first;
- Object periodId = sourceIndexAndPeriodId.second;
- if (sourceIndex < 0 || sourceIndex >= timelines.length) {
- return C.INDEX_UNSET;
- }
- int periodIndexInSource = timelines[sourceIndex].getIndexOfPeriod(periodId);
- return periodIndexInSource == C.INDEX_UNSET ? C.INDEX_UNSET
- : getFirstPeriodIndexInSource(sourceIndex) + periodIndexInSource;
+ return super.getPreviousWindowIndex(windowIndex, repeatMode);
}
- private int getSourceIndexForPeriod(int periodIndex) {
+ @Override
+ protected int getChildIndexByPeriodIndex(int periodIndex) {
return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1;
}
- private int getFirstPeriodIndexInSource(int sourceIndex) {
- return sourceIndex == 0 ? 0 : sourcePeriodOffsets[sourceIndex - 1];
- }
-
- private int getSourceIndexForWindow(int windowIndex) {
+ @Override
+ protected int getChildIndexByWindowIndex(int windowIndex) {
return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1;
}
- private int getFirstWindowIndexInSource(int sourceIndex) {
- return sourceIndex == 0 ? 0 : sourceWindowOffsets[sourceIndex - 1];
+ @Override
+ protected int getChildIndexByChildUid(Object childUid) {
+ if (!(childUid instanceof Integer)) {
+ return C.INDEX_UNSET;
+ }
+ return (Integer) childUid;
+ }
+
+ @Override
+ protected Timeline getTimelineByChildIndex(int childIndex) {
+ return timelines[childIndex];
+ }
+
+ @Override
+ protected int getFirstPeriodIndexByChildIndex(int childIndex) {
+ return childIndex == 0 ? 0 : sourcePeriodOffsets[childIndex - 1];
+ }
+
+ @Override
+ protected int getFirstWindowIndexByChildIndex(int childIndex) {
+ return childIndex == 0 ? 0 : sourceWindowOffsets[childIndex - 1];
+ }
+
+ @Override
+ protected Object getChildUidByChildIndex(int childIndex) {
+ return childIndex;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
new file mode 100644
index 0000000000..b00732e839
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
@@ -0,0 +1,639 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.util.Pair;
+import android.util.SparseIntArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
+ * during playback. Access to this class is thread-safe.
+ */
+public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent {
+
+ private static final int MSG_ADD = 0;
+ private static final int MSG_ADD_MULTIPLE = 1;
+ private static final int MSG_REMOVE = 2;
+ private static final int MSG_MOVE = 3;
+
+ // Accessed on the app thread.
+ private final List mediaSourcesPublic;
+
+ // Accessed on the playback thread.
+ private final List mediaSourceHolders;
+ private final MediaSourceHolder query;
+ private final Map mediaSourceByMediaPeriod;
+ private final List deferredMediaPeriods;
+
+ private ExoPlayer player;
+ private Listener listener;
+ private boolean preventListenerNotification;
+ private int windowCount;
+ private int periodCount;
+
+ public DynamicConcatenatingMediaSource() {
+ this.mediaSourceByMediaPeriod = new IdentityHashMap<>();
+ this.mediaSourcesPublic = new ArrayList<>();
+ this.mediaSourceHolders = new ArrayList<>();
+ this.deferredMediaPeriods = new ArrayList<>(1);
+ this.query = new MediaSourceHolder(null, null, -1, -1, -1);
+ }
+
+ /**
+ * Appends a {@link MediaSource} to the playlist.
+ *
+ * @param mediaSource The {@link MediaSource} to be added to the list.
+ */
+ public synchronized void addMediaSource(MediaSource mediaSource) {
+ addMediaSource(mediaSourcesPublic.size(), mediaSource);
+ }
+
+ /**
+ * Adds a {@link MediaSource} to the playlist.
+ *
+ * @param index The index at which the new {@link MediaSource} will be inserted. This index must
+ * be in the range of 0 <= index <= {@link #getSize()}.
+ * @param mediaSource The {@link MediaSource} to be added to the list.
+ */
+ public synchronized void addMediaSource(int index, MediaSource mediaSource) {
+ Assertions.checkNotNull(mediaSource);
+ Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource));
+ mediaSourcesPublic.add(index, mediaSource);
+ if (player != null) {
+ player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, Pair.create(index, mediaSource)));
+ }
+ }
+
+ /**
+ * Appends multiple {@link MediaSource}s to the playlist.
+ *
+ * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
+ * sources are added in the order in which they appear in this collection.
+ */
+ public synchronized void addMediaSources(Collection mediaSources) {
+ addMediaSources(mediaSourcesPublic.size(), mediaSources);
+ }
+
+ /**
+ * Adds multiple {@link MediaSource}s to the playlist.
+ *
+ * @param index The index at which the new {@link MediaSource}s will be inserted. This index must
+ * be in the range of 0 <= index <= {@link #getSize()}.
+ * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
+ * sources are added in the order in which they appear in this collection.
+ */
+ public synchronized void addMediaSources(int index, Collection mediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ Assertions.checkNotNull(mediaSource);
+ Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource));
+ }
+ mediaSourcesPublic.addAll(index, mediaSources);
+ if (player != null && !mediaSources.isEmpty()) {
+ player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE,
+ Pair.create(index, mediaSources)));
+ }
+ }
+
+ /**
+ * Removes a {@link MediaSource} from the playlist.
+ *
+ * @param index The index at which the media source will be removed. This index must be in the
+ * range of 0 <= index < {@link #getSize()}.
+ */
+ public synchronized void removeMediaSource(int index) {
+ mediaSourcesPublic.remove(index);
+ if (player != null) {
+ player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, index));
+ }
+ }
+
+ /**
+ * Moves an existing {@link MediaSource} within the playlist.
+ *
+ * @param currentIndex The current index of the media source in the playlist. This index must be
+ * in the range of 0 <= index < {@link #getSize()}.
+ * @param newIndex The target index of the media source in the playlist. This index must be in the
+ * range of 0 <= index < {@link #getSize()}.
+ */
+ public synchronized void moveMediaSource(int currentIndex, int newIndex) {
+ if (currentIndex == newIndex) {
+ return;
+ }
+ mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
+ if (player != null) {
+ player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE,
+ Pair.create(currentIndex, newIndex)));
+ }
+ }
+
+ /**
+ * Returns the number of media sources in the playlist.
+ */
+ public synchronized int getSize() {
+ return mediaSourcesPublic.size();
+ }
+
+ /**
+ * Returns the {@link MediaSource} at a specified index.
+ *
+ * @param index A index in the range of 0 <= index <= {@link #getSize()}.
+ * @return The {@link MediaSource} at this index.
+ */
+ public synchronized MediaSource getMediaSource(int index) {
+ return mediaSourcesPublic.get(index);
+ }
+
+ @Override
+ public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource,
+ Listener listener) {
+ this.player = player;
+ this.listener = listener;
+ preventListenerNotification = true;
+ addMediaSourcesInternal(0, mediaSourcesPublic);
+ preventListenerNotification = false;
+ maybeNotifyListener();
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
+ mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex);
+ MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex);
+ MediaPeriodId idInSource = new MediaPeriodId(id.periodIndex - holder.firstPeriodIndexInChild);
+ MediaPeriod mediaPeriod;
+ if (!holder.isPrepared) {
+ mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator);
+ deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod);
+ } else {
+ mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator);
+ }
+ mediaSourceByMediaPeriod.put(mediaPeriod, holder.mediaSource);
+ return mediaPeriod;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ MediaSource mediaSource = mediaSourceByMediaPeriod.get(mediaPeriod);
+ mediaSourceByMediaPeriod.remove(mediaPeriod);
+ if (mediaPeriod instanceof DeferredMediaPeriod) {
+ deferredMediaPeriods.remove(mediaPeriod);
+ ((DeferredMediaPeriod) mediaPeriod).releasePeriod();
+ } else {
+ mediaSource.releasePeriod(mediaPeriod);
+ }
+ }
+
+ @Override
+ public void releaseSource() {
+ for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
+ mediaSourceHolder.mediaSource.releaseSource();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ preventListenerNotification = true;
+ switch (messageType) {
+ case MSG_ADD: {
+ Pair messageData = (Pair) message;
+ addMediaSourceInternal(messageData.first, messageData.second);
+ break;
+ }
+ case MSG_ADD_MULTIPLE: {
+ Pair> messageData =
+ (Pair>) message;
+ addMediaSourcesInternal(messageData.first, messageData.second);
+ break;
+ }
+ case MSG_REMOVE: {
+ removeMediaSourceInternal((Integer) message);
+ break;
+ }
+ case MSG_MOVE: {
+ Pair messageData = (Pair) message;
+ moveMediaSourceInternal(messageData.first, messageData.second);
+ break;
+ }
+ default: {
+ throw new IllegalStateException();
+ }
+ }
+ preventListenerNotification = false;
+ maybeNotifyListener();
+ }
+
+ private void maybeNotifyListener() {
+ if (!preventListenerNotification) {
+ listener.onSourceInfoRefreshed(
+ new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount), null);
+ }
+ }
+
+ private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) {
+ final MediaSourceHolder newMediaSourceHolder;
+ Object newUid = System.identityHashCode(newMediaSource);
+ DeferredTimeline newTimeline = new DeferredTimeline();
+ if (newIndex > 0) {
+ MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1);
+ newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline,
+ previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(),
+ previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount(),
+ newUid);
+ } else {
+ newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, newUid);
+ }
+ correctOffsets(newIndex, newTimeline.getWindowCount(), newTimeline.getPeriodCount());
+ mediaSourceHolders.add(newIndex, newMediaSourceHolder);
+ newMediaSourceHolder.mediaSource.prepareSource(player, false, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
+ updateMediaSourceInternal(newMediaSourceHolder, newTimeline);
+ }
+ });
+ }
+
+ private void addMediaSourcesInternal(int index, Collection mediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ addMediaSourceInternal(index++, mediaSource);
+ }
+ }
+
+ private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) {
+ if (mediaSourceHolder == null) {
+ throw new IllegalArgumentException();
+ }
+ DeferredTimeline deferredTimeline = mediaSourceHolder.timeline;
+ if (deferredTimeline.getTimeline() == timeline) {
+ return;
+ }
+ int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount();
+ int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount();
+ if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) {
+ int index = findMediaSourceHolderByPeriodIndex(mediaSourceHolder.firstPeriodIndexInChild);
+ correctOffsets(index + 1, windowOffsetUpdate, periodOffsetUpdate);
+ }
+ mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline);
+ if (!mediaSourceHolder.isPrepared) {
+ for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) {
+ if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) {
+ deferredMediaPeriods.get(i).createPeriod();
+ deferredMediaPeriods.remove(i);
+ }
+ }
+ }
+ mediaSourceHolder.isPrepared = true;
+ maybeNotifyListener();
+ }
+
+ private void removeMediaSourceInternal(int index) {
+ MediaSourceHolder holder = mediaSourceHolders.get(index);
+ mediaSourceHolders.remove(index);
+ Timeline oldTimeline = holder.timeline;
+ correctOffsets(index, -oldTimeline.getWindowCount(), -oldTimeline.getPeriodCount());
+ holder.mediaSource.releaseSource();
+ }
+
+ private void moveMediaSourceInternal(int currentIndex, int newIndex) {
+ int startIndex = Math.min(currentIndex, newIndex);
+ int endIndex = Math.max(currentIndex, newIndex);
+ int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
+ int periodOffset = mediaSourceHolders.get(startIndex).firstPeriodIndexInChild;
+ mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex));
+ for (int i = startIndex; i <= endIndex; i++) {
+ MediaSourceHolder holder = mediaSourceHolders.get(i);
+ holder.firstWindowIndexInChild = windowOffset;
+ holder.firstPeriodIndexInChild = periodOffset;
+ windowOffset += holder.timeline.getWindowCount();
+ periodOffset += holder.timeline.getPeriodCount();
+ }
+ }
+
+ private void correctOffsets(int startIndex, int windowOffsetUpdate, int periodOffsetUpdate) {
+ windowCount += windowOffsetUpdate;
+ periodCount += periodOffsetUpdate;
+ for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
+ mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate;
+ mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate;
+ }
+ }
+
+ private int findMediaSourceHolderByPeriodIndex(int periodIndex) {
+ query.firstPeriodIndexInChild = periodIndex;
+ int index = Collections.binarySearch(mediaSourceHolders, query);
+ return index >= 0 ? index : -index - 2;
+ }
+
+ private static final class MediaSourceHolder implements Comparable {
+
+ public final MediaSource mediaSource;
+ public final Object uid;
+
+ public DeferredTimeline timeline;
+ public int firstWindowIndexInChild;
+ public int firstPeriodIndexInChild;
+ public boolean isPrepared;
+
+ public MediaSourceHolder(MediaSource mediaSource, DeferredTimeline timeline, int window,
+ int period, Object uid) {
+ this.mediaSource = mediaSource;
+ this.timeline = timeline;
+ this.firstWindowIndexInChild = window;
+ this.firstPeriodIndexInChild = period;
+ this.uid = uid;
+ }
+
+ @Override
+ public int compareTo(MediaSourceHolder other) {
+ return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild;
+ }
+ }
+
+ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {
+
+ private final int windowCount;
+ private final int periodCount;
+ private final int[] firstPeriodInChildIndices;
+ private final int[] firstWindowInChildIndices;
+ private final Timeline[] timelines;
+ private final int[] uids;
+ private final SparseIntArray childIndexByUid;
+
+ public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount,
+ int periodCount) {
+ super(mediaSourceHolders.size());
+ this.windowCount = windowCount;
+ this.periodCount = periodCount;
+ int childCount = mediaSourceHolders.size();
+ firstPeriodInChildIndices = new int[childCount];
+ firstWindowInChildIndices = new int[childCount];
+ timelines = new Timeline[childCount];
+ uids = new int[childCount];
+ childIndexByUid = new SparseIntArray();
+ int index = 0;
+ for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
+ timelines[index] = mediaSourceHolder.timeline;
+ firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild;
+ firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild;
+ uids[index] = (int) mediaSourceHolder.uid;
+ childIndexByUid.put(uids[index], index++);
+ }
+ }
+
+ @Override
+ protected int getChildIndexByPeriodIndex(int periodIndex) {
+ return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false);
+ }
+
+ @Override
+ protected int getChildIndexByWindowIndex(int windowIndex) {
+ return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false);
+ }
+
+ @Override
+ protected int getChildIndexByChildUid(Object childUid) {
+ if (!(childUid instanceof Integer)) {
+ return C.INDEX_UNSET;
+ }
+ int index = childIndexByUid.get((int) childUid, -1);
+ return index == -1 ? C.INDEX_UNSET : index;
+ }
+
+ @Override
+ protected Timeline getTimelineByChildIndex(int childIndex) {
+ return timelines[childIndex];
+ }
+
+ @Override
+ protected int getFirstPeriodIndexByChildIndex(int childIndex) {
+ return firstPeriodInChildIndices[childIndex];
+ }
+
+ @Override
+ protected int getFirstWindowIndexByChildIndex(int childIndex) {
+ return firstWindowInChildIndices[childIndex];
+ }
+
+ @Override
+ protected Object getChildUidByChildIndex(int childIndex) {
+ return uids[childIndex];
+ }
+
+ @Override
+ public int getWindowCount() {
+ return windowCount;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return periodCount;
+ }
+
+ }
+
+ private static final class DeferredTimeline extends Timeline {
+
+ private static final Object DUMMY_ID = new Object();
+ private static final Period period = new Period();
+
+ private final Timeline timeline;
+ private final Object replacedID;
+
+ public DeferredTimeline() {
+ timeline = null;
+ replacedID = null;
+ }
+
+ private DeferredTimeline(Timeline timeline, Object replacedID) {
+ this.timeline = timeline;
+ this.replacedID = replacedID;
+ }
+
+ public DeferredTimeline cloneWithNewTimeline(Timeline timeline) {
+ return new DeferredTimeline(timeline, replacedID == null && timeline.getPeriodCount() > 0
+ ? timeline.getPeriod(0, period, true).uid : replacedID);
+ }
+
+ public Timeline getTimeline() {
+ return timeline;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return timeline == null ? 1 : timeline.getWindowCount();
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ return timeline == null
+ // Dynamic window to indicate pending timeline updates.
+ ? window.set(setIds ? DUMMY_ID : null, C.TIME_UNSET, C.TIME_UNSET, false, true, 0,
+ C.TIME_UNSET, 0, 0, 0)
+ : timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return timeline == null ? 1 : timeline.getPeriodCount();
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ if (timeline == null) {
+ return period.set(setIds ? DUMMY_ID : null, setIds ? DUMMY_ID : null, 0, C.TIME_UNSET,
+ C.TIME_UNSET);
+ }
+ timeline.getPeriod(periodIndex, period, setIds);
+ if (period.uid == replacedID) {
+ period.uid = DUMMY_ID;
+ }
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return timeline == null ? (uid == DUMMY_ID ? 0 : C.INDEX_UNSET)
+ : timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedID : uid);
+ }
+
+ }
+
+ private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ public final MediaSource mediaSource;
+
+ private final MediaPeriodId id;
+ private final Allocator allocator;
+
+ private MediaPeriod mediaPeriod;
+ private Callback callback;
+ private long preparePositionUs;
+
+ public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
+ this.id = id;
+ this.allocator = allocator;
+ this.mediaSource = mediaSource;
+ }
+
+ public void createPeriod() {
+ mediaPeriod = mediaSource.createPeriod(id, allocator);
+ if (callback != null) {
+ mediaPeriod.prepare(this, preparePositionUs);
+ }
+ }
+
+ public void releasePeriod() {
+ if (mediaPeriod != null) {
+ mediaSource.releasePeriod(mediaPeriod);
+ }
+ }
+
+ @Override
+ public void prepare(Callback callback, long preparePositionUs) {
+ this.callback = callback;
+ this.preparePositionUs = preparePositionUs;
+ if (mediaPeriod != null) {
+ mediaPeriod.prepare(this, preparePositionUs);
+ }
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ if (mediaPeriod != null) {
+ mediaPeriod.maybeThrowPrepareError();
+ } else {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return mediaPeriod.getTrackGroups();
+ }
+
+ @Override
+ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+ return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
+ positionUs);
+ }
+
+ @Override
+ public void discardBuffer(long positionUs) {
+ mediaPeriod.discardBuffer(positionUs);
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ return mediaPeriod.readDiscontinuity();
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return mediaPeriod.getBufferedPositionUs();
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ return mediaPeriod.seekToUs(positionUs);
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return mediaPeriod.getNextLoadPositionUs();
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ callback.onContinueLoadingRequested(this);
+ }
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ callback.onPrepared(this);
+ }
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
index f247d4dd37..e7273f834b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
@@ -17,20 +17,18 @@ package com.google.android.exoplayer2.source;
import android.net.Uri;
import android.os.Handler;
-import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
-import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
-import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
@@ -43,12 +41,29 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
+import java.util.Arrays;
/**
* A {@link MediaPeriod} that extracts data using an {@link Extractor}.
*/
/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput,
- Loader.Callback, UpstreamFormatChangedListener {
+ Loader.Callback, Loader.ReleaseCallback,
+ UpstreamFormatChangedListener {
+
+ /**
+ * Listener for information about the period.
+ */
+ interface Listener {
+
+ /**
+ * Called when the duration or ability to seek within the period changes.
+ *
+ * @param durationUs The duration of the period, or {@link C#TIME_UNSET}.
+ * @param isSeekable Whether the period is seekable.
+ */
+ void onSourceInfoRefreshed(long durationUs, boolean isSeekable);
+
+ }
/**
* When the source's duration is unknown, it is calculated by adding this value to the largest
@@ -61,24 +76,26 @@ import java.io.IOException;
private final int minLoadableRetryCount;
private final Handler eventHandler;
private final ExtractorMediaSource.EventListener eventListener;
- private final MediaSource.Listener sourceListener;
+ private final Listener listener;
private final Allocator allocator;
private final String customCacheKey;
+ private final long continueLoadingCheckIntervalBytes;
private final Loader loader;
private final ExtractorHolder extractorHolder;
private final ConditionVariable loadCondition;
private final Runnable maybeFinishPrepareRunnable;
private final Runnable onContinueLoadingRequestedRunnable;
private final Handler handler;
- private final SparseArray sampleQueues;
private Callback callback;
private SeekMap seekMap;
- private boolean tracksBuilt;
+ private SampleQueue[] sampleQueues;
+ private int[] sampleQueueTrackIds;
+ private boolean sampleQueuesBuilt;
private boolean prepared;
private boolean seenFirstTrackSelection;
- private boolean notifyReset;
+ private boolean notifyDiscontinuity;
private int enabledTrackCount;
private TrackGroupArray tracks;
private long durationUs;
@@ -101,23 +118,26 @@ import java.io.IOException;
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
* @param eventHandler A handler for events. May be null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
- * @param sourceListener A listener to notify when the timeline has been loaded.
+ * @param listener A listener to notify when information about the period changes.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
+ * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
+ * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
*/
public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors,
int minLoadableRetryCount, Handler eventHandler,
- ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener,
- Allocator allocator, String customCacheKey) {
+ ExtractorMediaSource.EventListener eventListener, Listener listener,
+ Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) {
this.uri = uri;
this.dataSource = dataSource;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
- this.sourceListener = sourceListener;
+ this.listener = listener;
this.allocator = allocator;
this.customCacheKey = customCacheKey;
+ this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
loader = new Loader("Loader:ExtractorMediaPeriod");
extractorHolder = new ExtractorHolder(extractors, this);
loadCondition = new ConditionVariable();
@@ -136,30 +156,35 @@ import java.io.IOException;
}
};
handler = new Handler();
-
+ sampleQueueTrackIds = new int[0];
+ sampleQueues = new SampleQueue[0];
pendingResetPositionUs = C.TIME_UNSET;
- sampleQueues = new SparseArray<>();
length = C.LENGTH_UNSET;
}
public void release() {
- final ExtractorHolder extractorHolder = this.extractorHolder;
- loader.release(new Runnable() {
- @Override
- public void run() {
- extractorHolder.release();
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- sampleQueues.valueAt(i).disable();
- }
+ boolean releasedSynchronously = loader.release(this);
+ if (prepared && !releasedSynchronously) {
+ // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise
+ // sampleQueues may still be being modified by the loading thread.
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.discardToEnd();
}
- });
+ }
handler.removeCallbacksAndMessages(null);
released = true;
}
@Override
- public void prepare(Callback callback) {
+ public void onLoaderReleased() {
+ extractorHolder.release();
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ }
+
+ @Override
+ public void prepare(Callback callback, long positionUs) {
this.callback = callback;
loadCondition.open();
startLoading();
@@ -179,19 +204,21 @@ import java.io.IOException;
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
Assertions.checkState(prepared);
- // Disable old tracks.
+ int oldEnabledTrackCount = enabledTrackCount;
+ // Deselect old tracks.
for (int i = 0; i < selections.length; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
int track = ((SampleStreamImpl) streams[i]).track;
Assertions.checkState(trackEnabledStates[track]);
enabledTrackCount--;
trackEnabledStates[track] = false;
- sampleQueues.valueAt(track).disable();
streams[i] = null;
}
}
- // Enable new tracks.
- boolean selectedNewTracks = false;
+ // We'll always need to seek if this is a first selection to a non-zero position, or if we're
+ // making a selection having previously disabled all tracks.
+ boolean seekRequired = seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0;
+ // Select new tracks.
for (int i = 0; i < selections.length; i++) {
if (streams[i] == null && selections[i] != null) {
TrackSelection selection = selections[i];
@@ -203,25 +230,33 @@ import java.io.IOException;
trackEnabledStates[track] = true;
streams[i] = new SampleStreamImpl(track);
streamResetFlags[i] = true;
- selectedNewTracks = true;
- }
- }
- if (!seenFirstTrackSelection) {
- // At the time of the first track selection all queues will be enabled, so we need to disable
- // any that are no longer required.
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- if (!trackEnabledStates[i]) {
- sampleQueues.valueAt(i).disable();
+ // If there's still a chance of avoiding a seek, try and seek within the sample queue.
+ if (!seekRequired) {
+ SampleQueue sampleQueue = sampleQueues[track];
+ sampleQueue.rewind();
+ // A seek can be avoided if we're able to advance to the current playback position in the
+ // sample queue, or if we haven't read anything from the queue since the previous seek
+ // (this case is common for sparse tracks such as metadata tracks). In all other cases a
+ // seek is required.
+ seekRequired = !sampleQueue.advanceTo(positionUs, true, true)
+ && sampleQueue.getReadIndex() != 0;
}
}
}
if (enabledTrackCount == 0) {
- notifyReset = false;
+ notifyDiscontinuity = false;
if (loader.isLoading()) {
+ // Discard as much as we can synchronously.
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.discardToEnd();
+ }
loader.cancelLoading();
+ } else {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
}
- } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) {
+ } else if (seekRequired) {
positionUs = seekToUs(positionUs);
// We'll need to reset renderers consuming from all streams due to the seek.
for (int i = 0; i < streams.length; i++) {
@@ -236,7 +271,10 @@ import java.io.IOException;
@Override
public void discardBuffer(long positionUs) {
- // Do nothing.
+ int trackCount = sampleQueues.length;
+ for (int i = 0; i < trackCount; i++) {
+ sampleQueues[i].discardTo(positionUs, false, trackEnabledStates[i]);
+ }
}
@Override
@@ -259,8 +297,8 @@ import java.io.IOException;
@Override
public long readDiscontinuity() {
- if (notifyReset) {
- notifyReset = false;
+ if (notifyDiscontinuity) {
+ notifyDiscontinuity = false;
return lastSeekPositionUs;
}
return C.TIME_UNSET;
@@ -277,11 +315,11 @@ import java.io.IOException;
if (haveAudioVideoTracks) {
// Ignore non-AV tracks, which may be sparse or poorly interleaved.
largestQueuedTimestampUs = Long.MAX_VALUE;
- int trackCount = sampleQueues.size();
+ int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) {
if (trackIsAudioVideoFlags[i]) {
largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
- sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+ sampleQueues[i].getLargestQueuedTimestampUs());
}
}
} else {
@@ -296,34 +334,28 @@ import java.io.IOException;
// Treat all seeks into non-seekable media as being to t=0.
positionUs = seekMap.isSeekable() ? positionUs : 0;
lastSeekPositionUs = positionUs;
- int trackCount = sampleQueues.size();
- // If we're not pending a reset, see if we can seek within the sample queues.
- boolean seekInsideBuffer = !isPendingReset();
- for (int i = 0; seekInsideBuffer && i < trackCount; i++) {
- if (trackEnabledStates[i]) {
- seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs, false);
+ notifyDiscontinuity = false;
+ // If we're not pending a reset, see if we can seek within the buffer.
+ if (!isPendingReset() && seekInsideBufferUs(positionUs)) {
+ return positionUs;
+ }
+ // We were unable to seek within the buffer, so need to reset.
+ pendingResetPositionUs = positionUs;
+ loadingFinished = false;
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ } else {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
}
}
- // If we failed to seek within the sample queues, we need to restart.
- if (!seekInsideBuffer) {
- pendingResetPositionUs = positionUs;
- loadingFinished = false;
- if (loader.isLoading()) {
- loader.cancelLoading();
- } else {
- for (int i = 0; i < trackCount; i++) {
- sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
- }
- }
- }
- notifyReset = false;
return positionUs;
}
// SampleStream methods.
/* package */ boolean isReady(int track) {
- return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty());
+ return loadingFinished || (!isPendingReset() && sampleQueues[track].hasNextSample());
}
/* package */ void maybeThrowError() throws IOException {
@@ -332,20 +364,19 @@ import java.io.IOException;
/* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer,
boolean formatRequired) {
- if (notifyReset || isPendingReset()) {
+ if (notifyDiscontinuity || isPendingReset()) {
return C.RESULT_NOTHING_READ;
}
-
- return sampleQueues.valueAt(track).readData(formatHolder, buffer, formatRequired,
- loadingFinished, lastSeekPositionUs);
+ return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished,
+ lastSeekPositionUs);
}
/* package */ void skipData(int track, long positionUs) {
- DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track);
+ SampleQueue sampleQueue = sampleQueues[track];
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
- sampleQueue.skipAll();
+ sampleQueue.advanceToEnd();
} else {
- sampleQueue.skipToKeyframeBefore(positionUs, true);
+ sampleQueue.advanceTo(positionUs, true, true);
}
}
@@ -360,8 +391,7 @@ import java.io.IOException;
long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0
: largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
- sourceListener.onSourceInfoRefreshed(
- new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null);
+ listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable());
}
callback.onContinueLoadingRequested(this);
}
@@ -369,12 +399,14 @@ import java.io.IOException;
@Override
public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
+ if (released) {
+ return;
+ }
copyLengthFromLoader(loadable);
- if (!released && enabledTrackCount > 0) {
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
- }
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ if (enabledTrackCount > 0) {
callback.onContinueLoadingRequested(this);
}
}
@@ -398,18 +430,24 @@ import java.io.IOException;
@Override
public TrackOutput track(int id, int type) {
- DefaultTrackOutput trackOutput = sampleQueues.get(id);
- if (trackOutput == null) {
- trackOutput = new DefaultTrackOutput(allocator);
- trackOutput.setUpstreamFormatChangeListener(this);
- sampleQueues.put(id, trackOutput);
+ int trackCount = sampleQueues.length;
+ for (int i = 0; i < trackCount; i++) {
+ if (sampleQueueTrackIds[i] == id) {
+ return sampleQueues[i];
+ }
}
+ SampleQueue trackOutput = new SampleQueue(allocator);
+ trackOutput.setUpstreamFormatChangeListener(this);
+ sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
+ sampleQueueTrackIds[trackCount] = id;
+ sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1);
+ sampleQueues[trackCount] = trackOutput;
return trackOutput;
}
@Override
public void endTracks() {
- tracksBuilt = true;
+ sampleQueuesBuilt = true;
handler.post(maybeFinishPrepareRunnable);
}
@@ -429,22 +467,22 @@ import java.io.IOException;
// Internal methods.
private void maybeFinishPrepare() {
- if (released || prepared || seekMap == null || !tracksBuilt) {
+ if (released || prepared || seekMap == null || !sampleQueuesBuilt) {
return;
}
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ if (sampleQueue.getUpstreamFormat() == null) {
return;
}
}
loadCondition.close();
+ int trackCount = sampleQueues.length;
TrackGroup[] trackArray = new TrackGroup[trackCount];
trackIsAudioVideoFlags = new boolean[trackCount];
trackEnabledStates = new boolean[trackCount];
durationUs = seekMap.getDurationUs();
for (int i = 0; i < trackCount; i++) {
- Format trackFormat = sampleQueues.valueAt(i).getUpstreamFormat();
+ Format trackFormat = sampleQueues[i].getUpstreamFormat();
trackArray[i] = new TrackGroup(trackFormat);
String mimeType = trackFormat.sampleMimeType;
boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType);
@@ -453,8 +491,7 @@ import java.io.IOException;
}
tracks = new TrackGroupArray(trackArray);
prepared = true;
- sourceListener.onSourceInfoRefreshed(
- new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null);
+ listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable());
callback.onPrepared(this);
}
@@ -502,30 +539,51 @@ import java.io.IOException;
// previous load finished, so it's necessary to load from the start whenever commencing
// a new load.
lastSeekPositionUs = 0;
- notifyReset = prepared;
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]);
+ notifyDiscontinuity = prepared;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
}
loadable.setLoadPosition(0, 0);
}
}
+ /**
+ * Attempts to seek to the specified position within the sample queues.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @return Whether the in-buffer seek was successful.
+ */
+ private boolean seekInsideBufferUs(long positionUs) {
+ int trackCount = sampleQueues.length;
+ for (int i = 0; i < trackCount; i++) {
+ SampleQueue sampleQueue = sampleQueues[i];
+ sampleQueue.rewind();
+ boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false);
+ // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue
+ // is successful. We ignore whether seeks within non-AV queues are successful in this case, as
+ // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is
+ // successful only if the seek into every queue succeeds.
+ if (!seekInsideQueue && (trackIsAudioVideoFlags[i] || !haveAudioVideoTracks)) {
+ return false;
+ }
+ sampleQueue.discardToRead();
+ }
+ return true;
+ }
+
private int getExtractedSamplesCount() {
int extractedSamplesCount = 0;
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex();
+ for (SampleQueue sampleQueue : sampleQueues) {
+ extractedSamplesCount += sampleQueue.getWriteIndex();
}
return extractedSamplesCount;
}
private long getLargestQueuedTimestampUs() {
long largestQueuedTimestampUs = Long.MIN_VALUE;
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
+ for (SampleQueue sampleQueue : sampleQueues) {
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
- sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+ sampleQueue.getLargestQueuedTimestampUs());
}
return largestQueuedTimestampUs;
}
@@ -585,12 +643,6 @@ import java.io.IOException;
*/
/* package */ final class ExtractingLoadable implements Loadable {
- /**
- * The number of bytes that should be loaded between each each invocation of
- * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
- */
- private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
-
private final Uri uri;
private final DataSource dataSource;
private final ExtractorHolder extractorHolder;
@@ -650,7 +702,7 @@ import java.io.IOException;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
loadCondition.block();
result = extractor.read(input, positionHolder);
- if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) {
+ if (input.getPosition() > position + continueLoadingCheckIntervalBytes) {
position = input.getPosition();
loadCondition.close();
handler.post(onContinueLoadingRequestedRunnable);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
index c560616aae..51e9757165 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
@@ -39,7 +39,7 @@ import java.io.IOException;
*
* Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
*/
-public final class ExtractorMediaSource implements MediaSource, MediaSource.Listener {
+public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener {
/**
* Listener of {@link ExtractorMediaSource} events.
@@ -72,6 +72,12 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
*/
public static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1;
+ /**
+ * The default number of bytes that should be loaded between each each invocation of
+ * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
+ */
+ public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
+
private final Uri uri;
private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory;
@@ -80,10 +86,11 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
private final EventListener eventListener;
private final Timeline.Period period;
private final String customCacheKey;
+ private final int continueLoadingCheckIntervalBytes;
private MediaSource.Listener sourceListener;
- private Timeline timeline;
- private boolean timelineHasDuration;
+ private long timelineDurationUs;
+ private boolean timelineIsSeekable;
/**
* @param uri The {@link Uri} of the media stream.
@@ -96,8 +103,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
*/
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) {
- this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
- eventListener, null);
+ this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null);
}
/**
@@ -115,7 +121,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener,
String customCacheKey) {
this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
- eventListener, customCacheKey);
+ eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES);
}
/**
@@ -129,10 +135,12 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
+ * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
+ * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
*/
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler,
- EventListener eventListener, String customCacheKey) {
+ EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) {
this.uri = uri;
this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory;
@@ -140,14 +148,14 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.customCacheKey = customCacheKey;
+ this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
period = new Timeline.Period();
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
sourceListener = listener;
- timeline = new SinglePeriodTimeline(C.TIME_UNSET, false);
- listener.onSourceInfoRefreshed(timeline, null);
+ notifySourceInfoRefreshed(C.TIME_UNSET, false);
}
@Override
@@ -156,11 +164,11 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- Assertions.checkArgument(index == 0);
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ Assertions.checkArgument(id.periodIndex == 0);
return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
- this, allocator, customCacheKey);
+ this, allocator, customCacheKey, continueLoadingCheckIntervalBytes);
}
@Override
@@ -173,19 +181,27 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
sourceListener = null;
}
- // MediaSource.Listener implementation.
+ // ExtractorMediaPeriod.Listener implementation.
@Override
- public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
- long newTimelineDurationUs = newTimeline.getPeriod(0, period).getDurationUs();
- boolean newTimelineHasDuration = newTimelineDurationUs != C.TIME_UNSET;
- if (timelineHasDuration && !newTimelineHasDuration) {
- // Suppress source info changes that would make the duration unknown when it is already known.
+ public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) {
+ // If we already have the duration from a previous source info refresh, use it.
+ durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs;
+ if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable
+ || (timelineDurationUs != C.TIME_UNSET && durationUs == C.TIME_UNSET)) {
+ // Suppress no-op source info changes.
return;
}
- timeline = newTimeline;
- timelineHasDuration = newTimelineHasDuration;
- sourceListener.onSourceInfoRefreshed(timeline, null);
+ notifySourceInfoRefreshed(durationUs, isSeekable);
+ }
+
+ // Internal methods.
+
+ private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) {
+ timelineDurationUs = durationUs;
+ timelineIsSeekable = isSeekable;
+ sourceListener.onSourceInfoRefreshed(
+ new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
index 8b14c78234..a6e93a92b9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
@@ -15,36 +15,30 @@
*/
package com.google.android.exoplayer2.source;
-import android.util.Log;
-import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
/**
- * Loops a {@link MediaSource}.
+ * Loops a {@link MediaSource} a specified number of times.
+ *
+ * Note: To loop a {@link MediaSource} indefinitely, it is usually better to use
+ * {@link ExoPlayer#setRepeatMode(int)}.
*/
public final class LoopingMediaSource implements MediaSource {
- /**
- * The maximum number of periods that can be exposed by the source. The value of this constant is
- * large enough to cause indefinite looping in practice (the total duration of the looping source
- * will be approximately five years if the duration of each period is one second).
- */
- public static final int MAX_EXPOSED_PERIODS = 157680000;
-
- private static final String TAG = "LoopingMediaSource";
-
private final MediaSource childSource;
private final int loopCount;
private int childPeriodCount;
/**
- * Loops the provided source indefinitely.
+ * Loops the provided source indefinitely. Note that it is usually better to use
+ * {@link ExoPlayer#setRepeatMode(int)}.
*
* @param childSource The {@link MediaSource} to loop.
*/
@@ -56,9 +50,7 @@ public final class LoopingMediaSource implements MediaSource {
* Loops the provided source a specified number of times.
*
* @param childSource The {@link MediaSource} to loop.
- * @param loopCount The desired number of loops. Must be strictly positive. The actual number of
- * loops will be capped at the maximum that can achieved without causing the number of
- * periods exposed by the source to exceed {@link #MAX_EXPOSED_PERIODS}.
+ * @param loopCount The desired number of loops. Must be strictly positive.
*/
public LoopingMediaSource(MediaSource childSource, int loopCount) {
Assertions.checkArgument(loopCount > 0);
@@ -72,7 +64,9 @@ public final class LoopingMediaSource implements MediaSource {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
childPeriodCount = timeline.getPeriodCount();
- listener.onSourceInfoRefreshed(new LoopingTimeline(timeline, loopCount), manifest);
+ Timeline loopingTimeline = loopCount != Integer.MAX_VALUE
+ ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline);
+ listener.onSourceInfoRefreshed(loopingTimeline, manifest);
}
});
}
@@ -83,8 +77,10 @@ public final class LoopingMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- return childSource.createPeriod(index % childPeriodCount, allocator, positionUs);
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ return loopCount != Integer.MAX_VALUE
+ ? childSource.createPeriod(new MediaPeriodId(id.periodIndex % childPeriodCount), allocator)
+ : childSource.createPeriod(id, allocator);
}
@Override
@@ -97,7 +93,7 @@ public final class LoopingMediaSource implements MediaSource {
childSource.releaseSource();
}
- private static final class LoopingTimeline extends Timeline {
+ private static final class LoopingTimeline extends AbstractConcatenatedTimeline {
private final Timeline childTimeline;
private final int childPeriodCount;
@@ -105,20 +101,13 @@ public final class LoopingMediaSource implements MediaSource {
private final int loopCount;
public LoopingTimeline(Timeline childTimeline, int loopCount) {
+ super(loopCount);
this.childTimeline = childTimeline;
childPeriodCount = childTimeline.getPeriodCount();
childWindowCount = childTimeline.getWindowCount();
- // This is the maximum number of loops that can be performed without exceeding
- // MAX_EXPOSED_PERIODS periods.
- int maxLoopCount = MAX_EXPOSED_PERIODS / childPeriodCount;
- if (loopCount > maxLoopCount) {
- if (loopCount != Integer.MAX_VALUE) {
- Log.w(TAG, "Capped loops to avoid overflow: " + loopCount + " -> " + maxLoopCount);
- }
- this.loopCount = maxLoopCount;
- } else {
- this.loopCount = loopCount;
- }
+ this.loopCount = loopCount;
+ Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount,
+ "LoopingMediaSource contains too many periods");
}
@Override
@@ -126,45 +115,96 @@ public final class LoopingMediaSource implements MediaSource {
return childWindowCount * loopCount;
}
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- childTimeline.getWindow(windowIndex % childWindowCount, window, setIds,
- defaultPositionProjectionUs);
- int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount;
- window.firstPeriodIndex += periodIndexOffset;
- window.lastPeriodIndex += periodIndexOffset;
- return window;
- }
-
@Override
public int getPeriodCount() {
return childPeriodCount * loopCount;
}
@Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- childTimeline.getPeriod(periodIndex % childPeriodCount, period, setIds);
- int loopCount = (periodIndex / childPeriodCount);
- period.windowIndex += loopCount * childWindowCount;
- if (setIds) {
- period.uid = Pair.create(loopCount, period.uid);
+ protected int getChildIndexByPeriodIndex(int periodIndex) {
+ return periodIndex / childPeriodCount;
+ }
+
+ @Override
+ protected int getChildIndexByWindowIndex(int windowIndex) {
+ return windowIndex / childWindowCount;
+ }
+
+ @Override
+ protected int getChildIndexByChildUid(Object childUid) {
+ if (!(childUid instanceof Integer)) {
+ return C.INDEX_UNSET;
}
- return period;
+ return (Integer) childUid;
+ }
+
+ @Override
+ protected Timeline getTimelineByChildIndex(int childIndex) {
+ return childTimeline;
+ }
+
+ @Override
+ protected int getFirstPeriodIndexByChildIndex(int childIndex) {
+ return childIndex * childPeriodCount;
+ }
+
+ @Override
+ protected int getFirstWindowIndexByChildIndex(int childIndex) {
+ return childIndex * childWindowCount;
+ }
+
+ @Override
+ protected Object getChildUidByChildIndex(int childIndex) {
+ return childIndex;
+ }
+
+ }
+
+ private static final class InfinitelyLoopingTimeline extends Timeline {
+
+ private final Timeline childTimeline;
+
+ public InfinitelyLoopingTimeline(Timeline childTimeline) {
+ this.childTimeline = childTimeline;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return childTimeline.getWindowCount();
+ }
+
+ @Override
+ public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ int childNextWindowIndex = childTimeline.getNextWindowIndex(windowIndex, repeatMode);
+ return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex;
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ int childPreviousWindowIndex = childTimeline.getPreviousWindowIndex(windowIndex, repeatMode);
+ return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1
+ : childPreviousWindowIndex;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ return childTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return childTimeline.getPeriodCount();
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ return childTimeline.getPeriod(periodIndex, period, setIds);
}
@Override
public int getIndexOfPeriod(Object uid) {
- if (!(uid instanceof Pair)) {
- return C.INDEX_UNSET;
- }
- Pair, ?> loopCountAndChildUid = (Pair, ?>) uid;
- if (!(loopCountAndChildUid.first instanceof Integer)) {
- return C.INDEX_UNSET;
- }
- int loopCount = (Integer) loopCountAndChildUid.first;
- int periodIndexOffset = loopCount * childPeriodCount;
- return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset;
+ return childTimeline.getIndexOfPeriod(uid);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
index aaf4c89ff7..7a43dd7562 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -55,8 +55,10 @@ public interface MediaPeriod extends SequenceableLoader {
*
* @param callback Callback to receive updates from this period, including being notified when
* preparation completes.
+ * @param positionUs The position in microseconds relative to the start of the period at which to
+ * start loading data.
*/
- void prepare(Callback callback);
+ void prepare(Callback callback, long positionUs);
/**
* Throws an error that's preventing the period from becoming prepared. Does nothing if no such
@@ -126,16 +128,6 @@ public interface MediaPeriod extends SequenceableLoader {
*/
long readDiscontinuity();
- /**
- * Returns an estimate of the position up to which data is buffered for the enabled tracks.
- *
- * This method should only be called when at least one track is selected.
- *
- * @return An estimate of the absolute position in microseconds up to which data is buffered, or
- * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
- */
- long getBufferedPositionUs();
-
/**
* Attempts to seek to the specified position in microseconds.
*
@@ -151,6 +143,17 @@ public interface MediaPeriod extends SequenceableLoader {
// SequenceableLoader interface. Overridden to provide more specific documentation.
+ /**
+ * Returns an estimate of the position up to which data is buffered for the enabled tracks.
+ *
+ * This method should only be called when at least one track is selected.
+ *
+ * @return An estimate of the absolute position in microseconds up to which data is buffered, or
+ * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
+ */
+ @Override
+ long getBufferedPositionUs();
+
/**
* Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
*
@@ -166,9 +169,9 @@ public interface MediaPeriod extends SequenceableLoader {
* This method may be called both during and after the period has been prepared.
*
* A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
- * {@link Callback} passed to {@link #prepare(Callback)} to request that this method be called
- * when the period is permitted to continue loading data. A period may do this both during and
- * after preparation.
+ * {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be
+ * called when the period is permitted to continue loading data. A period may do this both during
+ * and after preparation.
*
* @param positionUs The current playback position.
* @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
index f013e790f7..790620a80c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.source;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -34,9 +36,100 @@ public interface MediaSource {
* Called when manifest and/or timeline has been refreshed.
*
* @param timeline The source's timeline.
- * @param manifest The loaded manifest.
+ * @param manifest The loaded manifest. May be null.
*/
- void onSourceInfoRefreshed(Timeline timeline, Object manifest);
+ void onSourceInfoRefreshed(Timeline timeline, @Nullable Object manifest);
+
+ }
+
+ /**
+ * Identifier for a {@link MediaPeriod}.
+ */
+ final class MediaPeriodId {
+
+ /**
+ * Value for unset media period identifiers.
+ */
+ public static final MediaPeriodId UNSET =
+ new MediaPeriodId(C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET);
+
+ /**
+ * The timeline period index.
+ */
+ public final int periodIndex;
+
+ /**
+ * If the media period is in an ad group, the index of the ad group in the period.
+ * {@link C#INDEX_UNSET} otherwise.
+ */
+ public final int adGroupIndex;
+
+ /**
+ * If the media period is in an ad group, the index of the ad in its ad group in the period.
+ * {@link C#INDEX_UNSET} otherwise.
+ */
+ public final int adIndexInAdGroup;
+
+ /**
+ * Creates a media period identifier for the specified period in the timeline.
+ *
+ * @param periodIndex The timeline period index.
+ */
+ public MediaPeriodId(int periodIndex) {
+ this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET);
+ }
+
+ /**
+ * Creates a media period identifier that identifies an ad within an ad group at the specified
+ * timeline period.
+ *
+ * @param periodIndex The index of the timeline period that contains the ad group.
+ * @param adGroupIndex The index of the ad group.
+ * @param adIndexInAdGroup The index of the ad in the ad group.
+ */
+ public MediaPeriodId(int periodIndex, int adGroupIndex, int adIndexInAdGroup) {
+ this.periodIndex = periodIndex;
+ this.adGroupIndex = adGroupIndex;
+ this.adIndexInAdGroup = adIndexInAdGroup;
+ }
+
+ /**
+ * Returns a copy of this period identifier but with {@code newPeriodIndex} as its period index.
+ */
+ public MediaPeriodId copyWithPeriodIndex(int newPeriodIndex) {
+ return periodIndex == newPeriodIndex ? this
+ : new MediaPeriodId(newPeriodIndex, adGroupIndex, adIndexInAdGroup);
+ }
+
+ /**
+ * Returns whether this period identifier identifies an ad in an ad group in a period.
+ */
+ public boolean isAd() {
+ return adGroupIndex != C.INDEX_UNSET;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ MediaPeriodId periodId = (MediaPeriodId) obj;
+ return periodIndex == periodId.periodIndex && adGroupIndex == periodId.adGroupIndex
+ && adIndexInAdGroup == periodId.adIndexInAdGroup;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + periodIndex;
+ result = 31 * result + adGroupIndex;
+ result = 31 * result + adIndexInAdGroup;
+ return result;
+ }
}
@@ -58,16 +151,15 @@ public interface MediaSource {
void maybeThrowSourceInfoRefreshError() throws IOException;
/**
- * Returns a new {@link MediaPeriod} corresponding to the period at the specified {@code index}.
- * This method may be called multiple times with the same index without an intervening call to
+ * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called
+ * multiple times with the same period identifier without an intervening call to
* {@link #releasePeriod(MediaPeriod)}.
*
- * @param index The index of the period.
+ * @param id The identifier of the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
- * @param positionUs The player's current playback position.
* @return A new {@link MediaPeriod}.
*/
- MediaPeriod createPeriod(int index, Allocator allocator, long positionUs);
+ MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator);
/**
* Releases the period.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
index 077b5576c1..e6a4d4e603 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -44,11 +44,11 @@ import java.util.IdentityHashMap;
}
@Override
- public void prepare(Callback callback) {
+ public void prepare(Callback callback, long positionUs) {
this.callback = callback;
pendingChildPrepareCount = periods.length;
for (MediaPeriod period : periods) {
- period.prepare(this);
+ period.prepare(this, positionUs);
}
}
@@ -168,14 +168,7 @@ import java.util.IdentityHashMap;
@Override
public long getBufferedPositionUs() {
- long bufferedPositionUs = Long.MAX_VALUE;
- for (MediaPeriod period : enabledPeriods) {
- long rendererBufferedPositionUs = period.getBufferedPositionUs();
- if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) {
- bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
- }
- }
- return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+ return sequenceableLoader.getBufferedPositionUs();
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
index 6f37165916..642752b35b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
@@ -116,10 +116,10 @@ public final class MergingMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
for (int i = 0; i < periods.length; i++) {
- periods[i] = mediaSources[i].createPeriod(index, allocator, positionUs);
+ periods[i] = mediaSources[i].createPeriod(id, allocator);
}
return new MergingMediaPeriod(periods);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
new file mode 100644
index 0000000000..03b2e3b715
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
@@ -0,0 +1,541 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * A queue of metadata describing the contents of a media buffer.
+ */
+/* package */ final class SampleMetadataQueue {
+
+ /**
+ * A holder for sample metadata not held by {@link DecoderInputBuffer}.
+ */
+ public static final class SampleExtrasHolder {
+
+ public int size;
+ public long offset;
+ public CryptoData cryptoData;
+
+ }
+
+ private static final int SAMPLE_CAPACITY_INCREMENT = 1000;
+
+ private int capacity;
+ private int[] sourceIds;
+ private long[] offsets;
+ private int[] sizes;
+ private int[] flags;
+ private long[] timesUs;
+ private CryptoData[] cryptoDatas;
+ private Format[] formats;
+
+ private int length;
+ private int absoluteStartIndex;
+ private int relativeStartIndex;
+ private int readPosition;
+
+ private long largestDiscardedTimestampUs;
+ private long largestQueuedTimestampUs;
+ private boolean upstreamKeyframeRequired;
+ private boolean upstreamFormatRequired;
+ private Format upstreamFormat;
+ private int upstreamSourceId;
+
+ public SampleMetadataQueue() {
+ capacity = SAMPLE_CAPACITY_INCREMENT;
+ sourceIds = new int[capacity];
+ offsets = new long[capacity];
+ timesUs = new long[capacity];
+ flags = new int[capacity];
+ sizes = new int[capacity];
+ cryptoDatas = new CryptoData[capacity];
+ formats = new Format[capacity];
+ largestDiscardedTimestampUs = Long.MIN_VALUE;
+ largestQueuedTimestampUs = Long.MIN_VALUE;
+ upstreamFormatRequired = true;
+ upstreamKeyframeRequired = true;
+ }
+
+ /**
+ * Clears all sample metadata from the queue.
+ *
+ * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false,
+ * samples queued after the reset (and before a subsequent call to {@link #format(Format)})
+ * are assumed to have the current upstream format. If set to true, {@link #format(Format)}
+ * must be called after the reset before any more samples can be queued.
+ */
+ public void reset(boolean resetUpstreamFormat) {
+ length = 0;
+ absoluteStartIndex = 0;
+ relativeStartIndex = 0;
+ readPosition = 0;
+ upstreamKeyframeRequired = true;
+ largestDiscardedTimestampUs = Long.MIN_VALUE;
+ largestQueuedTimestampUs = Long.MIN_VALUE;
+ if (resetUpstreamFormat) {
+ upstreamFormat = null;
+ upstreamFormatRequired = true;
+ }
+ }
+
+ /**
+ * Returns the current absolute write index.
+ */
+ public int getWriteIndex() {
+ return absoluteStartIndex + length;
+ }
+
+ /**
+ * Discards samples from the write side of the queue.
+ *
+ * @param discardFromIndex The absolute index of the first sample to be discarded.
+ * @return The reduced total number of bytes written after the samples have been discarded, or 0
+ * if the queue is now empty.
+ */
+ public long discardUpstreamSamples(int discardFromIndex) {
+ int discardCount = getWriteIndex() - discardFromIndex;
+ Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
+ length -= discardCount;
+ largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));
+ if (length == 0) {
+ return 0;
+ } else {
+ int relativeLastWriteIndex = getRelativeIndex(length - 1);
+ return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex];
+ }
+ }
+
+ public void sourceId(int sourceId) {
+ upstreamSourceId = sourceId;
+ }
+
+ // Called by the consuming thread.
+
+ /**
+ * Returns the current absolute read index.
+ */
+ public int getReadIndex() {
+ return absoluteStartIndex + readPosition;
+ }
+
+ /**
+ * Peeks the source id of the next sample to be read, or the current upstream source id if the
+ * queue is empty or if the read position is at the end of the queue.
+ *
+ * @return The source id.
+ */
+ public int peekSourceId() {
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId;
+ }
+
+ /**
+ * Returns whether a sample is available to be read.
+ */
+ public synchronized boolean hasNextSample() {
+ return readPosition != length;
+ }
+
+ /**
+ * Returns the upstream {@link Format} in which samples are being queued.
+ */
+ public synchronized Format getUpstreamFormat() {
+ return upstreamFormatRequired ? null : upstreamFormat;
+ }
+
+ /**
+ * Returns the largest sample timestamp that has been queued since the last call to
+ * {@link #reset(boolean)}.
+ *
+ * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
+ * considered as having been queued. Samples that were dequeued from the front of the queue are
+ * considered as having been queued.
+ *
+ * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
+ * samples have been queued.
+ */
+ public synchronized long getLargestQueuedTimestampUs() {
+ return largestQueuedTimestampUs;
+ }
+
+ /**
+ * Rewinds the read position to the first sample retained in the queue.
+ */
+ public synchronized void rewind() {
+ readPosition = 0;
+ }
+
+ /**
+ * Attempts to read from the queue.
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If a sample is read then the buffer is populated with information
+ * about the sample, but not its data. The size and absolute position of the data in the
+ * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
+ * and the absolute position of the first byte that may still be required after the current
+ * sample has been read. May be null if the caller requires that the format of the stream be
+ * read even if it's not changing.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even
+ * if it's not changing. A sample will never be read if set to true, however it is still
+ * possible for the end of stream or nothing to be read.
+ * @param loadingFinished True if an empty queue should be considered the end of the stream.
+ * @param downstreamFormat The current downstream {@link Format}. If the format of the next
+ * sample is different to the current downstream format then a format will be read.
+ * @param extrasHolder The holder into which extra sample information should be written.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
+ * or {@link C#RESULT_BUFFER_READ}.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ public synchronized int read(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
+ SampleExtrasHolder extrasHolder) {
+ if (!hasNextSample()) {
+ if (loadingFinished) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ } else if (upstreamFormat != null
+ && (formatRequired || upstreamFormat != downstreamFormat)) {
+ formatHolder.format = upstreamFormat;
+ return C.RESULT_FORMAT_READ;
+ } else {
+ return C.RESULT_NOTHING_READ;
+ }
+ }
+
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {
+ formatHolder.format = formats[relativeReadIndex];
+ return C.RESULT_FORMAT_READ;
+ }
+
+ if (buffer.isFlagsOnly()) {
+ return C.RESULT_NOTHING_READ;
+ }
+
+ buffer.timeUs = timesUs[relativeReadIndex];
+ buffer.setFlags(flags[relativeReadIndex]);
+ extrasHolder.size = sizes[relativeReadIndex];
+ extrasHolder.offset = offsets[relativeReadIndex];
+ extrasHolder.cryptoData = cryptoDatas[relativeReadIndex];
+
+ readPosition++;
+ return C.RESULT_BUFFER_READ;
+ }
+
+ /**
+ * Attempts to advance the read position to the sample before or at the specified time.
+ *
+ * @param timeUs The time to advance to.
+ * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified
+ * time, rather than to any sample before or at that time.
+ * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
+ * end of the queue, by advancing the read position to the last sample (or keyframe) in the
+ * queue.
+ * @return Whether the operation was a success. A successful advance is one in which the read
+ * position was unchanged or advanced, and is now at a sample meeting the specified criteria.
+ */
+ public synchronized boolean advanceTo(long timeUs, boolean toKeyframe,
+ boolean allowTimeBeyondBuffer) {
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]
+ || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) {
+ return false;
+ }
+ int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe);
+ if (offset == -1) {
+ return false;
+ }
+ readPosition += offset;
+ return true;
+ }
+
+ /**
+ * Advances the read position to the end of the queue.
+ */
+ public synchronized void advanceToEnd() {
+ if (!hasNextSample()) {
+ return;
+ }
+ readPosition = length;
+ }
+
+ /**
+ * Discards up to but not including the sample immediately before or at the specified time.
+ *
+ * @param timeUs The time to discard up to.
+ * @param toKeyframe If true then discards samples up to the keyframe before or at the specified
+ * time, rather than just any sample before or at that time.
+ * @param stopAtReadPosition If true then samples are only discarded if they're before the read
+ * position. If false then samples at and beyond the read position may be discarded, in which
+ * case the read position is advanced to the first remaining sample.
+ * @return The corresponding offset up to which data should be discarded, or
+ * {@link C#POSITION_UNSET} if no discarding of data is necessary.
+ */
+ public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
+ if (length == 0 || timeUs < timesUs[relativeStartIndex]) {
+ return C.POSITION_UNSET;
+ }
+ int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length;
+ int discardCount = findSampleBefore(relativeStartIndex, searchLength, timeUs, toKeyframe);
+ if (discardCount == -1) {
+ return C.POSITION_UNSET;
+ }
+ return discardSamples(discardCount);
+ }
+
+ /**
+ * Discards samples up to but not including the read position.
+ *
+ * @return The corresponding offset up to which data should be discarded, or
+ * {@link C#POSITION_UNSET} if no discarding of data is necessary.
+ */
+ public synchronized long discardToRead() {
+ if (readPosition == 0) {
+ return C.POSITION_UNSET;
+ }
+ return discardSamples(readPosition);
+ }
+
+ /**
+ * Discards all samples in the queue. The read position is also advanced.
+ *
+ * @return The corresponding offset up to which data should be discarded, or
+ * {@link C#POSITION_UNSET} if no discarding of data is necessary.
+ */
+ public synchronized long discardToEnd() {
+ if (length == 0) {
+ return C.POSITION_UNSET;
+ }
+ return discardSamples(length);
+ }
+
+ // Called by the loading thread.
+
+ public synchronized boolean format(Format format) {
+ if (format == null) {
+ upstreamFormatRequired = true;
+ return false;
+ }
+ upstreamFormatRequired = false;
+ if (Util.areEqual(format, upstreamFormat)) {
+ // Suppress changes between equal formats so we can use referential equality in readData.
+ return false;
+ } else {
+ upstreamFormat = format;
+ return true;
+ }
+ }
+
+ public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset,
+ int size, CryptoData cryptoData) {
+ if (upstreamKeyframeRequired) {
+ if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
+ return;
+ }
+ upstreamKeyframeRequired = false;
+ }
+ Assertions.checkState(!upstreamFormatRequired);
+ commitSampleTimestamp(timeUs);
+
+ int relativeEndIndex = getRelativeIndex(length);
+ timesUs[relativeEndIndex] = timeUs;
+ offsets[relativeEndIndex] = offset;
+ sizes[relativeEndIndex] = size;
+ flags[relativeEndIndex] = sampleFlags;
+ cryptoDatas[relativeEndIndex] = cryptoData;
+ formats[relativeEndIndex] = upstreamFormat;
+ sourceIds[relativeEndIndex] = upstreamSourceId;
+
+ length++;
+ if (length == capacity) {
+ // Increase the capacity.
+ int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
+ int[] newSourceIds = new int[newCapacity];
+ long[] newOffsets = new long[newCapacity];
+ long[] newTimesUs = new long[newCapacity];
+ int[] newFlags = new int[newCapacity];
+ int[] newSizes = new int[newCapacity];
+ CryptoData[] newCryptoDatas = new CryptoData[newCapacity];
+ Format[] newFormats = new Format[newCapacity];
+ int beforeWrap = capacity - relativeStartIndex;
+ System.arraycopy(offsets, relativeStartIndex, newOffsets, 0, beforeWrap);
+ System.arraycopy(timesUs, relativeStartIndex, newTimesUs, 0, beforeWrap);
+ System.arraycopy(flags, relativeStartIndex, newFlags, 0, beforeWrap);
+ System.arraycopy(sizes, relativeStartIndex, newSizes, 0, beforeWrap);
+ System.arraycopy(cryptoDatas, relativeStartIndex, newCryptoDatas, 0, beforeWrap);
+ System.arraycopy(formats, relativeStartIndex, newFormats, 0, beforeWrap);
+ System.arraycopy(sourceIds, relativeStartIndex, newSourceIds, 0, beforeWrap);
+ int afterWrap = relativeStartIndex;
+ System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
+ System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
+ System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
+ System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
+ System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap);
+ System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap);
+ System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
+ offsets = newOffsets;
+ timesUs = newTimesUs;
+ flags = newFlags;
+ sizes = newSizes;
+ cryptoDatas = newCryptoDatas;
+ formats = newFormats;
+ sourceIds = newSourceIds;
+ relativeStartIndex = 0;
+ length = capacity;
+ capacity = newCapacity;
+ }
+ }
+
+ public synchronized void commitSampleTimestamp(long timeUs) {
+ largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
+ }
+
+ /**
+ * Attempts to discard samples from the end of the queue to allow samples starting from the
+ * specified timestamp to be spliced in. Samples will not be discarded prior to the read position.
+ *
+ * @param timeUs The timestamp at which the splice occurs.
+ * @return Whether the splice was successful.
+ */
+ public synchronized boolean attemptSplice(long timeUs) {
+ if (length == 0) {
+ return timeUs > largestDiscardedTimestampUs;
+ }
+ long largestReadTimestampUs = Math.max(largestDiscardedTimestampUs,
+ getLargestTimestamp(readPosition));
+ if (largestReadTimestampUs >= timeUs) {
+ return false;
+ }
+ int retainCount = length;
+ int relativeSampleIndex = getRelativeIndex(length - 1);
+ while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) {
+ retainCount--;
+ relativeSampleIndex--;
+ if (relativeSampleIndex == -1) {
+ relativeSampleIndex = capacity - 1;
+ }
+ }
+ discardUpstreamSamples(absoluteStartIndex + retainCount);
+ return true;
+ }
+
+ // Internal methods.
+
+ /**
+ * Finds the sample in the specified range that's before or at the specified time. If
+ * {@code keyframe} is {@code true} then the sample is additionally required to be a keyframe.
+ *
+ * @param relativeStartIndex The relative index from which to start searching.
+ * @param length The length of the range being searched.
+ * @param timeUs The specified time.
+ * @param keyframe Whether only keyframes should be considered.
+ * @return The offset from {@code relativeStartIndex} to the found sample, or -1 if no matching
+ * sample was found.
+ */
+ private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) {
+ // This could be optimized to use a binary search, however in practice callers to this method
+ // normally pass times near to the start of the search region. Hence it's unclear whether
+ // switching to a binary search would yield any real benefit.
+ int sampleCountToTarget = -1;
+ int searchIndex = relativeStartIndex;
+ for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) {
+ if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ // We've found a suitable sample.
+ sampleCountToTarget = i;
+ }
+ searchIndex++;
+ if (searchIndex == capacity) {
+ searchIndex = 0;
+ }
+ }
+ return sampleCountToTarget;
+ }
+
+ /**
+ * Discards the specified number of samples.
+ *
+ * @param discardCount The number of samples to discard.
+ * @return The corresponding offset up to which data should be discarded, or
+ * {@link C#POSITION_UNSET} if no discarding of data is necessary.
+ */
+ private long discardSamples(int discardCount) {
+ largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs,
+ getLargestTimestamp(discardCount));
+ length -= discardCount;
+ absoluteStartIndex += discardCount;
+ relativeStartIndex += discardCount;
+ if (relativeStartIndex >= capacity) {
+ relativeStartIndex -= capacity;
+ }
+ readPosition -= discardCount;
+ if (readPosition < 0) {
+ readPosition = 0;
+ }
+ if (length == 0) {
+ int relativeLastDiscardIndex = (relativeStartIndex == 0 ? capacity : relativeStartIndex) - 1;
+ return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex];
+ } else {
+ return offsets[relativeStartIndex];
+ }
+ }
+
+ /**
+ * Finds the largest timestamp of any sample from the start of the queue up to the specified
+ * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of
+ * the keyframe itself, and of subsequent frames.
+ *
+ * @param length The length of the range being searched.
+ * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}.
+ */
+ private long getLargestTimestamp(int length) {
+ if (length == 0) {
+ return Long.MIN_VALUE;
+ }
+ long largestTimestampUs = Long.MIN_VALUE;
+ int relativeSampleIndex = getRelativeIndex(length - 1);
+ for (int i = 0; i < length; i++) {
+ largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]);
+ if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ break;
+ }
+ relativeSampleIndex--;
+ if (relativeSampleIndex == -1) {
+ relativeSampleIndex = capacity - 1;
+ }
+ }
+ return largestTimestampUs;
+ }
+
+ /**
+ * Returns the relative index for a given offset from the start of the queue.
+ *
+ * @param offset The offset, which must be in the range [0, length].
+ */
+ private int getRelativeIndex(int offset) {
+ int relativeIndex = relativeStartIndex + offset;
+ return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
new file mode 100644
index 0000000000..c7bae8f8b4
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
@@ -0,0 +1,701 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.source.SampleMetadataQueue.SampleExtrasHolder;
+import com.google.android.exoplayer2.upstream.Allocation;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * A queue of media samples.
+ */
+public final class SampleQueue implements TrackOutput {
+
+ /**
+ * A listener for changes to the upstream format.
+ */
+ public interface UpstreamFormatChangedListener {
+
+ /**
+ * Called on the loading thread when an upstream format change occurs.
+ *
+ * @param format The new upstream format.
+ */
+ void onUpstreamFormatChanged(Format format);
+
+ }
+
+ private static final int INITIAL_SCRATCH_SIZE = 32;
+
+ private final Allocator allocator;
+ private final int allocationLength;
+ private final SampleMetadataQueue metadataQueue;
+ private final SampleExtrasHolder extrasHolder;
+ private final ParsableByteArray scratch;
+
+ // References into the linked list of allocations.
+ private AllocationNode firstAllocationNode;
+ private AllocationNode readAllocationNode;
+ private AllocationNode writeAllocationNode;
+
+ // Accessed only by the consuming thread.
+ private Format downstreamFormat;
+
+ // Accessed only by the loading thread (or the consuming thread when there is no loading thread).
+ private boolean pendingFormatAdjustment;
+ private Format lastUnadjustedFormat;
+ private long sampleOffsetUs;
+ private long totalBytesWritten;
+ private boolean pendingSplice;
+ private UpstreamFormatChangedListener upstreamFormatChangeListener;
+
+ /**
+ * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
+ */
+ public SampleQueue(Allocator allocator) {
+ this.allocator = allocator;
+ allocationLength = allocator.getIndividualAllocationLength();
+ metadataQueue = new SampleMetadataQueue();
+ extrasHolder = new SampleExtrasHolder();
+ scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
+ firstAllocationNode = new AllocationNode(0, allocationLength);
+ readAllocationNode = firstAllocationNode;
+ writeAllocationNode = firstAllocationNode;
+ }
+
+ // Called by the consuming thread, but only when there is no loading thread.
+
+ /**
+ * Resets the output without clearing the upstream format. Equivalent to {@code reset(false)}.
+ */
+ public void reset() {
+ reset(false);
+ }
+
+ /**
+ * Resets the output.
+ *
+ * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false,
+ * samples queued after the reset (and before a subsequent call to {@link #format(Format)})
+ * are assumed to have the current upstream format. If set to true, {@link #format(Format)}
+ * must be called after the reset before any more samples can be queued.
+ */
+ public void reset(boolean resetUpstreamFormat) {
+ metadataQueue.reset(resetUpstreamFormat);
+ clearAllocationNodes(firstAllocationNode);
+ firstAllocationNode = new AllocationNode(0, allocationLength);
+ readAllocationNode = firstAllocationNode;
+ writeAllocationNode = firstAllocationNode;
+ totalBytesWritten = 0;
+ allocator.trim();
+ }
+
+ /**
+ * Sets a source identifier for subsequent samples.
+ *
+ * @param sourceId The source identifier.
+ */
+ public void sourceId(int sourceId) {
+ metadataQueue.sourceId(sourceId);
+ }
+
+ /**
+ * Indicates samples that are subsequently queued should be spliced into those already queued.
+ */
+ public void splice() {
+ pendingSplice = true;
+ }
+
+ /**
+ * Returns the current absolute write index.
+ */
+ public int getWriteIndex() {
+ return metadataQueue.getWriteIndex();
+ }
+
+ /**
+ * Discards samples from the write side of the queue.
+ *
+ * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the
+ * range [{@link #getReadIndex()}, {@link #getWriteIndex()}].
+ */
+ public void discardUpstreamSamples(int discardFromIndex) {
+ totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex);
+ if (totalBytesWritten == 0 || totalBytesWritten == firstAllocationNode.startPosition) {
+ clearAllocationNodes(firstAllocationNode);
+ firstAllocationNode = new AllocationNode(totalBytesWritten, allocationLength);
+ readAllocationNode = firstAllocationNode;
+ writeAllocationNode = firstAllocationNode;
+ } else {
+ // Find the last node containing at least 1 byte of data that we need to keep.
+ AllocationNode lastNodeToKeep = firstAllocationNode;
+ while (totalBytesWritten > lastNodeToKeep.endPosition) {
+ lastNodeToKeep = lastNodeToKeep.next;
+ }
+ // Discard all subsequent nodes.
+ AllocationNode firstNodeToDiscard = lastNodeToKeep.next;
+ clearAllocationNodes(firstNodeToDiscard);
+ // Reset the successor of the last node to be an uninitialized node.
+ lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength);
+ // Update writeAllocationNode and readAllocationNode as necessary.
+ writeAllocationNode = totalBytesWritten == lastNodeToKeep.endPosition ? lastNodeToKeep.next
+ : lastNodeToKeep;
+ if (readAllocationNode == firstNodeToDiscard) {
+ readAllocationNode = lastNodeToKeep.next;
+ }
+ }
+ }
+
+ // Called by the consuming thread.
+
+ /**
+ * Returns whether a sample is available to be read.
+ */
+ public boolean hasNextSample() {
+ return metadataQueue.hasNextSample();
+ }
+
+ /**
+ * Returns the current absolute read index.
+ */
+ public int getReadIndex() {
+ return metadataQueue.getReadIndex();
+ }
+
+ /**
+ * Peeks the source id of the next sample to be read, or the current upstream source id if the
+ * queue is empty or if the read position is at the end of the queue.
+ *
+ * @return The source id.
+ */
+ public int peekSourceId() {
+ return metadataQueue.peekSourceId();
+ }
+
+ /**
+ * Returns the upstream {@link Format} in which samples are being queued.
+ */
+ public Format getUpstreamFormat() {
+ return metadataQueue.getUpstreamFormat();
+ }
+
+ /**
+ * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
+ *
+ * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
+ * considered as having been queued. Samples that were dequeued from the front of the queue are
+ * considered as having been queued.
+ *
+ * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
+ * samples have been queued.
+ */
+ public long getLargestQueuedTimestampUs() {
+ return metadataQueue.getLargestQueuedTimestampUs();
+ }
+
+ /**
+ * Rewinds the read position to the first sample in the queue.
+ */
+ public void rewind() {
+ metadataQueue.rewind();
+ readAllocationNode = firstAllocationNode;
+ }
+
+ /**
+ * Discards up to but not including the sample immediately before or at the specified time.
+ *
+ * @param timeUs The time to discard to.
+ * @param toKeyframe If true then discards samples up to the keyframe before or at the specified
+ * time, rather than any sample before or at that time.
+ * @param stopAtReadPosition If true then samples are only discarded if they're before the
+ * read position. If false then samples at and beyond the read position may be discarded, in
+ * which case the read position is advanced to the first remaining sample.
+ */
+ public void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
+ discardDownstreamTo(metadataQueue.discardTo(timeUs, toKeyframe, stopAtReadPosition));
+ }
+
+ /**
+ * Discards up to but not including the read position.
+ */
+ public void discardToRead() {
+ discardDownstreamTo(metadataQueue.discardToRead());
+ }
+
+ /**
+ * Discards to the end of the queue. The read position is also advanced.
+ */
+ public void discardToEnd() {
+ discardDownstreamTo(metadataQueue.discardToEnd());
+ }
+
+ /**
+ * Advances the read position to the end of the queue.
+ */
+ public void advanceToEnd() {
+ metadataQueue.advanceToEnd();
+ }
+
+ /**
+ * Attempts to advance the read position to the sample before or at the specified time.
+ *
+ * @param timeUs The time to advance to.
+ * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified
+ * time, rather than to any sample before or at that time.
+ * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
+ * end of the queue, by advancing the read position to the last sample (or keyframe).
+ * @return Whether the operation was a success. A successful advance is one in which the read
+ * position was unchanged or advanced, and is now at a sample meeting the specified criteria.
+ */
+ public boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) {
+ return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer);
+ }
+
+ /**
+ * Attempts to read from the queue.
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If the end of the stream has been reached, the
+ * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even if
+ * it's not changing. A sample will never be read if set to true, however it is still possible
+ * for the end of stream or nothing to be read.
+ * @param loadingFinished True if an empty queue should be considered the end of the stream.
+ * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
+ * be set if the buffer's timestamp is less than this value.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+ * {@link C#RESULT_BUFFER_READ}.
+ */
+ public int read(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
+ boolean loadingFinished, long decodeOnlyUntilUs) {
+ int result = metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished,
+ downstreamFormat, extrasHolder);
+ switch (result) {
+ case C.RESULT_FORMAT_READ:
+ downstreamFormat = formatHolder.format;
+ return C.RESULT_FORMAT_READ;
+ case C.RESULT_BUFFER_READ:
+ if (!buffer.isEndOfStream()) {
+ if (buffer.timeUs < decodeOnlyUntilUs) {
+ buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ }
+ // Read encryption data if the sample is encrypted.
+ if (buffer.isEncrypted()) {
+ readEncryptionData(buffer, extrasHolder);
+ }
+ // Write the sample data into the holder.
+ buffer.ensureSpaceForWrite(extrasHolder.size);
+ readData(extrasHolder.offset, buffer.data, extrasHolder.size);
+ }
+ return C.RESULT_BUFFER_READ;
+ case C.RESULT_NOTHING_READ:
+ return C.RESULT_NOTHING_READ;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Reads encryption data for the current sample.
+ *
+ * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and
+ * {@link SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The
+ * same value is added to {@link SampleExtrasHolder#offset}.
+ *
+ * @param buffer The buffer into which the encryption data should be written.
+ * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
+ */
+ private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
+ long offset = extrasHolder.offset;
+
+ // Read the signal byte.
+ scratch.reset(1);
+ readData(offset, scratch.data, 1);
+ offset++;
+ byte signalByte = scratch.data[0];
+ boolean subsampleEncryption = (signalByte & 0x80) != 0;
+ int ivSize = signalByte & 0x7F;
+
+ // Read the initialization vector.
+ if (buffer.cryptoInfo.iv == null) {
+ buffer.cryptoInfo.iv = new byte[16];
+ }
+ readData(offset, buffer.cryptoInfo.iv, ivSize);
+ offset += ivSize;
+
+ // Read the subsample count, if present.
+ int subsampleCount;
+ if (subsampleEncryption) {
+ scratch.reset(2);
+ readData(offset, scratch.data, 2);
+ offset += 2;
+ subsampleCount = scratch.readUnsignedShort();
+ } else {
+ subsampleCount = 1;
+ }
+
+ // Write the clear and encrypted subsample sizes.
+ int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData;
+ if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
+ clearDataSizes = new int[subsampleCount];
+ }
+ int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData;
+ if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
+ encryptedDataSizes = new int[subsampleCount];
+ }
+ if (subsampleEncryption) {
+ int subsampleDataLength = 6 * subsampleCount;
+ scratch.reset(subsampleDataLength);
+ readData(offset, scratch.data, subsampleDataLength);
+ offset += subsampleDataLength;
+ scratch.setPosition(0);
+ for (int i = 0; i < subsampleCount; i++) {
+ clearDataSizes[i] = scratch.readUnsignedShort();
+ encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
+ }
+ } else {
+ clearDataSizes[0] = 0;
+ encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
+ }
+
+ // Populate the cryptoInfo.
+ CryptoData cryptoData = extrasHolder.cryptoData;
+ buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,
+ cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode,
+ cryptoData.encryptedBlocks, cryptoData.clearBlocks);
+
+ // Adjust the offset and size to take into account the bytes read.
+ int bytesRead = (int) (offset - extrasHolder.offset);
+ extrasHolder.offset += bytesRead;
+ extrasHolder.size -= bytesRead;
+ }
+
+ /**
+ * Reads data from the front of the rolling buffer.
+ *
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The buffer into which data should be written.
+ * @param length The number of bytes to read.
+ */
+ private void readData(long absolutePosition, ByteBuffer target, int length) {
+ advanceReadTo(absolutePosition);
+ int remaining = length;
+ while (remaining > 0) {
+ int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
+ Allocation allocation = readAllocationNode.allocation;
+ target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy);
+ remaining -= toCopy;
+ absolutePosition += toCopy;
+ if (absolutePosition == readAllocationNode.endPosition) {
+ readAllocationNode = readAllocationNode.next;
+ }
+ }
+ }
+
+ /**
+ * Reads data from the front of the rolling buffer.
+ *
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The array into which data should be written.
+ * @param length The number of bytes to read.
+ */
+ private void readData(long absolutePosition, byte[] target, int length) {
+ advanceReadTo(absolutePosition);
+ int remaining = length;
+ while (remaining > 0) {
+ int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
+ Allocation allocation = readAllocationNode.allocation;
+ System.arraycopy(allocation.data, readAllocationNode.translateOffset(absolutePosition),
+ target, length - remaining, toCopy);
+ remaining -= toCopy;
+ absolutePosition += toCopy;
+ if (absolutePosition == readAllocationNode.endPosition) {
+ readAllocationNode = readAllocationNode.next;
+ }
+ }
+ }
+
+ /**
+ * Advances {@link #readAllocationNode} to the specified absolute position.
+ *
+ * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced.
+ */
+ private void advanceReadTo(long absolutePosition) {
+ while (absolutePosition >= readAllocationNode.endPosition) {
+ readAllocationNode = readAllocationNode.next;
+ }
+ }
+
+ /**
+ * Advances {@link #firstAllocationNode} to the specified absolute position.
+ * {@link #readAllocationNode} is also advanced if necessary to avoid it falling behind
+ * {@link #firstAllocationNode}. Nodes that have been advanced past are cleared, and their
+ * underlying allocations are returned to the allocator.
+ *
+ * @param absolutePosition The position to which {@link #firstAllocationNode} should be advanced.
+ * May be {@link C#POSITION_UNSET}, in which case calling this method is a no-op.
+ */
+ private void discardDownstreamTo(long absolutePosition) {
+ if (absolutePosition == C.POSITION_UNSET) {
+ return;
+ }
+ while (absolutePosition >= firstAllocationNode.endPosition) {
+ allocator.release(firstAllocationNode.allocation);
+ firstAllocationNode = firstAllocationNode.clear();
+ }
+ // If we discarded the node referenced by readAllocationNode then we need to advance it to the
+ // first remaining node.
+ if (readAllocationNode.startPosition < firstAllocationNode.startPosition) {
+ readAllocationNode = firstAllocationNode;
+ }
+ }
+
+ // Called by the loading thread.
+
+ /**
+ * Sets a listener to be notified of changes to the upstream format.
+ *
+ * @param listener The listener.
+ */
+ public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) {
+ upstreamFormatChangeListener = listener;
+ }
+
+ /**
+ * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
+ * that are subsequently queued.
+ *
+ * @param sampleOffsetUs The timestamp offset in microseconds.
+ */
+ public void setSampleOffsetUs(long sampleOffsetUs) {
+ if (this.sampleOffsetUs != sampleOffsetUs) {
+ this.sampleOffsetUs = sampleOffsetUs;
+ pendingFormatAdjustment = true;
+ }
+ }
+
+ @Override
+ public void format(Format format) {
+ Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
+ boolean formatChanged = metadataQueue.format(adjustedFormat);
+ lastUnadjustedFormat = format;
+ pendingFormatAdjustment = false;
+ if (upstreamFormatChangeListener != null && formatChanged) {
+ upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
+ }
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ length = preAppend(length);
+ int bytesAppended = input.read(writeAllocationNode.allocation.data,
+ writeAllocationNode.translateOffset(totalBytesWritten), length);
+ if (bytesAppended == C.RESULT_END_OF_INPUT) {
+ if (allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ postAppend(bytesAppended);
+ return bytesAppended;
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray buffer, int length) {
+ while (length > 0) {
+ int bytesAppended = preAppend(length);
+ buffer.readBytes(writeAllocationNode.allocation.data,
+ writeAllocationNode.translateOffset(totalBytesWritten), bytesAppended);
+ length -= bytesAppended;
+ postAppend(bytesAppended);
+ }
+ }
+
+ @Override
+ public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+ CryptoData cryptoData) {
+ if (pendingFormatAdjustment) {
+ format(lastUnadjustedFormat);
+ }
+ if (pendingSplice) {
+ if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) {
+ return;
+ }
+ pendingSplice = false;
+ }
+ timeUs += sampleOffsetUs;
+ long absoluteOffset = totalBytesWritten - size - offset;
+ metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
+ }
+
+ // Private methods.
+
+ /**
+ * Clears allocation nodes starting from {@code fromNode}.
+ *
+ * @param fromNode The node from which to clear.
+ */
+ private void clearAllocationNodes(AllocationNode fromNode) {
+ if (!fromNode.wasInitialized) {
+ return;
+ }
+ // Bulk release allocations for performance (it's significantly faster when using
+ // DefaultAllocator because the allocator's lock only needs to be acquired and released once)
+ // [Internal: See b/29542039].
+ int allocationCount = (writeAllocationNode.wasInitialized ? 1 : 0)
+ + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) / allocationLength);
+ Allocation[] allocationsToRelease = new Allocation[allocationCount];
+ AllocationNode currentNode = fromNode;
+ for (int i = 0; i < allocationsToRelease.length; i++) {
+ allocationsToRelease[i] = currentNode.allocation;
+ currentNode = currentNode.clear();
+ }
+ allocator.release(allocationsToRelease);
+ }
+
+ /**
+ * Called before writing sample data to {@link #writeAllocationNode}. May cause
+ * {@link #writeAllocationNode} to be initialized.
+ *
+ * @param length The number of bytes that the caller wishes to write.
+ * @return The number of bytes that the caller is permitted to write, which may be less than
+ * {@code length}.
+ */
+ private int preAppend(int length) {
+ if (!writeAllocationNode.wasInitialized) {
+ writeAllocationNode.initialize(allocator.allocate(),
+ new AllocationNode(writeAllocationNode.endPosition, allocationLength));
+ }
+ return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten));
+ }
+
+ /**
+ * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced.
+ *
+ * @param length The number of bytes that were written.
+ */
+ private void postAppend(int length) {
+ totalBytesWritten += length;
+ if (totalBytesWritten == writeAllocationNode.endPosition) {
+ writeAllocationNode = writeAllocationNode.next;
+ }
+ }
+
+ /**
+ * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}.
+ *
+ * @param format The {@link Format} to adjust.
+ * @param sampleOffsetUs The offset to apply.
+ * @return The adjusted {@link Format}.
+ */
+ private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) {
+ if (format == null) {
+ return null;
+ }
+ if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
+ format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
+ }
+ return format;
+ }
+
+ /**
+ * A node in a linked list of {@link Allocation}s held by the output.
+ */
+ private static final class AllocationNode {
+
+ /**
+ * The absolute position of the start of the data (inclusive).
+ */
+ public final long startPosition;
+ /**
+ * The absolute position of the end of the data (exclusive).
+ */
+ public final long endPosition;
+ /**
+ * Whether the node has been initialized. Remains true after {@link #clear()}.
+ */
+ public boolean wasInitialized;
+ /**
+ * The {@link Allocation}, or {@code null} if the node is not initialized.
+ */
+ @Nullable public Allocation allocation;
+ /**
+ * The next {@link AllocationNode} in the list, or {@code null} if the node has not been
+ * initialized. Remains set after {@link #clear()}.
+ */
+ @Nullable public AllocationNode next;
+
+ /**
+ * @param startPosition See {@link #startPosition}.
+ * @param allocationLength The length of the {@link Allocation} with which this node will be
+ * initialized.
+ */
+ public AllocationNode(long startPosition, int allocationLength) {
+ this.startPosition = startPosition;
+ this.endPosition = startPosition + allocationLength;
+ }
+
+ /**
+ * Initializes the node.
+ *
+ * @param allocation The node's {@link Allocation}.
+ * @param next The next {@link AllocationNode}.
+ */
+ public void initialize(Allocation allocation, AllocationNode next) {
+ this.allocation = allocation;
+ this.next = next;
+ wasInitialized = true;
+ }
+
+ /**
+ * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to
+ * the specified absolute position.
+ *
+ * @param absolutePosition The absolute position.
+ * @return The corresponding offset into the allocation's data.
+ */
+ public int translateOffset(long absolutePosition) {
+ return (int) (absolutePosition - startPosition) + allocation.offset;
+ }
+
+ /**
+ * Clears {@link #allocation} and {@link #next}.
+ *
+ * @return The cleared next {@link AllocationNode}.
+ */
+ public AllocationNode clear() {
+ allocation = null;
+ AllocationNode temp = next;
+ next = null;
+ return temp;
+ }
+
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java
index f287153719..26cb9a2666 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java
@@ -36,6 +36,14 @@ public interface SequenceableLoader {
}
+ /**
+ * Returns an estimate of the position up to which data is buffered.
+ *
+ * @return An estimate of the absolute position in microseconds up to which data is buffered, or
+ * {@link C#TIME_END_OF_SOURCE} if the data is fully buffered.
+ */
+ long getBufferedPositionUs();
+
/**
* Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
index 447839392e..6f35438444 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
@@ -26,6 +26,8 @@ public final class SinglePeriodTimeline extends Timeline {
private static final Object ID = new Object();
+ private final long presentationStartTimeMs;
+ private final long windowStartTimeMs;
private final long periodDurationUs;
private final long windowDurationUs;
private final long windowPositionInPeriodUs;
@@ -45,8 +47,8 @@ public final class SinglePeriodTimeline extends Timeline {
}
/**
- * Creates a timeline with one period of known duration, and a window of known duration starting
- * at a specified position in the period.
+ * Creates a timeline with one period, and a window of known duration starting at a specified
+ * position in the period.
*
* @param periodDurationUs The duration of the period in microseconds.
* @param windowDurationUs The duration of the window in microseconds.
@@ -60,6 +62,31 @@ public final class SinglePeriodTimeline extends Timeline {
public SinglePeriodTimeline(long periodDurationUs, long windowDurationUs,
long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable,
boolean isDynamic) {
+ this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs,
+ windowDefaultStartPositionUs, isSeekable, isDynamic);
+ }
+
+ /**
+ * Creates a timeline with one period, and a window of known duration starting at a specified
+ * position in the period.
+ *
+ * @param presentationStartTimeMs The start time of the presentation in milliseconds since the
+ * epoch.
+ * @param windowStartTimeMs The window's start time in milliseconds since the epoch.
+ * @param periodDurationUs The duration of the period in microseconds.
+ * @param windowDurationUs The duration of the window in microseconds.
+ * @param windowPositionInPeriodUs The position of the start of the window in the period, in
+ * microseconds.
+ * @param windowDefaultStartPositionUs The default position relative to the start of the window at
+ * which to begin playback, in microseconds.
+ * @param isSeekable Whether seeking is supported within the window.
+ * @param isDynamic Whether the window may change when the timeline is updated.
+ */
+ public SinglePeriodTimeline(long presentationStartTimeMs, long windowStartTimeMs,
+ long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs,
+ long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) {
+ this.presentationStartTimeMs = presentationStartTimeMs;
+ this.windowStartTimeMs = windowStartTimeMs;
this.periodDurationUs = periodDurationUs;
this.windowDurationUs = windowDurationUs;
this.windowPositionInPeriodUs = windowPositionInPeriodUs;
@@ -86,7 +113,7 @@ public final class SinglePeriodTimeline extends Timeline {
windowDefaultStartPositionUs = C.TIME_UNSET;
}
}
- return window.set(id, C.TIME_UNSET, C.TIME_UNSET, isSeekable, isDynamic,
+ return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic,
windowDefaultStartPositionUs, windowDurationUs, 0, 0, windowPositionInPeriodUs);
}
@@ -99,7 +126,7 @@ public final class SinglePeriodTimeline extends Timeline {
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
Assertions.checkIndex(periodIndex, 0, 1);
Object id = setIds ? ID : null;
- return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false);
+ return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs);
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
index 8e38588e89..3435c01eeb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -79,7 +79,7 @@ import java.util.Arrays;
}
@Override
- public void prepare(Callback callback) {
+ public void prepare(Callback callback, long positionUs) {
callback.onPrepared(this);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
index f6ee84a6f4..99bc60d6fb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
@@ -95,8 +95,8 @@ public final class SingleSampleMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- Assertions.checkArgument(index == 0);
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ Assertions.checkArgument(id.periodIndex == 0);
return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount,
eventHandler, eventListener, eventSourceId);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
index 3882a330f9..9531aaf32e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
@@ -16,9 +16,9 @@
package com.google.android.exoplayer2.source.chunk;
import android.util.Log;
-import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;
/**
@@ -29,22 +29,22 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut
private static final String TAG = "BaseMediaChunkOutput";
private final int[] trackTypes;
- private final DefaultTrackOutput[] trackOutputs;
+ private final SampleQueue[] sampleQueues;
/**
* @param trackTypes The track types of the individual track outputs.
- * @param trackOutputs The individual track outputs.
+ * @param sampleQueues The individual sample queues.
*/
- public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput[] trackOutputs) {
+ public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) {
this.trackTypes = trackTypes;
- this.trackOutputs = trackOutputs;
+ this.sampleQueues = sampleQueues;
}
@Override
public TrackOutput track(int id, int type) {
for (int i = 0; i < trackTypes.length; i++) {
if (type == trackTypes[i]) {
- return trackOutputs[i];
+ return sampleQueues[i];
}
}
Log.e(TAG, "Unmatched track of type: " + type);
@@ -52,13 +52,13 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut
}
/**
- * Returns the current absolute write indices of the individual track outputs.
+ * Returns the current absolute write indices of the individual sample queues.
*/
public int[] getWriteIndices() {
- int[] writeIndices = new int[trackOutputs.length];
- for (int i = 0; i < trackOutputs.length; i++) {
- if (trackOutputs[i] != null) {
- writeIndices[i] = trackOutputs[i].getWriteIndex();
+ int[] writeIndices = new int[sampleQueues.length];
+ for (int i = 0; i < sampleQueues.length; i++) {
+ if (sampleQueues[i] != null) {
+ writeIndices[i] = sampleQueues[i].getWriteIndex();
}
}
return writeIndices;
@@ -66,12 +66,12 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut
/**
* Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
- * subsequently written to the track outputs.
+ * subsequently written to the sample queues.
*/
public void setSampleOffsetUs(long sampleOffsetUs) {
- for (DefaultTrackOutput trackOutput : trackOutputs) {
- if (trackOutput != null) {
- trackOutput.setSampleOffsetUs(sampleOffsetUs);
+ for (SampleQueue sampleQueue : sampleQueues) {
+ if (sampleQueue != null) {
+ sampleQueue.setSampleOffsetUs(sampleOffsetUs);
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
index 501f4998cf..07d1cce8cb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
@@ -186,8 +186,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput {
@Override
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
- byte[] encryptionKey) {
- trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey);
+ CryptoData cryptoData) {
+ trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
index c43f3d577a..0fc3d5881e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -19,8 +19,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -36,7 +36,7 @@ import java.util.List;
* May also be configured to expose additional embedded {@link SampleStream}s.
*/
public class ChunkSampleStream implements SampleStream, SequenceableLoader,
- Loader.Callback {
+ Loader.Callback, Loader.ReleaseCallback {
private final int primaryTrackType;
private final int[] embeddedTrackTypes;
@@ -49,8 +49,8 @@ public class ChunkSampleStream implements SampleStream, S
private final ChunkHolder nextChunkHolder;
private final LinkedList mediaChunks;
private final List readOnlyMediaChunks;
- private final DefaultTrackOutput primarySampleQueue;
- private final DefaultTrackOutput[] embeddedSampleQueues;
+ private final SampleQueue primarySampleQueue;
+ private final SampleQueue[] embeddedSampleQueues;
private final BaseMediaChunkOutput mediaChunkOutput;
private Format primaryDownstreamTrackFormat;
@@ -85,19 +85,19 @@ public class ChunkSampleStream implements SampleStream, S
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length;
- embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount];
+ embeddedSampleQueues = new SampleQueue[embeddedTrackCount];
embeddedTracksSelected = new boolean[embeddedTrackCount];
int[] trackTypes = new int[1 + embeddedTrackCount];
- DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount];
+ SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount];
- primarySampleQueue = new DefaultTrackOutput(allocator);
+ primarySampleQueue = new SampleQueue(allocator);
trackTypes[0] = primaryTrackType;
sampleQueues[0] = primarySampleQueue;
for (int i = 0; i < embeddedTrackCount; i++) {
- DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator);
- embeddedSampleQueues[i] = trackOutput;
- sampleQueues[i + 1] = trackOutput;
+ SampleQueue sampleQueue = new SampleQueue(allocator);
+ embeddedSampleQueues[i] = sampleQueue;
+ sampleQueues[i + 1] = sampleQueue;
trackTypes[i + 1] = embeddedTrackTypes[i];
}
@@ -106,17 +106,20 @@ public class ChunkSampleStream implements SampleStream, S
lastSeekPositionUs = positionUs;
}
+ // TODO: Generalize this method to also discard from the primary sample queue and stop discarding
+ // from this queue in readData and skipData. This will cause samples to be kept in the queue until
+ // they've been rendered, rather than being discarded as soon as they're read by the renderer.
+ // This will make in-buffer seeks more likely when seeking slightly forward from the current
+ // position. This change will need handling with care, in particular when considering removal of
+ // chunks from the front of the mediaChunks list.
/**
- * Discards buffered media for embedded tracks that are not currently selected, up to the
- * specified position.
+ * Discards buffered media for embedded tracks, up to the specified position.
*
* @param positionUs The position to discard up to, in microseconds.
*/
- public void discardUnselectedEmbeddedTracksTo(long positionUs) {
+ public void discardEmbeddedTracksTo(long positionUs) {
for (int i = 0; i < embeddedSampleQueues.length; i++) {
- if (!embeddedTracksSelected[i]) {
- embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true);
- }
+ embeddedSampleQueues[i].discardTo(positionUs, true, embeddedTracksSelected[i]);
}
}
@@ -135,7 +138,8 @@ public class ChunkSampleStream implements SampleStream, S
if (embeddedTrackTypes[i] == trackType) {
Assertions.checkState(!embeddedTracksSelected[i]);
embeddedTracksSelected[i] = true;
- embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true);
+ embeddedSampleQueues[i].rewind();
+ embeddedSampleQueues[i].advanceTo(positionUs, true, true);
return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i);
}
}
@@ -181,19 +185,15 @@ public class ChunkSampleStream implements SampleStream, S
public void seekToUs(long positionUs) {
lastSeekPositionUs = positionUs;
// If we're not pending a reset, see if we can seek within the primary sample queue.
- boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.skipToKeyframeBefore(
- positionUs, positionUs < getNextLoadPositionUs());
+ boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.advanceTo(positionUs, true,
+ positionUs < getNextLoadPositionUs());
if (seekInsideBuffer) {
- // We succeeded. We need to discard any chunks that we've moved past and perform the seek for
- // any embedded streams as well.
- while (mediaChunks.size() > 1
- && mediaChunks.get(1).getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) {
- mediaChunks.removeFirst();
- }
- // TODO: For this to work correctly, the embedded streams must not discard anything from their
- // sample queues beyond the current read position of the primary stream.
- for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
- embeddedSampleQueue.skipToKeyframeBefore(positionUs, true);
+ // We succeeded. Discard samples and corresponding chunks prior to the seek position.
+ discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
+ primarySampleQueue.discardToRead();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.rewind();
+ embeddedSampleQueue.discardTo(positionUs, true, false);
}
} else {
// We failed, and need to restart.
@@ -203,9 +203,9 @@ public class ChunkSampleStream implements SampleStream, S
if (loader.isLoading()) {
loader.cancelLoading();
} else {
- primarySampleQueue.reset(true);
- for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
- embeddedSampleQueue.reset(true);
+ primarySampleQueue.reset();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
}
}
}
@@ -217,18 +217,29 @@ public class ChunkSampleStream implements SampleStream, S
* This method should be called when the stream is no longer required.
*/
public void release() {
- primarySampleQueue.disable();
- for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
- embeddedSampleQueue.disable();
+ boolean releasedSynchronously = loader.release(this);
+ if (!releasedSynchronously) {
+ // Discard as much as we can synchronously.
+ primarySampleQueue.discardToEnd();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.discardToEnd();
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReleased() {
+ primarySampleQueue.reset();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
}
- loader.release();
}
// SampleStream implementation.
@Override
public boolean isReady() {
- return loadingFinished || (!isPendingReset() && !primarySampleQueue.isEmpty());
+ return loadingFinished || (!isPendingReset() && primarySampleQueue.hasNextSample());
}
@Override
@@ -246,17 +257,22 @@ public class ChunkSampleStream implements SampleStream, S
return C.RESULT_NOTHING_READ;
}
discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
- return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
+ int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished,
lastSeekPositionUs);
+ if (result == C.RESULT_BUFFER_READ) {
+ primarySampleQueue.discardToRead();
+ }
+ return result;
}
@Override
public void skipData(long positionUs) {
if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) {
- primarySampleQueue.skipAll();
+ primarySampleQueue.advanceToEnd();
} else {
- primarySampleQueue.skipToKeyframeBefore(positionUs, true);
+ primarySampleQueue.advanceTo(positionUs, true, true);
}
+ primarySampleQueue.discardToRead();
}
// Loader.Callback implementation.
@@ -279,9 +295,9 @@ public class ChunkSampleStream implements SampleStream, S
loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs,
loadable.bytesLoaded());
if (!released) {
- primarySampleQueue.reset(true);
- for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
- embeddedSampleQueue.reset(true);
+ primarySampleQueue.reset();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
}
callback.onContinueLoadingRequested(this);
}
@@ -336,6 +352,7 @@ public class ChunkSampleStream implements SampleStream, S
nextChunkHolder.clear();
if (endOfStream) {
+ pendingResetPositionUs = C.TIME_UNSET;
loadingFinished = true;
return true;
}
@@ -389,18 +406,20 @@ public class ChunkSampleStream implements SampleStream, S
}
private void discardDownstreamMediaChunks(int primaryStreamReadIndex) {
- while (mediaChunks.size() > 1
- && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) {
- mediaChunks.removeFirst();
+ if (!mediaChunks.isEmpty()) {
+ while (mediaChunks.size() > 1
+ && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) {
+ mediaChunks.removeFirst();
+ }
+ BaseMediaChunk currentChunk = mediaChunks.getFirst();
+ Format trackFormat = currentChunk.trackFormat;
+ if (!trackFormat.equals(primaryDownstreamTrackFormat)) {
+ eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat,
+ currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+ currentChunk.startTimeUs);
+ }
+ primaryDownstreamTrackFormat = trackFormat;
}
- BaseMediaChunk currentChunk = mediaChunks.getFirst();
- Format trackFormat = currentChunk.trackFormat;
- if (!trackFormat.equals(primaryDownstreamTrackFormat)) {
- eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat,
- currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
- currentChunk.startTimeUs);
- }
- primaryDownstreamTrackFormat = trackFormat;
}
/**
@@ -413,18 +432,18 @@ public class ChunkSampleStream implements SampleStream, S
if (mediaChunks.size() <= queueLength) {
return false;
}
- long startTimeUs = 0;
+ BaseMediaChunk removed;
+ long startTimeUs;
long endTimeUs = mediaChunks.getLast().endTimeUs;
- BaseMediaChunk removed = null;
- while (mediaChunks.size() > queueLength) {
+ do {
removed = mediaChunks.removeLast();
startTimeUs = removed.startTimeUs;
- loadingFinished = false;
- }
+ } while (mediaChunks.size() > queueLength);
primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0));
for (int i = 0; i < embeddedSampleQueues.length; i++) {
embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1));
}
+ loadingFinished = false;
eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs);
return true;
}
@@ -436,11 +455,10 @@ public class ChunkSampleStream implements SampleStream, S
public final ChunkSampleStream parent;
- private final DefaultTrackOutput sampleQueue;
+ private final SampleQueue sampleQueue;
private final int index;
- public EmbeddedSampleStream(ChunkSampleStream parent, DefaultTrackOutput sampleQueue,
- int index) {
+ public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, int index) {
this.parent = parent;
this.sampleQueue = sampleQueue;
this.index = index;
@@ -448,15 +466,15 @@ public class ChunkSampleStream implements SampleStream, S
@Override
public boolean isReady() {
- return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty());
+ return loadingFinished || (!isPendingReset() && sampleQueue.hasNextSample());
}
@Override
public void skipData(long positionUs) {
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
- sampleQueue.skipAll();
+ sampleQueue.advanceToEnd();
} else {
- sampleQueue.skipToKeyframeBefore(positionUs, true);
+ sampleQueue.advanceTo(positionUs, true, true);
}
}
@@ -471,7 +489,7 @@ public class ChunkSampleStream implements SampleStream, S
if (isPendingReset()) {
return C.RESULT_NOTHING_READ;
}
- return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
+ return sampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished,
lastSeekPositionUs);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
index cfbefc0c2e..cc39c88fd0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
@@ -93,7 +93,7 @@ public class ContainerMediaChunk extends BaseMediaChunk {
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public final void load() throws IOException, InterruptedException {
- DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded);
try {
// Create and open the input.
ExtractorInput input = new DefaultExtractorInput(dataSource,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
index 69474aa150..4acf0b8525 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
@@ -72,7 +72,7 @@ public final class InitializationChunk extends Chunk {
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void load() throws IOException, InterruptedException {
- DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded);
try {
// Create and open the input.
ExtractorInput input = new DefaultExtractorInput(dataSource,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
index a008c9cd84..02cf7dfd55 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
@@ -85,7 +85,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void load() throws IOException, InterruptedException {
- DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded);
try {
// Create and open the input.
long length = dataSource.open(loadDataSpec);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
index 795189e1a6..6a9b83a015 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.cea.Cea608Decoder;
import com.google.android.exoplayer2.text.cea.Cea708Decoder;
import com.google.android.exoplayer2.text.dvb.DvbDecoder;
+import com.google.android.exoplayer2.text.ssa.SsaDecoder;
import com.google.android.exoplayer2.text.subrip.SubripDecoder;
import com.google.android.exoplayer2.text.ttml.TtmlDecoder;
import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder;
@@ -58,6 +59,7 @@ public interface SubtitleDecoderFactory {
*
WebVTT (MP4) ({@link Mp4WebvttDecoder})
*
TTML ({@link TtmlDecoder})
*
SubRip ({@link SubripDecoder})
+ *
SSA/ASS ({@link SsaDecoder})
*
TX3G ({@link Tx3gDecoder})
*
Cea608 ({@link Cea608Decoder})
*
Cea708 ({@link Cea708Decoder})
@@ -70,6 +72,7 @@ public interface SubtitleDecoderFactory {
public boolean supportsFormat(Format format) {
String mimeType = format.sampleMimeType;
return MimeTypes.TEXT_VTT.equals(mimeType)
+ || MimeTypes.TEXT_SSA.equals(mimeType)
|| MimeTypes.APPLICATION_TTML.equals(mimeType)
|| MimeTypes.APPLICATION_MP4VTT.equals(mimeType)
|| MimeTypes.APPLICATION_SUBRIP.equals(mimeType)
@@ -85,6 +88,8 @@ public interface SubtitleDecoderFactory {
switch (format.sampleMimeType) {
case MimeTypes.TEXT_VTT:
return new WebvttDecoder();
+ case MimeTypes.TEXT_SSA:
+ return new SsaDecoder(format.initializationData);
case MimeTypes.APPLICATION_MP4VTT:
return new Mp4WebvttDecoder();
case MimeTypes.APPLICATION_TTML:
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java
index 4950549b19..1820d43e75 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java
@@ -130,7 +130,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
}
@Override
- protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
streamFormat = formats[0];
if (decoder != null) {
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java
new file mode 100644
index 0000000000..d2f5a67c27
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.ssa;
+
+import android.text.TextUtils;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.LongArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for SSA/ASS.
+ */
+public final class SsaDecoder extends SimpleSubtitleDecoder {
+
+ private static final String TAG = "SsaDecoder";
+
+ private static final Pattern SSA_TIMECODE_PATTERN = Pattern.compile(
+ "(?:(\\d+):)?(\\d+):(\\d+)(?::|\\.)(\\d+)");
+ private static final String FORMAT_LINE_PREFIX = "Format: ";
+ private static final String DIALOGUE_LINE_PREFIX = "Dialogue: ";
+
+ private final boolean haveInitializationData;
+
+ private int formatKeyCount;
+ private int formatStartIndex;
+ private int formatEndIndex;
+ private int formatTextIndex;
+
+ public SsaDecoder() {
+ this(null);
+ }
+
+ /**
+ * @param initializationData Optional initialization data for the decoder. If not null, the
+ * initialization data must consist of two byte arrays. The first must contain an SSA format
+ * line. The second must contain an SSA header that will be assumed common to all samples.
+ */
+ public SsaDecoder(List initializationData) {
+ super("SsaDecoder");
+ if (initializationData != null) {
+ haveInitializationData = true;
+ String formatLine = new String(initializationData.get(0));
+ Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX));
+ parseFormatLine(formatLine);
+ parseHeader(new ParsableByteArray(initializationData.get(1)));
+ } else {
+ haveInitializationData = false;
+ }
+ }
+
+ @Override
+ protected SsaSubtitle decode(byte[] bytes, int length, boolean reset) {
+ ArrayList cues = new ArrayList<>();
+ LongArray cueTimesUs = new LongArray();
+
+ ParsableByteArray data = new ParsableByteArray(bytes, length);
+ if (!haveInitializationData) {
+ parseHeader(data);
+ }
+ parseEventBody(data, cues, cueTimesUs);
+
+ Cue[] cuesArray = new Cue[cues.size()];
+ cues.toArray(cuesArray);
+ long[] cueTimesUsArray = cueTimesUs.toArray();
+ return new SsaSubtitle(cuesArray, cueTimesUsArray);
+ }
+
+ /**
+ * Parses the header of the subtitle.
+ *
+ * @param data A {@link ParsableByteArray} from which the header should be read.
+ */
+ private void parseHeader(ParsableByteArray data) {
+ String currentLine;
+ while ((currentLine = data.readLine()) != null) {
+ // TODO: Parse useful data from the header.
+ if (currentLine.startsWith("[Events]")) {
+ // We've reached the event body.
+ return;
+ }
+ }
+ }
+
+ /**
+ * Parses the event body of the subtitle.
+ *
+ * @param data A {@link ParsableByteArray} from which the body should be read.
+ * @param cues A list to which parsed cues will be added.
+ * @param cueTimesUs An array to which parsed cue timestamps will be added.
+ */
+ private void parseEventBody(ParsableByteArray data, List cues, LongArray cueTimesUs) {
+ String currentLine;
+ while ((currentLine = data.readLine()) != null) {
+ if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) {
+ parseFormatLine(currentLine);
+ } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) {
+ parseDialogueLine(currentLine, cues, cueTimesUs);
+ }
+ }
+ }
+
+ /**
+ * Parses a format line.
+ *
+ * @param formatLine The line to parse.
+ */
+ private void parseFormatLine(String formatLine) {
+ String[] values = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ",");
+ formatKeyCount = values.length;
+ formatStartIndex = C.INDEX_UNSET;
+ formatEndIndex = C.INDEX_UNSET;
+ formatTextIndex = C.INDEX_UNSET;
+ for (int i = 0; i < formatKeyCount; i++) {
+ String key = Util.toLowerInvariant(values[i].trim());
+ switch (key) {
+ case "start":
+ formatStartIndex = i;
+ break;
+ case "end":
+ formatEndIndex = i;
+ break;
+ case "text":
+ formatTextIndex = i;
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parses a dialogue line.
+ *
+ * @param dialogueLine The line to parse.
+ * @param cues A list to which parsed cues will be added.
+ * @param cueTimesUs An array to which parsed cue timestamps will be added.
+ */
+ private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) {
+ if (formatKeyCount == 0) {
+ Log.w(TAG, "Skipping dialogue line before format: " + dialogueLine);
+ return;
+ }
+
+ String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length())
+ .split(",", formatKeyCount);
+ long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]);
+ if (startTimeUs == C.TIME_UNSET) {
+ Log.w(TAG, "Skipping invalid timing: " + dialogueLine);
+ return;
+ }
+
+ long endTimeUs = C.TIME_UNSET;
+ String endTimeString = lineValues[formatEndIndex];
+ if (!endTimeString.trim().isEmpty()) {
+ endTimeUs = SsaDecoder.parseTimecodeUs(endTimeString);
+ if (endTimeUs == C.TIME_UNSET) {
+ Log.w(TAG, "Skipping invalid timing: " + dialogueLine);
+ return;
+ }
+ }
+
+ String text = lineValues[formatTextIndex]
+ .replaceAll("\\{.*?\\}", "")
+ .replaceAll("\\\\N", "\n")
+ .replaceAll("\\\\n", "\n");
+ cues.add(new Cue(text));
+ cueTimesUs.add(startTimeUs);
+ if (endTimeUs != C.TIME_UNSET) {
+ cues.add(null);
+ cueTimesUs.add(endTimeUs);
+ }
+ }
+
+ /**
+ * Parses an SSA timecode string.
+ *
+ * @param timeString The string to parse.
+ * @return The parsed timestamp in microseconds.
+ */
+ public static long parseTimecodeUs(String timeString) {
+ Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString);
+ if (!matcher.matches()) {
+ return C.TIME_UNSET;
+ }
+ long timestampUs = Long.parseLong(matcher.group(1)) * 60 * 60 * C.MICROS_PER_SECOND;
+ timestampUs += Long.parseLong(matcher.group(2)) * 60 * C.MICROS_PER_SECOND;
+ timestampUs += Long.parseLong(matcher.group(3)) * C.MICROS_PER_SECOND;
+ timestampUs += Long.parseLong(matcher.group(4)) * 10000; // 100ths of a second.
+ return timestampUs;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java
new file mode 100644
index 0000000000..339119ed6b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.ssa;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A representation of an SSA/ASS subtitle.
+ */
+/* package */ final class SsaSubtitle implements Subtitle {
+
+ private final Cue[] cues;
+ private final long[] cueTimesUs;
+
+ /**
+ * @param cues The cues in the subtitle. Null entries may be used to represent empty cues.
+ * @param cueTimesUs The cue times, in microseconds.
+ */
+ public SsaSubtitle(Cue[] cues, long[] cueTimesUs) {
+ this.cues = cues;
+ this.cueTimesUs = cueTimesUs;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);
+ return index < cueTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return cueTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index >= 0);
+ Assertions.checkArgument(index < cueTimesUs.length);
+ return cueTimesUs[index];
+ }
+
+ @Override
+ public List getCues(long timeUs) {
+ int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
+ if (index == -1 || cues[index] == null) {
+ // timeUs is earlier than the start of the first cue, or we have an empty cue.
+ return Collections.emptyList();
+ } else {
+ return Collections.singletonList(cues[index]);
+ }
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
index e438aa1837..a215bf3cc9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
@@ -290,7 +290,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser,
TtmlNode.ATTR_TTS_DISPLAY_ALIGN);
if (displayAlign != null) {
- switch (displayAlign.toLowerCase()) {
+ switch (Util.toLowerInvariant(displayAlign)) {
case "center":
lineAnchor = Cue.ANCHOR_TYPE_MIDDLE;
line += height / 2;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
index 2a426c9c52..fe2b920933 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -33,37 +33,115 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
- * A {@link MappingTrackSelector} that allows configuration of common parameters. It is safe to call
- * the methods of this class from the application thread. See {@link Parameters#Parameters()} for
- * default selection parameters.
+ * A default {@link TrackSelector} suitable for most use cases.
+ *
+ *
Constraint based track selection
+ * Whilst this selector supports setting specific track overrides, the recommended way of
+ * changing which tracks are selected is by setting {@link Parameters} that constrain the track
+ * selection process. For example an instance can specify a preferred language for
+ * the audio track, and impose constraints on the maximum video resolution that should be selected
+ * for adaptive playbacks. Modifying the parameters is simple:
+ *
+ * {@code
+ * Parameters currentParameters = trackSelector.getParameters();
+ * // Generate new parameters to prefer German audio and impose a maximum video size constraint.
+ * Parameters newParameters = currentParameters
+ * .withPreferredAudioLanguage("de")
+ * .withMaxVideoSize(1024, 768);
+ * // Set the new parameters on the selector.
+ * trackSelector.setParameters(newParameters);}
+ *
+ * There are several benefits to using constraint based track selection instead of specific track
+ * overrides:
+ *
+ *
You can specify constraints before knowing what tracks the media provides. This can
+ * simplify track selection code (e.g. you don't have to listen for changes in the available
+ * tracks before configuring the selector).
+ *
Constraints can be applied consistently across all periods in a complex piece of media,
+ * even if those periods contain different tracks. In contrast, a specific track override is only
+ * applied to periods whose tracks match those for which the override was set.
+ *
+ *
+ *
Track overrides, disabling renderers and tunneling
+ * This selector extends {@link MappingTrackSelector}, and so inherits its support for setting
+ * specific track overrides, disabling renderers and configuring tunneled media playback. See
+ * {@link MappingTrackSelector} for details.
+ *
+ *
Extending this class
+ * This class is designed to be extensible by developers who wish to customize its behavior but do
+ * not wish to implement their own {@link MappingTrackSelector} or {@link TrackSelector} from
+ * scratch.
*/
public class DefaultTrackSelector extends MappingTrackSelector {
/**
- * Holder for available configurations for the {@link DefaultTrackSelector}.
+ * Constraint parameters for {@link DefaultTrackSelector}.
*/
public static final class Parameters {
- // Audio.
+ // Audio
+ /**
+ * The preferred language for audio, as well as for forced text tracks as defined by RFC 5646.
+ * {@code null} selects the default track, or the first track if there's no default.
+ */
public final String preferredAudioLanguage;
- // Text.
+ // Text
+ /**
+ * The preferred language for text tracks as defined by RFC 5646. {@code null} selects the
+ * default track if there is one, or no track otherwise.
+ */
public final String preferredTextLanguage;
- // Video.
- public final boolean allowMixedMimeAdaptiveness;
- public final boolean allowNonSeamlessAdaptiveness;
+ // Video
+ /**
+ * Maximum allowed video width.
+ */
public final int maxVideoWidth;
+ /**
+ * Maximum allowed video height.
+ */
public final int maxVideoHeight;
+ /**
+ * Maximum video bitrate.
+ */
public final int maxVideoBitrate;
+ /**
+ * Whether to exceed video constraints when no selection can be made otherwise.
+ */
public final boolean exceedVideoConstraintsIfNecessary;
- public final boolean exceedRendererCapabilitiesIfNecessary;
+ /**
+ * Viewport width in pixels. Constrains video tracks selections for adaptive playbacks so that
+ * only tracks suitable for the viewport are selected.
+ */
public final int viewportWidth;
+ /**
+ * Viewport height in pixels. Constrains video tracks selections for adaptive playbacks so that
+ * only tracks suitable for the viewport are selected.
+ */
public final int viewportHeight;
- public final boolean orientationMayChange;
+ /**
+ * Whether the viewport orientation may change during playback. Constrains video tracks
+ * selections for adaptive playbacks so that only tracks suitable for the viewport are selected.
+ */
+ public final boolean viewportOrientationMayChange;
+
+ // General
+ /**
+ * Whether to allow adaptive selections containing mixed mime types.
+ */
+ public final boolean allowMixedMimeAdaptiveness;
+ /**
+ * Whether to allow adaptive selections where adaptation may not be completely seamless.
+ */
+ public final boolean allowNonSeamlessAdaptiveness;
+ /**
+ * Whether to exceed renderer capabilities when no selection can be made otherwise.
+ */
+ public final boolean exceedRendererCapabilitiesIfNecessary;
/**
- * Constructor with default selection parameters:
+ * Default parameters. The default values are:
*
*
No preferred audio language is set.
*
No preferred text language is set.
@@ -73,7 +151,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
*
No max video bitrate.
*
Video constraints are exceeded if no supported selection can be made otherwise.
*
Renderer capabilities are exceeded if no supported selection can be made.
- *
No viewport width/height constraints are set.
+ *
No viewport constraints are set.
*
*/
public Parameters() {
@@ -82,29 +160,24 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
- * @param preferredAudioLanguage The preferred language for audio, as well as for forced text
- * tracks as defined by RFC 5646. {@code null} to select the default track, or first track
- * if there's no default.
- * @param preferredTextLanguage The preferred language for text tracks as defined by RFC 5646.
- * {@code null} to select the default track, or first track if there's no default.
- * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types.
- * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed.
- * @param maxVideoWidth Maximum allowed video width.
- * @param maxVideoHeight Maximum allowed video height.
- * @param maxVideoBitrate Maximum allowed video bitrate.
- * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no
- * selection can be made otherwise.
- * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no
- * selection can be made otherwise.
- * @param viewportWidth Viewport width in pixels.
- * @param viewportHeight Viewport height in pixels.
- * @param orientationMayChange Whether orientation may change during playback.
+ * @param preferredAudioLanguage See {@link #preferredAudioLanguage}
+ * @param preferredTextLanguage See {@link #preferredTextLanguage}
+ * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness}
+ * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness}
+ * @param maxVideoWidth See {@link #maxVideoWidth}
+ * @param maxVideoHeight See {@link #maxVideoHeight}
+ * @param maxVideoBitrate See {@link #maxVideoBitrate}
+ * @param exceedVideoConstraintsIfNecessary See {@link #exceedVideoConstraintsIfNecessary}
+ * @param exceedRendererCapabilitiesIfNecessary See {@link #preferredTextLanguage}
+ * @param viewportWidth See {@link #viewportWidth}
+ * @param viewportHeight See {@link #viewportHeight}
+ * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}
*/
public Parameters(String preferredAudioLanguage, String preferredTextLanguage,
boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness,
int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate,
boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary,
- int viewportWidth, int viewportHeight, boolean orientationMayChange) {
+ int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
this.preferredAudioLanguage = preferredAudioLanguage;
this.preferredTextLanguage = preferredTextLanguage;
this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness;
@@ -116,17 +189,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary;
this.viewportWidth = viewportWidth;
this.viewportHeight = viewportHeight;
- this.orientationMayChange = orientationMayChange;
+ this.viewportOrientationMayChange = viewportOrientationMayChange;
}
/**
- * Returns a {@link Parameters} instance with the provided preferred language for audio and
- * forced text tracks.
+ * Returns an instance with the provided preferred language for audio and forced text tracks.
*
* @param preferredAudioLanguage The preferred language as defined by RFC 5646. {@code null} to
* select the default track, or first track if there's no default.
- * @return A {@link Parameters} instance with the provided preferred language for audio and
- * forced text tracks.
+ * @return An instance with the provided preferred language for audio and forced text tracks.
*/
public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) {
preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage);
@@ -136,15 +207,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return new Parameters(preferredAudioLanguage, preferredTextLanguage,
allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
- viewportWidth, viewportHeight, orientationMayChange);
+ viewportWidth, viewportHeight, viewportOrientationMayChange);
}
/**
- * Returns a {@link Parameters} instance with the provided preferred language for text tracks.
+ * Returns an instance with the provided preferred language for text tracks.
*
* @param preferredTextLanguage The preferred language as defined by RFC 5646. {@code null} to
* select the default track, or no track if there's no default.
- * @return A {@link Parameters} instance with the provided preferred language for text tracks.
+ * @return An instance with the provided preferred language for text tracks.
*/
public Parameters withPreferredTextLanguage(String preferredTextLanguage) {
preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage);
@@ -154,14 +225,14 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return new Parameters(preferredAudioLanguage, preferredTextLanguage,
allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
- viewportWidth, viewportHeight, orientationMayChange);
+ viewportWidth, viewportHeight, viewportOrientationMayChange);
}
/**
- * Returns a {@link Parameters} instance with the provided mixed mime adaptiveness allowance.
+ * Returns an instance with the provided mixed mime adaptiveness allowance.
*
* @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types.
- * @return A {@link Parameters} instance with the provided mixed mime adaptiveness allowance.
+ * @return An instance with the provided mixed mime adaptiveness allowance.
*/
public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) {
if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) {
@@ -170,14 +241,14 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return new Parameters(preferredAudioLanguage, preferredTextLanguage,
allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
- viewportWidth, viewportHeight, orientationMayChange);
+ viewportWidth, viewportHeight, viewportOrientationMayChange);
}
/**
- * Returns a {@link Parameters} instance with the provided seamless adaptiveness allowance.
+ * Returns an instance with the provided seamless adaptiveness allowance.
*
* @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed.
- * @return A {@link Parameters} instance with the provided seamless adaptiveness allowance.
+ * @return An instance with the provided seamless adaptiveness allowance.
*/
public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) {
if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) {
@@ -186,15 +257,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return new Parameters(preferredAudioLanguage, preferredTextLanguage,
allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
- viewportWidth, viewportHeight, orientationMayChange);
+ viewportWidth, viewportHeight, viewportOrientationMayChange);
}
/**
- * Returns a {@link Parameters} instance with the provided max video size.
+ * Returns an instance with the provided max video size.
*
* @param maxVideoWidth The max video width.
* @param maxVideoHeight The max video width.
- * @return A {@link Parameters} instance with the provided max video size.
+ * @return An instance with the provided max video size.
*/
public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {
if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) {
@@ -203,14 +274,14 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return new Parameters(preferredAudioLanguage, preferredTextLanguage,
allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
- viewportWidth, viewportHeight, orientationMayChange);
+ viewportWidth, viewportHeight, viewportOrientationMayChange);
}
/**
- * Returns a {@link Parameters} instance with the provided max video bitrate.
+ * Returns an instance with the provided max video bitrate.
*
* @param maxVideoBitrate The max video bitrate.
- * @return A {@link Parameters} instance with the provided max video bitrate.
+ * @return An instance with the provided max video bitrate.
*/
public Parameters withMaxVideoBitrate(int maxVideoBitrate) {
if (maxVideoBitrate == this.maxVideoBitrate) {
@@ -219,13 +290,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return new Parameters(preferredAudioLanguage, preferredTextLanguage,
allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
- viewportWidth, viewportHeight, orientationMayChange);
+ viewportWidth, viewportHeight, viewportOrientationMayChange);
}
/**
* Equivalent to {@code withMaxVideoSize(1279, 719)}.
*
- * @return A {@link Parameters} instance with maximum standard definition as maximum video size.
+ * @return An instance with maximum standard definition as maximum video size.
*/
public Parameters withMaxVideoSizeSd() {
return withMaxVideoSize(1279, 719);
@@ -234,20 +305,18 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/**
* Equivalent to {@code withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}.
*
- * @return A {@link Parameters} instance without video size constraints.
+ * @return An instance without video size constraints.
*/
public Parameters withoutVideoSizeConstraints() {
return withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
/**
- * Returns a {@link Parameters} instance with the provided
- * {@code exceedVideoConstraintsIfNecessary} value.
+ * Returns an instance with the provided {@code exceedVideoConstraintsIfNecessary} value.
*
* @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no
* selection can be made otherwise.
- * @return A {@link Parameters} instance with the provided
- * {@code exceedVideoConstraintsIfNecessary} value.
+ * @return An instance with the provided {@code exceedVideoConstraintsIfNecessary} value.
*/
public Parameters withExceedVideoConstraintsIfNecessary(
boolean exceedVideoConstraintsIfNecessary) {
@@ -257,17 +326,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return new Parameters(preferredAudioLanguage, preferredTextLanguage,
allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
- viewportWidth, viewportHeight, orientationMayChange);
+ viewportWidth, viewportHeight, viewportOrientationMayChange);
}
/**
- * Returns a {@link Parameters} instance with the provided
- * {@code exceedRendererCapabilitiesIfNecessary} value.
+ * Returns an instance with the provided {@code exceedRendererCapabilitiesIfNecessary} value.
*
* @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no
* selection can be made otherwise.
- * @return A {@link Parameters} instance with the provided
- * {@code exceedRendererCapabilitiesIfNecessary} value.
+ * @return An instance with the provided {@code exceedRendererCapabilitiesIfNecessary} value.
*/
public Parameters withExceedRendererCapabilitiesIfNecessary(
boolean exceedRendererCapabilitiesIfNecessary) {
@@ -277,48 +344,47 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return new Parameters(preferredAudioLanguage, preferredTextLanguage,
allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
- viewportWidth, viewportHeight, orientationMayChange);
+ viewportWidth, viewportHeight, viewportOrientationMayChange);
}
/**
- * Returns a {@link Parameters} instance with the provided viewport size.
+ * Returns an instance with the provided viewport size.
*
* @param viewportWidth Viewport width in pixels.
* @param viewportHeight Viewport height in pixels.
- * @param orientationMayChange Whether orientation may change during playback.
- * @return A {@link Parameters} instance with the provided viewport size.
+ * @param viewportOrientationMayChange Whether orientation may change during playback.
+ * @return An instance with the provided viewport size.
*/
public Parameters withViewportSize(int viewportWidth, int viewportHeight,
- boolean orientationMayChange) {
+ boolean viewportOrientationMayChange) {
if (viewportWidth == this.viewportWidth && viewportHeight == this.viewportHeight
- && orientationMayChange == this.orientationMayChange) {
+ && viewportOrientationMayChange == this.viewportOrientationMayChange) {
return this;
}
return new Parameters(preferredAudioLanguage, preferredTextLanguage,
allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
- viewportWidth, viewportHeight, orientationMayChange);
+ viewportWidth, viewportHeight, viewportOrientationMayChange);
}
/**
- * Returns a {@link Parameters} instance where the viewport size is obtained from the provided
- * {@link Context}.
+ * Returns an instance where the viewport size is obtained from the provided {@link Context}.
*
* @param context The context to obtain the viewport size from.
- * @param orientationMayChange Whether orientation may change during playback.
- * @return A {@link Parameters} instance where the viewport size is obtained from the provided
- * {@link Context}.
+ * @param viewportOrientationMayChange Whether orientation may change during playback.
+ * @return An instance where the viewport size is obtained from the provided {@link Context}.
*/
- public Parameters withViewportSizeFromContext(Context context, boolean orientationMayChange) {
+ public Parameters withViewportSizeFromContext(Context context,
+ boolean viewportOrientationMayChange) {
// Assume the viewport is fullscreen.
Point viewportSize = Util.getPhysicalDisplaySize(context);
- return withViewportSize(viewportSize.x, viewportSize.y, orientationMayChange);
+ return withViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange);
}
/**
* Equivalent to {@code withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}.
*
- * @return A {@link Parameters} instance without viewport size constraints.
+ * @return An instance without viewport size constraints.
*/
public Parameters withoutViewportSizeConstraints() {
return withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true);
@@ -338,7 +404,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
&& maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight
&& exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary
&& exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary
- && orientationMayChange == other.orientationMayChange
+ && viewportOrientationMayChange == other.viewportOrientationMayChange
&& viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight
&& maxVideoBitrate == other.maxVideoBitrate
&& TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage)
@@ -356,7 +422,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
result = 31 * result + maxVideoBitrate;
result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0);
result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0);
- result = 31 * result + (orientationMayChange ? 1 : 0);
+ result = 31 * result + (viewportOrientationMayChange ? 1 : 0);
result = 31 * result + viewportWidth;
result = 31 * result + viewportHeight;
return result;
@@ -443,12 +509,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) {
if (!selectedVideoTracks) {
rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i],
- rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth,
- params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness,
- params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight,
- params.orientationMayChange, adaptiveTrackSelectionFactory,
- params.exceedVideoConstraintsIfNecessary,
- params.exceedRendererCapabilitiesIfNecessary);
+ rendererTrackGroupArrays[i], rendererFormatSupports[i], params,
+ adaptiveTrackSelectionFactory);
selectedVideoTracks = rendererTrackSelections[i] != null;
}
seenVideoRendererWithMappedTracks |= rendererTrackGroupArrays[i].length > 0;
@@ -465,8 +527,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
case C.TRACK_TYPE_AUDIO:
if (!selectedAudioTracks) {
rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i],
- rendererFormatSupports[i], params.preferredAudioLanguage,
- params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness,
+ rendererFormatSupports[i], params,
seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory);
selectedAudioTracks = rendererTrackSelections[i] != null;
}
@@ -474,15 +535,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
case C.TRACK_TYPE_TEXT:
if (!selectedTextTracks) {
rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i],
- rendererFormatSupports[i], params.preferredTextLanguage,
- params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary);
+ rendererFormatSupports[i], params);
selectedTextTracks = rendererTrackSelections[i] != null;
}
break;
default:
rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(),
- rendererTrackGroupArrays[i], rendererFormatSupports[i],
- params.exceedRendererCapabilitiesIfNecessary);
+ rendererTrackGroupArrays[i], rendererFormatSupports[i], params);
break;
}
}
@@ -491,42 +550,48 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Video track selection implementation.
+ /**
+ * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to
+ * create a {@link TrackSelection} for a video renderer.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} for the renderer.
+ * @param groups The {@link TrackGroupArray} mapped to the renderer.
+ * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped
+ * track, indexed by track group index and track index (in that order).
+ * @param params The selector's current constraint parameters.
+ * @param adaptiveTrackSelectionFactory A factory for generating adaptive track selections, or
+ * null if a fixed track selection is required.
+ * @return The {@link TrackSelection} for the renderer, or null if no selection was made.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities,
- TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight,
- int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness,
- int viewportWidth, int viewportHeight, boolean orientationMayChange,
- TrackSelection.Factory adaptiveTrackSelectionFactory, boolean exceedConstraintsIfNecessary,
- boolean exceedRendererCapabilitiesIfNecessary) throws ExoPlaybackException {
+ TrackGroupArray groups, int[][] formatSupport, Parameters params,
+ TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException {
TrackSelection selection = null;
if (adaptiveTrackSelectionFactory != null) {
selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport,
- maxVideoWidth, maxVideoHeight, maxVideoBitrate, allowNonSeamlessAdaptiveness,
- allowMixedMimeAdaptiveness, viewportWidth, viewportHeight,
- orientationMayChange, adaptiveTrackSelectionFactory);
+ params, adaptiveTrackSelectionFactory);
}
if (selection == null) {
- selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight,
- maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange,
- exceedConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary);
+ selection = selectFixedVideoTrack(groups, formatSupport, params);
}
return selection;
}
private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities,
- TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight,
- int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness,
- int viewportWidth, int viewportHeight, boolean orientationMayChange,
+ TrackGroupArray groups, int[][] formatSupport, Parameters params,
TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException {
- int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness
+ int requiredAdaptiveSupport = params.allowNonSeamlessAdaptiveness
? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS)
: RendererCapabilities.ADAPTIVE_SEAMLESS;
- boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness
+ boolean allowMixedMimeTypes = params.allowMixedMimeAdaptiveness
&& (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0;
for (int i = 0; i < groups.length; i++) {
TrackGroup group = groups.get(i);
int[] adaptiveTracks = getAdaptiveVideoTracksForGroup(group, formatSupport[i],
- allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight,
- maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange);
+ allowMixedMimeTypes, requiredAdaptiveSupport, params.maxVideoWidth, params.maxVideoHeight,
+ params.maxVideoBitrate, params.viewportWidth, params.viewportHeight,
+ params.viewportOrientationMayChange);
if (adaptiveTracks.length > 0) {
return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks);
}
@@ -537,13 +602,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private static int[] getAdaptiveVideoTracksForGroup(TrackGroup group, int[] formatSupport,
boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth,
int maxVideoHeight, int maxVideoBitrate, int viewportWidth, int viewportHeight,
- boolean orientationMayChange) {
+ boolean viewportOrientationMayChange) {
if (group.length < 2) {
return NO_TRACKS;
}
List selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth,
- viewportHeight, orientationMayChange);
+ viewportHeight, viewportOrientationMayChange);
if (selectedTrackIndices.size() < 2) {
return NO_TRACKS;
}
@@ -614,9 +679,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups,
- int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate,
- int viewportWidth, int viewportHeight, boolean orientationMayChange,
- boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) {
+ int[][] formatSupport, Parameters params) {
TrackGroup selectedGroup = null;
int selectedTrackIndex = 0;
int selectedTrackScore = 0;
@@ -625,16 +688,17 @@ public class DefaultTrackSelector extends MappingTrackSelector {
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
TrackGroup trackGroup = groups.get(groupIndex);
List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup,
- viewportWidth, viewportHeight, orientationMayChange);
+ params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange);
int[] trackFormatSupport = formatSupport[groupIndex];
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
- if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+ if (isSupported(trackFormatSupport[trackIndex],
+ params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex);
boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex)
- && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth)
- && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight)
- && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate);
- if (!isWithinConstraints && !exceedConstraintsIfNecessary) {
+ && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth)
+ && (format.height == Format.NO_VALUE || format.height <= params.maxVideoHeight)
+ && (format.bitrate == Format.NO_VALUE || format.bitrate <= params.maxVideoBitrate);
+ if (!isWithinConstraints && !params.exceedVideoConstraintsIfNecessary) {
// Track should not be selected.
continue;
}
@@ -689,9 +753,21 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Audio track selection implementation.
+ /**
+ * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to
+ * create a {@link TrackSelection} for an audio renderer.
+ *
+ * @param groups The {@link TrackGroupArray} mapped to the renderer.
+ * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped
+ * track, indexed by track group index and track index (in that order).
+ * @param params The selector's current constraint parameters.
+ * @param adaptiveTrackSelectionFactory A factory for generating adaptive track selections, or
+ * null if a fixed track selection is required.
+ * @return The {@link TrackSelection} for the renderer, or null if no selection was made.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport,
- String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary,
- boolean allowMixedMimeAdaptiveness, TrackSelection.Factory adaptiveTrackSelectionFactory) {
+ Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) {
int selectedGroupIndex = C.INDEX_UNSET;
int selectedTrackIndex = C.INDEX_UNSET;
int selectedTrackScore = 0;
@@ -699,10 +775,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
TrackGroup trackGroup = groups.get(groupIndex);
int[] trackFormatSupport = formatSupport[groupIndex];
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
- if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+ if (isSupported(trackFormatSupport[trackIndex],
+ params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex);
int trackScore = getAudioTrackScore(trackFormatSupport[trackIndex],
- preferredAudioLanguage, format);
+ params.preferredAudioLanguage, format);
if (trackScore > selectedTrackScore) {
selectedGroupIndex = groupIndex;
selectedTrackIndex = trackIndex;
@@ -720,7 +797,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
if (adaptiveTrackSelectionFactory != null) {
// If the group of the track with the highest score allows it, try to enable adaptation.
int[] adaptiveTracks = getAdaptiveAudioTracks(selectedGroup,
- formatSupport[selectedGroupIndex], allowMixedMimeAdaptiveness);
+ formatSupport[selectedGroupIndex], params.allowMixedMimeAdaptiveness);
if (adaptiveTracks.length > 0) {
return adaptiveTrackSelectionFactory.createTrackSelection(selectedGroup,
adaptiveTracks);
@@ -804,9 +881,19 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Text track selection implementation.
+ /**
+ * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to
+ * create a {@link TrackSelection} for a text renderer.
+ *
+ * @param groups The {@link TrackGroupArray} mapped to the renderer.
+ * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped
+ * track, indexed by track group index and track index (in that order).
+ * @param params The selector's current constraint parameters.
+ * @return The {@link TrackSelection} for the renderer, or null if no selection was made.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
- String preferredTextLanguage, String preferredAudioLanguage,
- boolean exceedRendererCapabilitiesIfNecessary) {
+ Parameters params) {
TrackGroup selectedGroup = null;
int selectedTrackIndex = 0;
int selectedTrackScore = 0;
@@ -814,12 +901,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
TrackGroup trackGroup = groups.get(groupIndex);
int[] trackFormatSupport = formatSupport[groupIndex];
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
- if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+ if (isSupported(trackFormatSupport[trackIndex],
+ params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex);
boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0;
int trackScore;
- if (formatHasLanguage(format, preferredTextLanguage)) {
+ if (formatHasLanguage(format, params.preferredTextLanguage)) {
if (isDefault) {
trackScore = 6;
} else if (!isForced) {
@@ -833,7 +921,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} else if (isDefault) {
trackScore = 3;
} else if (isForced) {
- if (formatHasLanguage(format, preferredAudioLanguage)) {
+ if (formatHasLanguage(format, params.preferredAudioLanguage)) {
trackScore = 2;
} else {
trackScore = 1;
@@ -859,8 +947,20 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// General track selection methods.
+ /**
+ * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to
+ * create a {@link TrackSelection} for a renderer whose type is neither video, audio or text.
+ *
+ * @param trackType The type of the renderer.
+ * @param groups The {@link TrackGroupArray} mapped to the renderer.
+ * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped
+ * track, indexed by track group index and track index (in that order).
+ * @param params The selector's current constraint parameters.
+ * @return The {@link TrackSelection} for the renderer, or null if no selection was made.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups,
- int[][] formatSupport, boolean exceedRendererCapabilitiesIfNecessary) {
+ int[][] formatSupport, Parameters params) {
TrackGroup selectedGroup = null;
int selectedTrackIndex = 0;
int selectedTrackScore = 0;
@@ -868,7 +968,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
TrackGroup trackGroup = groups.get(groupIndex);
int[] trackFormatSupport = formatSupport[groupIndex];
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
- if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+ if (isSupported(trackFormatSupport[trackIndex],
+ params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex);
boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
int trackScore = isDefault ? 2 : 1;
@@ -887,12 +988,34 @@ public class DefaultTrackSelector extends MappingTrackSelector {
: new FixedTrackSelection(selectedGroup, selectedTrackIndex);
}
+ /**
+ * Applies the {@link RendererCapabilities#FORMAT_SUPPORT_MASK} to a value obtained from
+ * {@link RendererCapabilities#supportsFormat(Format)}, returning true if the result is
+ * {@link RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set
+ * and the result is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.
+ *
+ * @param formatSupport A value obtained from {@link RendererCapabilities#supportsFormat(Format)}.
+ * @param allowExceedsCapabilities Whether to return true if the format support component of the
+ * value is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.
+ * @return True if the format support component is {@link RendererCapabilities#FORMAT_HANDLED}, or
+ * if {@code allowExceedsCapabilities} is set and the format support component is
+ * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.
+ */
protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) {
int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK;
return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities
&& maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES);
}
+ /**
+ * Returns whether a {@link Format} specifies a particular language, or {@code false} if
+ * {@code language} is null.
+ *
+ * @param format The {@link Format}.
+ * @param language The language.
+ * @return Whether the format specifies the language, or {@code false} if {@code language} is
+ * null.
+ */
protected static boolean formatHasLanguage(Format format, String language) {
return language != null
&& TextUtils.equals(language, Util.normalizeLanguageCode(format.language));
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
index 3499efdb16..45ac9eab6e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
@@ -20,6 +20,8 @@ import android.util.SparseArray;
import android.util.SparseBooleanArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.RendererConfiguration;
import com.google.android.exoplayer2.source.TrackGroup;
@@ -31,10 +33,260 @@ import java.util.Map;
/**
* Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s
- * and renderers, and then from that mapping create a {@link TrackSelection} for each renderer.
+ * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each
+ * renderer.
+ *
+ *
Track overrides
+ * Mapping track selectors support overriding of track selections for each renderer. To specify an
+ * override for a renderer it's first necessary to obtain the tracks that have been mapped to it:
+ *
+ * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so
+ * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the
+ * player can be used to determine when the current tracks (and therefore the mapping) changes. If
+ * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query
+ * the properties of the available tracks to determine the {@code groupIndex} of the track group you
+ * want to select and the {@code trackIndices} within it. You can then create and set the override:
+ *
+ * where {@code trackSelectionFactory} is a {@link TrackSelection.Factory} for generating concrete
+ * {@link TrackSelection} instances for the override. It's also possible to pass {@code null} as the
+ * selection override if you don't want any tracks to be selected.
+ *
+ * Note that an override applies only when the track groups available to the renderer match the
+ * {@link TrackGroupArray} for which the override was specified. Overrides can be cleared using
+ * the {@code clearSelectionOverride} methods.
+ *
+ *
Disabling renderers
+ * Renderers can be disabled using {@link #setRendererDisabled(int, boolean)}. Disabling a renderer
+ * differs from setting a {@code null} override because the renderer is disabled unconditionally,
+ * whereas a {@code null} override is applied only when the track groups available to the renderer
+ * match the {@link TrackGroupArray} for which it was specified.
+ *
+ *
Tunneling
+ * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks
+ * support it. See {@link #setTunnelingAudioSessionId(int)} for more details.
*/
public abstract class MappingTrackSelector extends TrackSelector {
+ /**
+ * Provides mapped track information for each renderer.
+ */
+ public static final class MappedTrackInfo {
+
+ /**
+ * The renderer does not have any associated tracks.
+ */
+ public static final int RENDERER_SUPPORT_NO_TRACKS = 0;
+ /**
+ * The renderer has associated tracks, but all are of unsupported types.
+ */
+ public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1;
+ /**
+ * The renderer has associated tracks and at least one is of a supported type, but all of the
+ * tracks whose types are supported exceed the renderer's capabilities.
+ */
+ public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2;
+ /**
+ * The renderer has associated tracks and can play at least one of them.
+ */
+ public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3;
+
+ /**
+ * The number of renderers to which tracks are mapped.
+ */
+ public final int length;
+
+ private final int[] rendererTrackTypes;
+ private final TrackGroupArray[] trackGroups;
+ private final int[] mixedMimeTypeAdaptiveSupport;
+ private final int[][][] formatSupport;
+ private final TrackGroupArray unassociatedTrackGroups;
+
+ /**
+ * @param rendererTrackTypes The track type supported by each renderer.
+ * @param trackGroups The {@link TrackGroup}s mapped to each renderer.
+ * @param mixedMimeTypeAdaptiveSupport The result of
+ * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer.
+ * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each
+ * mapped track, indexed by renderer index, track group index and track index (in that
+ * order).
+ * @param unassociatedTrackGroups Any {@link TrackGroup}s not mapped to any renderer.
+ */
+ /* package */ MappedTrackInfo(int[] rendererTrackTypes,
+ TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport,
+ int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) {
+ this.rendererTrackTypes = rendererTrackTypes;
+ this.trackGroups = trackGroups;
+ this.formatSupport = formatSupport;
+ this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport;
+ this.unassociatedTrackGroups = unassociatedTrackGroups;
+ this.length = trackGroups.length;
+ }
+
+ /**
+ * Returns the {@link TrackGroup}s mapped to the renderer at the specified index.
+ *
+ * @param rendererIndex The renderer index.
+ * @return The corresponding {@link TrackGroup}s.
+ */
+ public TrackGroupArray getTrackGroups(int rendererIndex) {
+ return trackGroups[rendererIndex];
+ }
+
+ /**
+ * Returns the extent to which a renderer can play the tracks in the track groups mapped to it.
+ *
+ * @param rendererIndex The renderer index.
+ * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS},
+ * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS},
+ * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.
+ */
+ public int getRendererSupport(int rendererIndex) {
+ int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
+ int[][] rendererFormatSupport = formatSupport[rendererIndex];
+ for (int i = 0; i < rendererFormatSupport.length; i++) {
+ for (int j = 0; j < rendererFormatSupport[i].length; j++) {
+ int trackRendererSupport;
+ switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) {
+ case RendererCapabilities.FORMAT_HANDLED:
+ return RENDERER_SUPPORT_PLAYABLE_TRACKS;
+ case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
+ trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS;
+ break;
+ default:
+ trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS;
+ break;
+ }
+ bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport);
+ }
+ }
+ return bestRendererSupport;
+ }
+
+ /**
+ * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all
+ * renderers of the specified track type. If no renderers exist for the specified type then
+ * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned.
+ *
+ * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants.
+ * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS},
+ * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS},
+ * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.
+ */
+ public int getTrackTypeRendererSupport(int trackType) {
+ int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
+ for (int i = 0; i < length; i++) {
+ if (rendererTrackTypes[i] == trackType) {
+ bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i));
+ }
+ }
+ return bestRendererSupport;
+ }
+
+ /**
+ * Returns the extent to which an individual track is supported by the renderer.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groupIndex The index of the track group to which the track belongs.
+ * @param trackIndex The index of the track within the track group.
+ * @return One of {@link RendererCapabilities#FORMAT_HANDLED},
+ * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES},
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}.
+ */
+ public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) {
+ return formatSupport[rendererIndex][groupIndex][trackIndex]
+ & RendererCapabilities.FORMAT_SUPPORT_MASK;
+ }
+
+ /**
+ * Returns the extent to which a renderer supports adaptation between supported tracks in a
+ * specified {@link TrackGroup}.
+ *
+ * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+ * {@link RendererCapabilities#FORMAT_HANDLED} are always considered.
+ * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered.
+ * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+ * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if
+ * {@code includeCapabilitiesExceededTracks} is set to {@code true}.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groupIndex The index of the track group.
+ * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the
+ * renderer should be included when determining support. False otherwise.
+ * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS},
+ * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and
+ * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.
+ */
+ public int getAdaptiveSupport(int rendererIndex, int groupIndex,
+ boolean includeCapabilitiesExceededTracks) {
+ int trackCount = trackGroups[rendererIndex].get(groupIndex).length;
+ // Iterate over the tracks in the group, recording the indices of those to consider.
+ int[] trackIndices = new int[trackCount];
+ int trackIndexCount = 0;
+ for (int i = 0; i < trackCount; i++) {
+ int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i);
+ if (fixedSupport == RendererCapabilities.FORMAT_HANDLED
+ || (includeCapabilitiesExceededTracks
+ && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) {
+ trackIndices[trackIndexCount++] = i;
+ }
+ }
+ trackIndices = Arrays.copyOf(trackIndices, trackIndexCount);
+ return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices);
+ }
+
+ /**
+ * Returns the extent to which a renderer supports adaptation between specified tracks within
+ * a {@link TrackGroup}.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groupIndex The index of the track group.
+ * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS},
+ * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and
+ * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.
+ */
+ public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) {
+ int handledTrackCount = 0;
+ int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS;
+ boolean multipleMimeTypes = false;
+ String firstSampleMimeType = null;
+ for (int i = 0; i < trackIndices.length; i++) {
+ int trackIndex = trackIndices[i];
+ String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex)
+ .sampleMimeType;
+ if (handledTrackCount++ == 0) {
+ firstSampleMimeType = sampleMimeType;
+ } else {
+ multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType);
+ }
+ adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i]
+ & RendererCapabilities.ADAPTIVE_SUPPORT_MASK);
+ }
+ return multipleMimeTypes
+ ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex])
+ : adaptiveSupport;
+ }
+
+ /**
+ * Returns {@link TrackGroup}s not mapped to any renderer.
+ */
+ public TrackGroupArray getUnassociatedTrackGroups() {
+ return unassociatedTrackGroups;
+ }
+
+ }
+
/**
* A track selection override.
*/
@@ -47,8 +299,8 @@ public abstract class MappingTrackSelector extends TrackSelector {
/**
* @param factory A factory for creating selections from this override.
- * @param groupIndex The overriding group index.
- * @param tracks The overriding track indices within the group.
+ * @param groupIndex The overriding track group index.
+ * @param tracks The overriding track indices within the track group.
*/
public SelectionOverride(TrackSelection.Factory factory, int groupIndex, int... tracks) {
this.factory = factory;
@@ -60,7 +312,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
/**
* Creates an selection from this override.
*
- * @param groups The groups whose selection is being overridden.
+ * @param groups The track groups whose selection is being overridden.
* @return The selection.
*/
public TrackSelection createTrackSelection(TrackGroupArray groups) {
@@ -94,7 +346,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
}
/**
- * Returns the mapping information associated with the current track selections, or null if no
+ * Returns the mapping information for the currently active track selection, or null if no
* selection is currently active.
*/
public final MappedTrackInfo getCurrentMappedTrackInfo() {
@@ -102,7 +354,8 @@ public abstract class MappingTrackSelector extends TrackSelector {
}
/**
- * Sets whether the renderer at the specified index is disabled.
+ * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents the
+ * selector from selecting any tracks for it.
*
* @param rendererIndex The renderer index.
* @param disabled Whether the renderer is disabled.
@@ -127,16 +380,22 @@ public abstract class MappingTrackSelector extends TrackSelector {
}
/**
- * Overrides the track selection for the renderer at a specified index.
+ * Overrides the track selection for the renderer at the specified index.
*
- * When the {@link TrackGroupArray} available to the renderer at the specified index matches the
- * one provided, the override is applied. When the {@link TrackGroupArray} does not match, the
- * override has no effect. The override replaces any previous override for the renderer and the
- * provided {@link TrackGroupArray}.
+ * When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the override
+ * is applied. When the {@link TrackGroupArray} does not match, the override has no effect. The
+ * override replaces any previous override for the specified {@link TrackGroupArray} for the
+ * specified {@link Renderer}.
*
- * Passing a {@code null} override will explicitly disable the renderer. To remove overrides use
- * {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link #clearSelectionOverrides(int)}
- * or {@link #clearSelectionOverrides()}.
+ * Passing a {@code null} override will cause the renderer to be disabled when the
+ * {@link TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray}
+ * does not match a {@code null} override has no effect. Hence a {@code null} override differs
+ * from disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the
+ * renderer is disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as
+ * {@link #setRendererDisabled(int, boolean)} disables the renderer unconditionally.
+ *
+ * To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)},
+ * {@link #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}.
*
* @param rendererIndex The renderer index.
* @param groups The {@link TrackGroupArray} for which the override should be applied.
@@ -201,7 +460,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
}
/**
- * Clears all track selection override for the specified renderer.
+ * Clears all track selection overrides for the specified renderer.
*
* @param rendererIndex The renderer index.
*/
@@ -216,7 +475,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
}
/**
- * Clears all track selection overrides.
+ * Clears all track selection overrides for all renderers.
*/
public final void clearSelectionOverrides() {
if (selectionOverrides.size() == 0) {
@@ -338,15 +597,15 @@ public abstract class MappingTrackSelector extends TrackSelector {
}
/**
- * Given an array of renderers and a set of {@link TrackGroup}s mapped to each of them, provides a
- * {@link TrackSelection} per renderer.
+ * Given an array of renderer capabilities and the {@link TrackGroupArray}s mapped to each of
+ * them, provides a {@link TrackSelection} per renderer.
*
* @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which
* {@link TrackSelection}s are to be generated.
- * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry
- * corresponds to the renderer of equal index in {@code renderers}.
- * @param rendererFormatSupports Maps every available track to a specific level of support as
- * defined by the renderer {@code FORMAT_*} constants.
+ * @param rendererTrackGroupArrays The {@link TrackGroupArray}s mapped to each of the renderers.
+ * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for
+ * each mapped track, indexed by renderer index, track group index and track index (in that
+ * order).
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
protected abstract TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities,
@@ -354,9 +613,9 @@ public abstract class MappingTrackSelector extends TrackSelector {
throws ExoPlaybackException;
/**
- * Finds the renderer to which the provided {@link TrackGroup} should be associated.
+ * Finds the renderer to which the provided {@link TrackGroup} should be mapped.
*
- * A {@link TrackGroup} is associated to a renderer that reports
+ * A {@link TrackGroup} is mapped to the renderer that reports
* {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group,
* or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or
* {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In
@@ -364,13 +623,13 @@ public abstract class MappingTrackSelector extends TrackSelector {
* lowest index is associated.
*
* If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the
- * tracks in the group, then {@code renderers.length} is returned to indicate that no association
- * was made.
+ * tracks in the group, then {@code renderers.length} is returned to indicate that the group was
+ * not mapped to any renderer.
*
* @param rendererCapabilities The {@link RendererCapabilities} of the renderers.
- * @param group The {@link TrackGroup} whose associated renderer is to be found.
- * @return The index of the associated renderer, or {@code renderers.length} if no
- * association was made.
+ * @param group The track group to map to a renderer.
+ * @return The index of the renderer to which the track group was mapped, or
+ * {@code renderers.length} if it was not mapped to any renderer.
* @throws ExoPlaybackException If an error occurs finding a renderer.
*/
private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group)
@@ -400,7 +659,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
* {@link TrackGroup}, returning the results in an array.
*
* @param rendererCapabilities The {@link RendererCapabilities} of the renderer.
- * @param group The {@link TrackGroup} to evaluate.
+ * @param group The track group to evaluate.
* @return An array containing the result of calling
* {@link RendererCapabilities#supportsFormat} for each track in the group.
* @throws ExoPlaybackException If an error occurs determining the format support.
@@ -520,214 +779,4 @@ public abstract class MappingTrackSelector extends TrackSelector {
return true;
}
- /**
- * Provides track information for each renderer.
- */
- public static final class MappedTrackInfo {
-
- /**
- * The renderer does not have any associated tracks.
- */
- public static final int RENDERER_SUPPORT_NO_TRACKS = 0;
- /**
- * The renderer has associated tracks, but all are of unsupported types.
- */
- public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1;
- /**
- * The renderer has associated tracks and at least one is of a supported type, but all of the
- * tracks whose types are supported exceed the renderer's capabilities.
- */
- public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2;
- /**
- * The renderer has associated tracks and can play at least one of them.
- */
- public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3;
-
- /**
- * The number of renderers to which tracks are mapped.
- */
- public final int length;
-
- private final int[] rendererTrackTypes;
- private final TrackGroupArray[] trackGroups;
- private final int[] mixedMimeTypeAdaptiveSupport;
- private final int[][][] formatSupport;
- private final TrackGroupArray unassociatedTrackGroups;
-
- /**
- * @param rendererTrackTypes The track type supported by each renderer.
- * @param trackGroups The {@link TrackGroupArray}s for each renderer.
- * @param mixedMimeTypeAdaptiveSupport The result of
- * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer.
- * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each
- * track, indexed by renderer index, group index and track index (in that order).
- * @param unassociatedTrackGroups Contains {@link TrackGroup}s not associated with any renderer.
- */
- /* package */ MappedTrackInfo(int[] rendererTrackTypes,
- TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport,
- int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) {
- this.rendererTrackTypes = rendererTrackTypes;
- this.trackGroups = trackGroups;
- this.formatSupport = formatSupport;
- this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport;
- this.unassociatedTrackGroups = unassociatedTrackGroups;
- this.length = trackGroups.length;
- }
-
- /**
- * Returns the array of {@link TrackGroup}s associated to the renderer at a specified index.
- *
- * @param rendererIndex The renderer index.
- * @return The corresponding {@link TrackGroup}s.
- */
- public TrackGroupArray getTrackGroups(int rendererIndex) {
- return trackGroups[rendererIndex];
- }
-
- /**
- * Returns the extent to which a renderer can support playback of the tracks associated to it.
- *
- * @param rendererIndex The renderer index.
- * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS},
- * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS},
- * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.
- */
- public int getRendererSupport(int rendererIndex) {
- int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
- int[][] rendererFormatSupport = formatSupport[rendererIndex];
- for (int i = 0; i < rendererFormatSupport.length; i++) {
- for (int j = 0; j < rendererFormatSupport[i].length; j++) {
- int trackRendererSupport;
- switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) {
- case RendererCapabilities.FORMAT_HANDLED:
- return RENDERER_SUPPORT_PLAYABLE_TRACKS;
- case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
- trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS;
- break;
- default:
- trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS;
- break;
- }
- bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport);
- }
- }
- return bestRendererSupport;
- }
-
- /**
- * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all
- * renderers of the specified track type. If no renderers exist for the specified type then
- * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned.
- *
- * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants.
- * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS},
- * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS},
- * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.
- */
- public int getTrackTypeRendererSupport(int trackType) {
- int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
- for (int i = 0; i < length; i++) {
- if (rendererTrackTypes[i] == trackType) {
- bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i));
- }
- }
- return bestRendererSupport;
- }
-
- /**
- * Returns the extent to which the format of an individual track is supported by the renderer.
- *
- * @param rendererIndex The renderer index.
- * @param groupIndex The index of the group to which the track belongs.
- * @param trackIndex The index of the track within the group.
- * @return One of {@link RendererCapabilities#FORMAT_HANDLED},
- * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES},
- * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and
- * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}.
- */
- public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) {
- return formatSupport[rendererIndex][groupIndex][trackIndex]
- & RendererCapabilities.FORMAT_SUPPORT_MASK;
- }
-
- /**
- * Returns the extent to which the renderer supports adaptation between supported tracks in a
- * specified {@link TrackGroup}.
- *
- * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
- * {@link RendererCapabilities#FORMAT_HANDLED} are always considered.
- * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
- * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or
- * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered.
- * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
- * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if
- * {@code includeCapabilitiesExceededTracks} is set to {@code true}.
- *
- * @param rendererIndex The renderer index.
- * @param groupIndex The index of the group.
- * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the
- * renderer should be included when determining support. False otherwise.
- * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS},
- * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and
- * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.
- */
- public int getAdaptiveSupport(int rendererIndex, int groupIndex,
- boolean includeCapabilitiesExceededTracks) {
- int trackCount = trackGroups[rendererIndex].get(groupIndex).length;
- // Iterate over the tracks in the group, recording the indices of those to consider.
- int[] trackIndices = new int[trackCount];
- int trackIndexCount = 0;
- for (int i = 0; i < trackCount; i++) {
- int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i);
- if (fixedSupport == RendererCapabilities.FORMAT_HANDLED
- || (includeCapabilitiesExceededTracks
- && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) {
- trackIndices[trackIndexCount++] = i;
- }
- }
- trackIndices = Arrays.copyOf(trackIndices, trackIndexCount);
- return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices);
- }
-
- /**
- * Returns the extent to which the renderer supports adaptation between specified tracks within
- * a {@link TrackGroup}.
- *
- * @param rendererIndex The renderer index.
- * @param groupIndex The index of the group.
- * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS},
- * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and
- * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.
- */
- public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) {
- int handledTrackCount = 0;
- int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS;
- boolean multipleMimeTypes = false;
- String firstSampleMimeType = null;
- for (int i = 0; i < trackIndices.length; i++) {
- int trackIndex = trackIndices[i];
- String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex)
- .sampleMimeType;
- if (handledTrackCount++ == 0) {
- firstSampleMimeType = sampleMimeType;
- } else {
- multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType);
- }
- adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i]
- & RendererCapabilities.ADAPTIVE_SUPPORT_MASK);
- }
- return multipleMimeTypes
- ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex])
- : adaptiveSupport;
- }
-
- /**
- * Returns the {@link TrackGroup}s not associated with any renderer.
- */
- public TrackGroupArray getUnassociatedTrackGroups() {
- return unassociatedTrackGroups;
- }
-
- }
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java
index 6c9fbfcb00..a26fee6f78 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java
@@ -16,19 +16,74 @@
package com.google.android.exoplayer2.trackselection;
import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.RendererConfiguration;
import com.google.android.exoplayer2.source.TrackGroupArray;
-/** Selects tracks to be consumed by available renderers. */
+/**
+ * The component of an {@link ExoPlayer} responsible for selecting tracks to be consumed by each of
+ * the player's {@link Renderer}s. The {@link DefaultTrackSelector} implementation should be
+ * suitable for most use cases.
+ *
+ *
Interactions with the player
+ * The following interactions occur between the player and its track selector during playback.
+ *
+ *
+ *
When the player is created it will initialize the track selector by calling
+ * {@link #init(InvalidationListener)}.
+ *
When the player needs to make a track selection it will call
+ * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)}. This typically occurs at the
+ * start of playback, when the player starts to buffer a new period of the media being played,
+ * and when the track selector invalidates its previous selections.
+ *
The player may perform a track selection well in advance of the selected tracks becoming
+ * active, where active is defined to mean that the renderers are actually consuming media
+ * corresponding to the selection that was made. For example when playing media containing
+ * multiple periods, the track selection for a period is made when the player starts to buffer
+ * that period. Hence if the player's buffering policy is to maintain a 30 second buffer, the
+ * selection will occur approximately 30 seconds in advance of it becoming active. In fact the
+ * selection may never become active, for example if the user seeks to some other period of the
+ * media during the 30 second gap. The player indicates to the track selector when a selection
+ * it has previously made becomes active by calling {@link #onSelectionActivated(Object)}.
+ *
If the track selector wishes to indicate to the player that selections it has previously
+ * made are invalid, it can do so by calling
+ * {@link InvalidationListener#onTrackSelectionsInvalidated()} on the
+ * {@link InvalidationListener} that was passed to {@link #init(InvalidationListener)}. A
+ * track selector may wish to do this if its configuration has changed, for example if it now
+ * wishes to prefer audio tracks in a particular language. This will trigger the player to make
+ * new track selections. Note that the player will have to re-buffer in the case that the new
+ * track selection for the currently playing period differs from the one that was invalidated.
+ *
+ *
+ *
+ *
Renderer configuration
+ * The {@link TrackSelectorResult} returned by
+ * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} contains not only
+ * {@link TrackSelection}s for each renderer, but also {@link RendererConfiguration}s defining
+ * configuration parameters that the renderers should apply when consuming the corresponding media.
+ * Whilst it may seem counter-intuitive for a track selector to also specify renderer configuration
+ * information, in practice the two are tightly bound together. It may only be possible to play a
+ * certain combination tracks if the renderers are configured in a particular way. Equally, it may
+ * only be possible to configure renderers in a particular way if certain tracks are selected. Hence
+ * it makes sense to determined the track selection and corresponding renderer configurations in a
+ * single step.
+ *
+ *
Threading model
+ * All calls made by the player into the track selector are on the player's internal playback
+ * thread. The track selector may call {@link InvalidationListener#onTrackSelectionsInvalidated()}
+ * from any thread.
+ */
public abstract class TrackSelector {
/**
- * Notified when previous selections by a {@link TrackSelector} are no longer valid.
+ * Notified when selections previously made by a {@link TrackSelector} are no longer valid.
*/
public interface InvalidationListener {
/**
- * Called by a {@link TrackSelector} when previous selections are no longer valid.
+ * Called by a {@link TrackSelector} to indicate that selections it has previously made are no
+ * longer valid. May be called from any thread.
*/
void onTrackSelectionsInvalidated();
@@ -37,16 +92,17 @@ public abstract class TrackSelector {
private InvalidationListener listener;
/**
- * Initializes the selector.
+ * Called by the player to initialize the selector.
*
- * @param listener A listener for the selector.
+ * @param listener An invalidation listener that the selector can call to indicate that selections
+ * it has previously made are no longer valid.
*/
public final void init(InvalidationListener listener) {
this.listener = listener;
}
/**
- * Performs a track selection for renderers.
+ * Called by the player to perform a track selection.
*
* @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks
* are to be selected.
@@ -58,15 +114,16 @@ public abstract class TrackSelector {
TrackGroupArray trackGroups) throws ExoPlaybackException;
/**
- * Called when a {@link TrackSelectorResult} previously generated by
+ * Called by the player when a {@link TrackSelectorResult} previously generated by
* {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} is activated.
*
- * @param info The value of {@link TrackSelectorResult#info} in the activated result.
+ * @param info The value of {@link TrackSelectorResult#info} in the activated selection.
*/
public abstract void onSelectionActivated(Object info);
/**
- * Invalidates all previously generated track selections.
+ * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously
+ * generated track selections.
*/
protected final void invalidate() {
if (listener != null) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
index 5cdb157570..cab9a689be 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
@@ -25,11 +25,11 @@ import com.google.android.exoplayer2.util.Util;
public final class TrackSelectorResult {
/**
- * The groups provided to the {@link TrackSelector}.
+ * The track groups that were provided to the {@link TrackSelector}.
*/
public final TrackGroupArray groups;
/**
- * A {@link TrackSelectionArray} containing the selection for each renderer.
+ * A {@link TrackSelectionArray} containing the track selection for each renderer.
*/
public final TrackSelectionArray selections;
/**
@@ -43,10 +43,10 @@ public final class TrackSelectorResult {
public final RendererConfiguration[] rendererConfigurations;
/**
- * @param groups The groups provided to the {@link TrackSelector}.
+ * @param groups The track groups provided to the {@link TrackSelector}.
* @param selections A {@link TrackSelectionArray} containing the selection for each renderer.
* @param info An opaque object that will be returned to
- * {@link TrackSelector#onSelectionActivated(Object)} should the selections be activated.
+ * {@link TrackSelector#onSelectionActivated(Object)} should the selection be activated.
* @param rendererConfigurations A {@link RendererConfiguration} for each renderer, to be used
* with the selections.
*/
@@ -62,7 +62,7 @@ public final class TrackSelectorResult {
* Returns whether this result is equivalent to {@code other} for all renderers.
*
* @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}
- * will be returned in all cases.
+ * will be returned.
* @return Whether this result is equivalent to {@code other} for all renderers.
*/
public boolean isEquivalent(TrackSelectorResult other) {
@@ -83,9 +83,10 @@ public final class TrackSelectorResult {
* renderer.
*
* @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}
- * will be returned in all cases.
+ * will be returned.
* @param index The renderer index to check for equivalence.
- * @return Whether this result is equivalent to {@code other} for all renderers.
+ * @return Whether this result is equivalent to {@code other} for the renderer at the specified
+ * index.
*/
public boolean isEquivalent(TrackSelectorResult other, int index) {
if (other == null) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java
index 08b42533cc..f5aa81f325 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java
@@ -25,29 +25,22 @@ public final class Allocation {
/**
* The array containing the allocated space. The allocated space might not be at the start of the
- * array, and so {@link #translateOffset(int)} method must be used when indexing into it.
+ * array, and so {@link #offset} must be used when indexing into it.
*/
public final byte[] data;
- private final int offset;
+ /**
+ * The offset of the allocated space in {@link #data}.
+ */
+ public final int offset;
/**
* @param data The array containing the allocated space.
- * @param offset The offset of the allocated space within the array.
+ * @param offset The offset of the allocated space in {@code data}.
*/
public Allocation(byte[] data, int offset) {
this.data = data;
this.offset = offset;
}
- /**
- * Translates a zero-based offset into the allocation to the corresponding {@link #data} offset.
- *
- * @param offset The zero-based offset to translate.
- * @return The corresponding offset in {@link #data}.
- */
- public int translateOffset(int offset) {
- return this.offset + offset;
- }
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
index d3c63b4454..ab1542c7a6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -68,7 +68,7 @@ public final class DataSpec {
* The position of the data when read from {@link #uri}.
*
* Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location
- * of a subset of the underyling data.
+ * of a subset of the underlying data.
*/
public final long position;
/**
@@ -187,4 +187,31 @@ public final class DataSpec {
+ ", " + position + ", " + length + ", " + key + ", " + flags + "]";
}
+ /**
+ * Returns a {@link DataSpec} that represents a subrange of the data defined by this DataSpec. The
+ * subrange includes data from the offset up to the end of this DataSpec.
+ *
+ * @param offset The offset of the subrange.
+ * @return A {@link DataSpec} that represents a subrange of the data defined by this DataSpec.
+ */
+ public DataSpec subrange(long offset) {
+ return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset);
+ }
+
+ /**
+ * Returns a {@link DataSpec} that represents a subrange of the data defined by this DataSpec.
+ *
+ * @param offset The offset of the subrange.
+ * @param length The length of the subrange.
+ * @return A {@link DataSpec} that represents a subrange of the data defined by this DataSpec.
+ */
+ public DataSpec subrange(long offset, long length) {
+ if (offset == 0 && this.length == length) {
+ return this;
+ } else {
+ return new DataSpec(uri, postBody, absoluteStreamPosition + offset, position + offset, length,
+ key, flags);
+ }
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java
index 20f28e7a7d..db04b2580e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java
@@ -16,8 +16,8 @@
package com.google.android.exoplayer2.upstream;
import android.os.Handler;
-import android.os.SystemClock;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.SlidingPercentile;
/**
@@ -37,6 +37,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
private final Handler eventHandler;
private final EventListener eventListener;
private final SlidingPercentile slidingPercentile;
+ private final Clock clock;
private int streamCount;
private long sampleStartTimeMs;
@@ -55,9 +56,15 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
}
public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) {
+ this(eventHandler, eventListener, maxWeight, Clock.DEFAULT);
+ }
+
+ public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight,
+ Clock clock) {
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.slidingPercentile = new SlidingPercentile(maxWeight);
+ this.clock = clock;
bitrateEstimate = NO_ESTIMATE;
}
@@ -69,7 +76,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
@Override
public synchronized void onTransferStart(Object source, DataSpec dataSpec) {
if (streamCount == 0) {
- sampleStartTimeMs = SystemClock.elapsedRealtime();
+ sampleStartTimeMs = clock.elapsedRealtime();
}
streamCount++;
}
@@ -82,7 +89,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
@Override
public synchronized void onTransferEnd(Object source) {
Assertions.checkState(streamCount > 0);
- long nowMs = SystemClock.elapsedRealtime();
+ long nowMs = clock.elapsedRealtime();
int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
totalElapsedTimeMs += sampleElapsedTimeMs;
totalBytesTransferred += sampleBytesTransferred;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
index 9d13383a56..cbb8ba92a5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
@@ -17,9 +17,11 @@ package com.google.android.exoplayer2.upstream;
import android.content.Context;
import android.net.Uri;
+import android.util.Log;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
/**
* A {@link DataSource} that supports multiple URI schemes. The supported schemes are:
@@ -30,6 +32,8 @@ import java.io.IOException;
* local file URI).
*
asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4).
*
content: For fetching data from a content URI (e.g. content://authority/path/123).
+ *
rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
+ * explicit dependency on ExoPlayer's RTMP extension.
*
http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), if
* constructed using {@link #DefaultDataSource(Context, TransferListener, String, boolean)}, or
* any other schemes supported by a base data source if constructed using
@@ -38,13 +42,22 @@ import java.io.IOException;
*/
public final class DefaultDataSource implements DataSource {
+ private static final String TAG = "DefaultDataSource";
+
private static final String SCHEME_ASSET = "asset";
private static final String SCHEME_CONTENT = "content";
+ private static final String SCHEME_RTMP = "rtmp";
+
+ private final Context context;
+ private final TransferListener super DataSource> listener;
private final DataSource baseDataSource;
- private final DataSource fileDataSource;
- private final DataSource assetDataSource;
- private final DataSource contentDataSource;
+
+ // Lazily initialized.
+ private DataSource fileDataSource;
+ private DataSource assetDataSource;
+ private DataSource contentDataSource;
+ private DataSource rtmpDataSource;
private DataSource dataSource;
@@ -95,10 +108,9 @@ public final class DefaultDataSource implements DataSource {
*/
public DefaultDataSource(Context context, TransferListener super DataSource> listener,
DataSource baseDataSource) {
+ this.context = context.getApplicationContext();
+ this.listener = listener;
this.baseDataSource = Assertions.checkNotNull(baseDataSource);
- this.fileDataSource = new FileDataSource(listener);
- this.assetDataSource = new AssetDataSource(context, listener);
- this.contentDataSource = new ContentDataSource(context, listener);
}
@Override
@@ -108,14 +120,16 @@ public final class DefaultDataSource implements DataSource {
String scheme = dataSpec.uri.getScheme();
if (Util.isLocalFileUri(dataSpec.uri)) {
if (dataSpec.uri.getPath().startsWith("/android_asset/")) {
- dataSource = assetDataSource;
+ dataSource = getAssetDataSource();
} else {
- dataSource = fileDataSource;
+ dataSource = getFileDataSource();
}
} else if (SCHEME_ASSET.equals(scheme)) {
- dataSource = assetDataSource;
+ dataSource = getAssetDataSource();
} else if (SCHEME_CONTENT.equals(scheme)) {
- dataSource = contentDataSource;
+ dataSource = getContentDataSource();
+ } else if (SCHEME_RTMP.equals(scheme)) {
+ dataSource = getRtmpDataSource();
} else {
dataSource = baseDataSource;
}
@@ -144,4 +158,48 @@ public final class DefaultDataSource implements DataSource {
}
}
+ private DataSource getFileDataSource() {
+ if (fileDataSource == null) {
+ fileDataSource = new FileDataSource(listener);
+ }
+ return fileDataSource;
+ }
+
+ private DataSource getAssetDataSource() {
+ if (assetDataSource == null) {
+ assetDataSource = new AssetDataSource(context, listener);
+ }
+ return assetDataSource;
+ }
+
+ private DataSource getContentDataSource() {
+ if (contentDataSource == null) {
+ contentDataSource = new ContentDataSource(context, listener);
+ }
+ return contentDataSource;
+ }
+
+ private DataSource getRtmpDataSource() {
+ if (rtmpDataSource == null) {
+ try {
+ Class> clazz = Class.forName("com.google.android.exoplayer2.ext.rtmp.RtmpDataSource");
+ rtmpDataSource = (DataSource) clazz.getDeclaredConstructor().newInstance();
+ } catch (ClassNotFoundException e) {
+ Log.w(TAG, "Attempting to play RTMP stream without depending on the RTMP extension");
+ } catch (InstantiationException e) {
+ Log.e(TAG, "Error instantiating RtmpDataSource", e);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Error instantiating RtmpDataSource", e);
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Error instantiating RtmpDataSource", e);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "Error instantiating RtmpDataSource", e);
+ }
+ if (rtmpDataSource == null) {
+ rtmpDataSource = baseDataSource;
+ }
+ }
+ return rtmpDataSource;
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java
index 1bdebf7c17..02ccfafa89 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java
@@ -119,17 +119,23 @@ public final class Loader implements LoaderErrorThrower {
}
+ /**
+ * A callback to be notified when a {@link Loader} has finished being released.
+ */
+ public interface ReleaseCallback {
+
+ /**
+ * Called when the {@link Loader} has finished being released.
+ */
+ void onLoaderReleased();
+
+ }
+
public static final int RETRY = 0;
public static final int RETRY_RESET_ERROR_COUNT = 1;
public static final int DONT_RETRY = 2;
public static final int DONT_RETRY_FATAL = 3;
- private static final int MSG_START = 0;
- private static final int MSG_CANCEL = 1;
- private static final int MSG_END_OF_SOURCE = 2;
- private static final int MSG_IO_EXCEPTION = 3;
- private static final int MSG_FATAL_ERROR = 4;
-
private final ExecutorService downloadExecutorService;
private LoadTask extends Loadable> currentTask;
@@ -150,7 +156,7 @@ public final class Loader implements LoaderErrorThrower {
*
* @param The type of the loadable.
* @param loadable The {@link Loadable} to load.
- * @param callback A callback to called when the load ends.
+ * @param callback A callback to be called when the load ends.
* @param defaultMinRetryCount The minimum number of times the load must be retried before
* {@link #maybeThrowError()} will propagate an error.
* @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
@@ -188,20 +194,28 @@ public final class Loader implements LoaderErrorThrower {
}
/**
- * Releases the {@link Loader}, running {@code postLoadAction} on its thread. This method should
- * be called when the {@link Loader} is no longer required.
+ * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer
+ * required.
*
- * @param postLoadAction A {@link Runnable} to run on the loader's thread when
- * {@link Loadable#load()} is no longer running.
+ * @param callback A callback to be called when the release ends. Will be called synchronously
+ * from this method if no load is in progress, or asynchronously once the load has been
+ * canceled otherwise. May be null.
+ * @return True if {@code callback} was called synchronously. False if it will be called
+ * asynchronously or if {@code callback} is null.
*/
- public void release(Runnable postLoadAction) {
+ public boolean release(ReleaseCallback callback) {
+ boolean callbackInvoked = false;
if (currentTask != null) {
currentTask.cancel(true);
- }
- if (postLoadAction != null) {
- downloadExecutorService.execute(postLoadAction);
+ if (callback != null) {
+ downloadExecutorService.execute(new ReleaseTask(callback));
+ }
+ } else if (callback != null) {
+ callback.onLoaderReleased();
+ callbackInvoked = true;
}
downloadExecutorService.shutdown();
+ return callbackInvoked;
}
// LoaderErrorThrower implementation.
@@ -228,6 +242,12 @@ public final class Loader implements LoaderErrorThrower {
private static final String TAG = "LoadTask";
+ private static final int MSG_START = 0;
+ private static final int MSG_CANCEL = 1;
+ private static final int MSG_END_OF_SOURCE = 2;
+ private static final int MSG_IO_EXCEPTION = 3;
+ private static final int MSG_FATAL_ERROR = 4;
+
private final T loadable;
private final Loader.Callback callback;
public final int defaultMinRetryCount;
@@ -390,4 +410,24 @@ public final class Loader implements LoaderErrorThrower {
}
+ private static final class ReleaseTask extends Handler implements Runnable {
+
+ private final ReleaseCallback callback;
+
+ public ReleaseTask(ReleaseCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ sendEmptyMessage(0);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ callback.onLoaderReleased();
+ }
+
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
index bb1f88e5ea..cf2dedbe54 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
@@ -1,16 +1,16 @@
/*
- * Copyright (C) 2017 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,
+ * Copyright (C) 2017 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
+ * See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
@@ -22,28 +22,36 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
import java.io.IOException;
import java.util.NavigableSet;
/**
* Caching related utility methods.
*/
+@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"})
public final class CacheUtil {
- /** Holds the counters used during caching. */
+ /** Counters used during caching. */
public static class CachingCounters {
- /** Total number of already cached bytes. */
- public long alreadyCachedBytes;
+ /** The number of bytes already in the cache. */
+ public volatile long alreadyCachedBytes;
+ /** The number of newly cached bytes. */
+ public volatile long newlyCachedBytes;
+ /** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */
+ public volatile long contentLength = C.LENGTH_UNSET;
+
/**
- * Total number of downloaded bytes.
- *
- *
{@link #getCached(DataSpec, Cache, CachingCounters)} sets it to the count of the missing
- * bytes or to {@link C#LENGTH_UNSET} if {@code dataSpec} is unbounded and content length isn't
- * available in the {@code cache}.
+ * Returns the sum of {@link #alreadyCachedBytes} and {@link #newlyCachedBytes}.
*/
- public long downloadedBytes;
+ public long totalCachedBytes() {
+ return alreadyCachedBytes + newlyCachedBytes;
+ }
}
+ /** Default buffer size to be used while caching. */
+ public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024;
+
/**
* Generates a cache key out of the given {@link Uri}.
*
@@ -64,26 +72,57 @@ public final class CacheUtil {
}
/**
- * Returns already cached and missing bytes in the {@code cache} for the data defined by {@code
- * dataSpec}.
+ * Sets a {@link CachingCounters} to contain the number of bytes already downloaded and the
+ * length for the content defined by a {@code dataSpec}. {@link CachingCounters#newlyCachedBytes}
+ * is reset to 0.
*
* @param dataSpec Defines the data to be checked.
* @param cache A {@link Cache} which has the data.
- * @param counters The counters to be set. If null a new {@link CachingCounters} is created and
- * used.
- * @return The used {@link CachingCounters} instance.
+ * @param counters The {@link CachingCounters} to update.
*/
- public static CachingCounters getCached(DataSpec dataSpec, Cache cache,
- CachingCounters counters) {
- try {
- return internalCache(dataSpec, cache, null, null, null, 0, counters);
- } catch (IOException | InterruptedException e) {
- throw new IllegalStateException(e);
+ public static void getCached(DataSpec dataSpec, Cache cache, CachingCounters counters) {
+ String key = getKey(dataSpec);
+ long start = dataSpec.absoluteStreamPosition;
+ long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key);
+ counters.contentLength = left;
+ counters.alreadyCachedBytes = 0;
+ counters.newlyCachedBytes = 0;
+ while (left != 0) {
+ long blockLength = cache.getCachedBytes(key, start,
+ left != C.LENGTH_UNSET ? left : Long.MAX_VALUE);
+ if (blockLength > 0) {
+ counters.alreadyCachedBytes += blockLength;
+ } else {
+ blockLength = -blockLength;
+ if (blockLength == Long.MAX_VALUE) {
+ return;
+ }
+ }
+ start += blockLength;
+ left -= left == C.LENGTH_UNSET ? 0 : blockLength;
}
}
/**
- * Caches the data defined by {@code dataSpec} while skipping already cached data.
+ * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early
+ * if the end of the input is reached.
+ *
+ * @param dataSpec Defines the data to be cached.
+ * @param cache A {@link Cache} to store the data.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param counters Counters to update during caching.
+ * @throws IOException If an error occurs reading from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public static void cache(DataSpec dataSpec, Cache cache, DataSource upstream,
+ CachingCounters counters) throws IOException, InterruptedException {
+ cache(dataSpec, cache, new CacheDataSource(cache, upstream),
+ new byte[DEFAULT_BUFFER_SIZE_BYTES], null, 0, counters, false);
+ }
+
+ /**
+ * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops
+ * early if end of input is reached and {@code enableEOFException} is false.
*
* @param dataSpec Defines the data to be cached.
* @param cache A {@link Cache} to store the data.
@@ -92,123 +131,109 @@ public final class CacheUtil {
* @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
* caching.
* @param priority The priority of this task. Used with {@code priorityTaskManager}.
- * @param counters The counters to be set during caching. If not null its values reset to
- * zero before using. If null a new {@link CachingCounters} is created and used.
- * @return The used {@link CachingCounters} instance.
+ * @param counters Counters to update during caching.
+ * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been
+ * reached unexpectedly.
* @throws IOException If an error occurs reading from the source.
* @throws InterruptedException If the thread was interrupted.
*/
- public static CachingCounters cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource,
+ public static void cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource,
byte[] buffer, PriorityTaskManager priorityTaskManager, int priority,
- CachingCounters counters) throws IOException, InterruptedException {
+ CachingCounters counters, boolean enableEOFException)
+ throws IOException, InterruptedException {
Assertions.checkNotNull(dataSource);
Assertions.checkNotNull(buffer);
- return internalCache(dataSpec, cache, dataSource, buffer, priorityTaskManager, priority,
- counters);
- }
- /**
- * Caches the data defined by {@code dataSpec} while skipping already cached data. If {@code
- * dataSource} or {@code buffer} is null performs a dry run.
- *
- * @param dataSpec Defines the data to be cached.
- * @param cache A {@link Cache} to store the data.
- * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. If null a dry run
- * is performed.
- * @param buffer The buffer to be used while caching. If null a dry run is performed.
- * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
- * caching.
- * @param priority The priority of this task. Used with {@code priorityTaskManager}.
- * @param counters The counters to be set during caching. If not null its values reset to
- * zero before using. If null a new {@link CachingCounters} is created and used.
- * @return The used {@link CachingCounters} instance.
- * @throws IOException If not dry run and an error occurs reading from the source.
- * @throws InterruptedException If not dry run and the thread was interrupted.
- */
- private static CachingCounters internalCache(DataSpec dataSpec, Cache cache,
- CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager,
- int priority, CachingCounters counters) throws IOException, InterruptedException {
- long start = dataSpec.position;
- long left = dataSpec.length;
- String key = getKey(dataSpec);
- if (left == C.LENGTH_UNSET) {
- left = cache.getContentLength(key);
- if (left == C.LENGTH_UNSET) {
- left = Long.MAX_VALUE;
- }
- }
- if (counters == null) {
- counters = new CachingCounters();
+ if (counters != null) {
+ // Initialize the CachingCounter values.
+ getCached(dataSpec, cache, counters);
} else {
- counters.alreadyCachedBytes = 0;
- counters.downloadedBytes = 0;
+ // Dummy CachingCounters. No need to initialize as they will not be visible to the caller.
+ counters = new CachingCounters();
}
- while (left > 0) {
- long blockLength = cache.getCachedBytes(key, start, left);
- // Skip already cached data
+
+ String key = getKey(dataSpec);
+ long start = dataSpec.absoluteStreamPosition;
+ long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key);
+ while (left != 0) {
+ long blockLength = cache.getCachedBytes(key, start,
+ left != C.LENGTH_UNSET ? left : Long.MAX_VALUE);
if (blockLength > 0) {
- counters.alreadyCachedBytes += blockLength;
+ // Skip already cached data.
} else {
// There is a hole in the cache which is at least "-blockLength" long.
blockLength = -blockLength;
- if (dataSource != null && buffer != null) {
- DataSpec subDataSpec = new DataSpec(dataSpec.uri, start,
- blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength, key);
- long read = readAndDiscard(subDataSpec, dataSource, buffer, priorityTaskManager,
- priority);
- counters.downloadedBytes += read;
- if (read < blockLength) {
- // Reached end of data.
- break;
+ long read = readAndDiscard(dataSpec, start, blockLength, dataSource, buffer,
+ priorityTaskManager, priority, counters);
+ if (read < blockLength) {
+ // Reached to the end of the data.
+ if (enableEOFException && left != C.LENGTH_UNSET) {
+ throw new EOFException();
}
- } else if (blockLength == Long.MAX_VALUE) {
- counters.downloadedBytes = C.LENGTH_UNSET;
break;
- } else {
- counters.downloadedBytes += blockLength;
}
}
start += blockLength;
- if (left != Long.MAX_VALUE) {
- left -= blockLength;
- }
+ left -= left == C.LENGTH_UNSET ? 0 : blockLength;
}
- return counters;
}
/**
* Reads and discards all data specified by the {@code dataSpec}.
*
- * @param dataSpec Defines the data to be read.
+ * @param dataSpec Defines the data to be read. {@code absoluteStreamPosition} and {@code length}
+ * fields are overwritten by the following parameters.
+ * @param absoluteStreamPosition The absolute position of the data to be read.
+ * @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown.
* @param dataSource The {@link DataSource} to read the data from.
* @param buffer The buffer to be used while downloading.
* @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
* caching.
* @param priority The priority of this task.
+ * @param counters Counters to be set during reading.
* @return Number of read bytes, or 0 if no data is available because the end of the opened range
- * has been reached.
+ * has been reached.
*/
- private static long readAndDiscard(DataSpec dataSpec, DataSource dataSource, byte[] buffer,
- PriorityTaskManager priorityTaskManager, int priority)
- throws IOException, InterruptedException {
+ private static long readAndDiscard(DataSpec dataSpec, long absoluteStreamPosition, long length,
+ DataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, int priority,
+ CachingCounters counters) throws IOException, InterruptedException {
while (true) {
if (priorityTaskManager != null) {
// Wait for any other thread with higher priority to finish its job.
priorityTaskManager.proceed(priority);
}
try {
- dataSource.open(dataSpec);
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ // Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in
+ // case the given length exceeds the end of input.
+ dataSpec = new DataSpec(dataSpec.uri, dataSpec.postBody, absoluteStreamPosition,
+ dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition,
+ C.LENGTH_UNSET, dataSpec.key,
+ dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH);
+ long resolvedLength = dataSource.open(dataSpec);
+ if (counters.contentLength == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) {
+ counters.contentLength = dataSpec.absoluteStreamPosition + resolvedLength;
+ }
long totalRead = 0;
- while (true) {
+ while (totalRead != length) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
- int read = dataSource.read(buffer, 0, buffer.length);
+ int read = dataSource.read(buffer, 0,
+ length != C.LENGTH_UNSET ? (int) Math.min(buffer.length, length - totalRead)
+ : buffer.length);
if (read == C.RESULT_END_OF_INPUT) {
- return totalRead;
+ if (counters.contentLength == C.LENGTH_UNSET) {
+ counters.contentLength = dataSpec.absoluteStreamPosition + totalRead;
+ }
+ break;
}
totalRead += read;
+ counters.newlyCachedBytes += read;
}
+ return totalRead;
} catch (PriorityTaskManager.PriorityTooLowException exception) {
// catch and try again
} finally {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
index bbff7dc4a2..2da6ba759b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
@@ -110,7 +110,8 @@ public final class SimpleCache implements Cache {
@Override
public synchronized NavigableSet getCachedSpans(String key) {
CachedContent cachedContent = index.get(key);
- return cachedContent == null ? null : new TreeSet(cachedContent.getSpans());
+ return cachedContent == null || cachedContent.isEmpty() ? null
+ : new TreeSet(cachedContent.getSpans());
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java
index 01f667ed86..f8d5759c2c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java
@@ -16,16 +16,24 @@
package com.google.android.exoplayer2.util;
/**
- * An interface through which system clocks can be read. The {@link SystemClock} implementation
+ * An interface through which system clocks can be read. The {@link #DEFAULT} implementation
* must be used for all non-test cases.
*/
public interface Clock {
/**
- * Returns {@link android.os.SystemClock#elapsedRealtime}.
- *
- * @return Elapsed milliseconds since boot.
+ * Default {@link Clock} to use for all non-test cases.
+ */
+ Clock DEFAULT = new SystemClock();
+
+ /**
+ * @see android.os.SystemClock#elapsedRealtime()
*/
long elapsedRealtime();
+ /**
+ * @see android.os.SystemClock#sleep(long)
+ */
+ void sleep(long sleepTimeMs);
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
index db1122dbe7..2d4a1ec96f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
@@ -65,6 +65,7 @@ public final class MimeTypes {
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_SSA = BASE_TYPE_TEXT + "/x-ssa";
public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4";
public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm";
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java
index 0456bcb879..199ceff892 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java
@@ -155,6 +155,9 @@ public final class ParsableBitArray {
* @return An integer whose bottom n bits hold the read data.
*/
public int readBits(int numBits) {
+ if (numBits == 0) {
+ return 0;
+ }
int returnValue = 0;
bitOffset += numBits;
while (bitOffset > 8) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java
new file mode 100644
index 0000000000..53cb051230
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.Player;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Util class for repeat mode handling.
+ */
+public final class RepeatModeUtil {
+
+ /**
+ * Set of repeat toggle modes. Can be combined using bit-wise operations.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE,
+ REPEAT_TOGGLE_MODE_ALL})
+ public @interface RepeatToggleModes {}
+ /**
+ * All repeat mode buttons disabled.
+ */
+ public static final int REPEAT_TOGGLE_MODE_NONE = 0;
+ /**
+ * "Repeat One" button enabled.
+ */
+ public static final int REPEAT_TOGGLE_MODE_ONE = 1;
+ /**
+ * "Repeat All" button enabled.
+ */
+ public static final int REPEAT_TOGGLE_MODE_ALL = 2;
+
+ private RepeatModeUtil() {
+ // Prevent instantiation.
+ }
+
+ /**
+ * Gets the next repeat mode out of {@code enabledModes} starting from {@code currentMode}.
+ *
+ * @param currentMode The current repeat mode.
+ * @param enabledModes Bitmask of enabled modes.
+ * @return The next repeat mode.
+ */
+ public static @Player.RepeatMode int getNextRepeatMode(@Player.RepeatMode int currentMode,
+ int enabledModes) {
+ for (int offset = 1; offset <= 2; offset++) {
+ @Player.RepeatMode int proposedMode = (currentMode + offset) % 3;
+ if (isRepeatModeEnabled(proposedMode, enabledModes)) {
+ return proposedMode;
+ }
+ }
+ return currentMode;
+ }
+
+ /**
+ * Verifies whether a given {@code repeatMode} is enabled in the bitmask {@code enabledModes}.
+ *
+ * @param repeatMode The mode to check.
+ * @param enabledModes The bitmask representing the enabled modes.
+ * @return {@code true} if enabled.
+ */
+ public static boolean isRepeatModeEnabled(@Player.RepeatMode int repeatMode, int enabledModes) {
+ switch (repeatMode) {
+ case Player.REPEAT_MODE_OFF:
+ return true;
+ case Player.REPEAT_MODE_ONE:
+ return (enabledModes & REPEAT_TOGGLE_MODE_ONE) != 0;
+ case Player.REPEAT_MODE_ALL:
+ return (enabledModes & REPEAT_TOGGLE_MODE_ALL) != 0;
+ default:
+ return false;
+ }
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
index b05675f647..1f937b721b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
@@ -18,11 +18,16 @@ package com.google.android.exoplayer2.util;
/**
* The standard implementation of {@link Clock}.
*/
-public final class SystemClock implements Clock {
+/* package */ final class SystemClock implements Clock {
@Override
public long elapsedRealtime() {
return android.os.SystemClock.elapsedRealtime();
}
+ @Override
+ public void sleep(long sleepTimeMs) {
+ android.os.SystemClock.sleep(sleepTimeMs);
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
index 50932cdf48..b958a54244 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
@@ -34,7 +34,6 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
@@ -680,25 +679,6 @@ public final class Util {
return intArray;
}
- /**
- * Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec}
- * that represents the remainder of the data.
- *
- * @param dataSpec The original {@link DataSpec}.
- * @param bytesLoaded The number of bytes already loaded.
- * @return A {@link DataSpec} that represents the remainder of the data.
- */
- public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) {
- if (bytesLoaded == 0) {
- return dataSpec;
- } else {
- long remainingLength = dataSpec.length == C.LENGTH_UNSET ? C.LENGTH_UNSET
- : dataSpec.length - bytesLoaded;
- return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength,
- dataSpec.key, dataSpec.flags);
- }
- }
-
/**
* Returns the integer equal to the big-endian concatenation of the characters in {@code string}
* as bytes. The string must be no more than four characters long.
@@ -816,6 +796,85 @@ public final class Util {
}
}
+ /**
+ * Returns the {@link C.AudioUsage} corresponding to the specified {@link C.StreamType}.
+ */
+ @C.AudioUsage
+ public static int getAudioUsageForStreamType(@C.StreamType int streamType) {
+ switch (streamType) {
+ case C.STREAM_TYPE_ALARM:
+ return C.USAGE_ALARM;
+ case C.STREAM_TYPE_DTMF:
+ return C.USAGE_VOICE_COMMUNICATION_SIGNALLING;
+ case C.STREAM_TYPE_NOTIFICATION:
+ return C.USAGE_NOTIFICATION;
+ case C.STREAM_TYPE_RING:
+ return C.USAGE_NOTIFICATION_RINGTONE;
+ case C.STREAM_TYPE_SYSTEM:
+ return C.USAGE_ASSISTANCE_SONIFICATION;
+ case C.STREAM_TYPE_VOICE_CALL:
+ return C.USAGE_VOICE_COMMUNICATION;
+ case C.STREAM_TYPE_USE_DEFAULT:
+ case C.STREAM_TYPE_MUSIC:
+ default:
+ return C.USAGE_MEDIA;
+ }
+ }
+
+ /**
+ * Returns the {@link C.AudioContentType} corresponding to the specified {@link C.StreamType}.
+ */
+ @C.AudioContentType
+ public static int getAudioContentTypeForStreamType(@C.StreamType int streamType) {
+ switch (streamType) {
+ case C.STREAM_TYPE_ALARM:
+ case C.STREAM_TYPE_DTMF:
+ case C.STREAM_TYPE_NOTIFICATION:
+ case C.STREAM_TYPE_RING:
+ case C.STREAM_TYPE_SYSTEM:
+ return C.CONTENT_TYPE_SONIFICATION;
+ case C.STREAM_TYPE_VOICE_CALL:
+ return C.CONTENT_TYPE_SPEECH;
+ case C.STREAM_TYPE_USE_DEFAULT:
+ case C.STREAM_TYPE_MUSIC:
+ default:
+ return C.CONTENT_TYPE_MUSIC;
+ }
+ }
+
+ /**
+ * Returns the {@link C.StreamType} corresponding to the specified {@link C.AudioUsage}.
+ */
+ @C.StreamType
+ public static int getStreamTypeForAudioUsage(@C.AudioUsage int usage) {
+ switch (usage) {
+ case C.USAGE_MEDIA:
+ case C.USAGE_GAME:
+ case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
+ return C.STREAM_TYPE_MUSIC;
+ case C.USAGE_ASSISTANCE_SONIFICATION:
+ return C.STREAM_TYPE_SYSTEM;
+ case C.USAGE_VOICE_COMMUNICATION:
+ return C.STREAM_TYPE_VOICE_CALL;
+ case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:
+ return C.STREAM_TYPE_DTMF;
+ case C.USAGE_ALARM:
+ return C.STREAM_TYPE_ALARM;
+ case C.USAGE_NOTIFICATION_RINGTONE:
+ return C.STREAM_TYPE_RING;
+ case C.USAGE_NOTIFICATION:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
+ case C.USAGE_NOTIFICATION_EVENT:
+ return C.STREAM_TYPE_NOTIFICATION;
+ case C.USAGE_ASSISTANCE_ACCESSIBILITY:
+ case C.USAGE_UNKNOWN:
+ default:
+ return C.STREAM_TYPE_DEFAULT;
+ }
+ }
+
/**
* Makes a best guess to infer the type from a {@link Uri}.
*
@@ -836,7 +895,7 @@ public final class Util {
*/
@C.ContentType
public static int inferContentType(String fileName) {
- fileName = fileName.toLowerCase();
+ fileName = Util.toLowerInvariant(fileName);
if (fileName.endsWith(".mpd")) {
return C.TYPE_DASH;
} else if (fileName.endsWith(".m3u8")) {
@@ -977,15 +1036,15 @@ public final class Util {
int expectedLength = length - percentCharacterCount * 2;
StringBuilder builder = new StringBuilder(expectedLength);
Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName);
- int endOfLastMatch = 0;
+ int startOfNotEscaped = 0;
while (percentCharacterCount > 0 && matcher.find()) {
char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16);
- builder.append(fileName, endOfLastMatch, matcher.start()).append(unescapedCharacter);
- endOfLastMatch = matcher.end();
+ builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter);
+ startOfNotEscaped = matcher.end();
percentCharacterCount--;
}
- if (endOfLastMatch < length) {
- builder.append(fileName, endOfLastMatch, length);
+ if (startOfNotEscaped < length) {
+ builder.append(fileName, startOfNotEscaped, length);
}
if (builder.length() != expectedLength) {
return null;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
index 23d9941cf3..e32f23fed7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
@@ -41,6 +41,8 @@ import static android.opengl.GLES20.glDeleteTextures;
import static android.opengl.GLES20.glGenTextures;
import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.graphics.SurfaceTexture.OnFrameAvailableListener;
import android.opengl.EGL14;
@@ -68,19 +70,8 @@ public final class DummySurface extends Surface {
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
- /**
- * Whether the device supports secure dummy surfaces.
- */
- public static final boolean SECURE_SUPPORTED;
- static {
- if (Util.SDK_INT >= 17) {
- EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
- String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
- SECURE_SUPPORTED = extensions != null && extensions.contains("EGL_EXT_protected_content");
- } else {
- SECURE_SUPPORTED = false;
- }
- }
+ private static boolean secureSupported;
+ private static boolean secureSupportedInitialized;
/**
* Whether the surface is secure.
@@ -90,18 +81,40 @@ public final class DummySurface extends Surface {
private final DummySurfaceThread thread;
private boolean threadReleased;
+ /**
+ * Returns whether the device supports secure dummy surfaces.
+ *
+ * @param context Any {@link Context}.
+ * @return Whether the device supports secure dummy surfaces.
+ */
+ public static synchronized boolean isSecureSupported(Context context) {
+ if (!secureSupportedInitialized) {
+ if (Util.SDK_INT >= 17) {
+ EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
+ secureSupported = extensions != null && extensions.contains("EGL_EXT_protected_content")
+ && !deviceNeedsSecureDummySurfaceWorkaround(context);
+ }
+ secureSupportedInitialized = true;
+ }
+ return secureSupported;
+ }
+
/**
* Returns a newly created dummy surface. The surface must be released by calling {@link #release}
* when it's no longer required.
*
* Must only be called if {@link Util#SDK_INT} is 17 or higher.
*
+ * @param context Any {@link Context}.
* @param secure Whether a secure surface is required. Must only be requested if
- * {@link #SECURE_SUPPORTED} is {@code true}.
+ * {@link #isSecureSupported(Context)} returns {@code true}.
+ * @throws IllegalStateException If a secure surface is requested on a device for which
+ * {@link #isSecureSupported(Context)} returns {@code false}.
*/
- public static DummySurface newInstanceV17(boolean secure) {
+ public static DummySurface newInstanceV17(Context context, boolean secure) {
assertApiLevel17OrHigher();
- Assertions.checkState(!secure || SECURE_SUPPORTED);
+ Assertions.checkState(!secure || isSecureSupported(context));
DummySurfaceThread thread = new DummySurfaceThread();
return thread.init(secure);
}
@@ -133,6 +146,23 @@ public final class DummySurface extends Surface {
}
}
+ /**
+ * Returns whether the device is known to advertise secure surface textures but not implement them
+ * correctly.
+ *
+ * @param context Any {@link Context}.
+ */
+ private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) {
+ return Util.SDK_INT == 24
+ && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955"))
+ && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager());
+ }
+
+ @TargetApi(24)
+ private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) {
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE);
+ }
+
private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener,
Callback {
@@ -255,8 +285,8 @@ public final class DummySurface extends Surface {
if (secure) {
glAttributes = new int[] {
EGL_CONTEXT_CLIENT_VERSION, 2,
- EGL_PROTECTED_CONTENT_EXT,
- EGL_TRUE, EGL_NONE};
+ EGL_PROTECTED_CONTENT_EXT, EGL_TRUE,
+ EGL_NONE};
} else {
glAttributes = new int[] {
EGL_CONTEXT_CLIENT_VERSION, 2,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
index fbbcd9a99a..9a2927cc3f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -40,6 +40,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
@@ -62,16 +63,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] {
1920, 1600, 1440, 1280, 960, 854, 640, 540, 480};
+ // Generally there is zero or one pending output stream offset. We track more offsets to allow for
+ // pending output streams that have fewer frames than the codec latency.
+ private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10;
+
+ private final Context context;
private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper;
private final EventDispatcher eventDispatcher;
private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify;
private final boolean deviceNeedsAutoFrcWorkaround;
+ private final long[] pendingOutputStreamOffsetsUs;
private Format[] streamFormats;
private CodecMaxValues codecMaxValues;
private Surface surface;
+ private Surface dummySurface;
@C.VideoScalingMode
private int scalingMode;
private boolean renderedFirstFrame;
@@ -95,6 +103,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private int tunnelingAudioSessionId;
/* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener;
+ private long outputStreamOffsetUs;
+ private int pendingOutputStreamOffsetCount;
+
/**
* @param context A context.
* @param mediaCodecSelector A decoder selector.
@@ -157,9 +168,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
+ this.context = context.getApplicationContext();
frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround();
+ pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
+ outputStreamOffsetUs = C.TIME_UNSET;
joiningDeadlineMs = C.TIME_UNSET;
currentWidth = Format.NO_VALUE;
currentHeight = Format.NO_VALUE;
@@ -219,9 +233,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
@Override
- protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
streamFormats = formats;
- super.onStreamChanged(formats);
+ if (outputStreamOffsetUs == C.TIME_UNSET) {
+ outputStreamOffsetUs = offsetUs;
+ } else {
+ if (pendingOutputStreamOffsetCount == pendingOutputStreamOffsetsUs.length) {
+ Log.w(TAG, "Too many stream changes, so dropping offset: "
+ + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]);
+ } else {
+ pendingOutputStreamOffsetCount++;
+ }
+ pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs;
+ }
+ super.onStreamChanged(formats, offsetUs);
}
@Override
@@ -229,6 +254,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
super.onPositionReset(positionUs, joining);
clearRenderedFirstFrame();
consecutiveDroppedFrameCount = 0;
+ if (pendingOutputStreamOffsetCount != 0) {
+ outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1];
+ pendingOutputStreamOffsetCount = 0;
+ }
if (joining) {
setJoiningDeadlineMs();
} else {
@@ -238,7 +267,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
public boolean isReady() {
- if ((renderedFirstFrame || super.shouldInitCodec()) && super.isReady()) {
+ if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface)
+ || getCodec() == null || tunneling)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineMs = C.TIME_UNSET;
return true;
@@ -275,10 +305,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
currentHeight = Format.NO_VALUE;
currentPixelWidthHeightRatio = Format.NO_VALUE;
pendingPixelWidthHeightRatio = Format.NO_VALUE;
+ outputStreamOffsetUs = C.TIME_UNSET;
+ pendingOutputStreamOffsetCount = 0;
clearReportedVideoSize();
clearRenderedFirstFrame();
frameReleaseTimeHelper.disable();
tunnelingOnFrameRenderedListener = null;
+ tunneling = false;
try {
super.onDisabled();
} finally {
@@ -303,6 +336,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
private void setSurface(Surface surface) throws ExoPlaybackException {
+ if (surface == null) {
+ // Use a dummy surface if possible.
+ if (dummySurface != null) {
+ surface = dummySurface;
+ } else {
+ MediaCodecInfo codecInfo = getCodecInfo();
+ if (codecInfo != null && shouldUseDummySurface(codecInfo.secure)) {
+ dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure);
+ surface = dummySurface;
+ }
+ }
+ }
// We only need to update the codec if the surface has changed.
if (this.surface != surface) {
this.surface = surface;
@@ -316,7 +361,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maybeInitCodec();
}
}
- if (surface != null) {
+ if (surface != null && surface != dummySurface) {
// If we know the video size, report it again immediately.
maybeRenotifyVideoSizeChanged();
// We haven't rendered to the new surface yet.
@@ -329,17 +374,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
}
- } else if (surface != null) {
- // The surface is unchanged and non-null. If we know the video size and/or have already
- // rendered to the surface, report these again immediately.
+ } else if (surface != null && surface != dummySurface) {
+ // The surface is set and unchanged. If we know the video size and/or have already rendered to
+ // the surface, report these again immediately.
maybeRenotifyVideoSizeChanged();
maybeRenotifyRenderedFirstFrame();
}
}
@Override
- protected boolean shouldInitCodec() {
- return super.shouldInitCodec() && surface != null && surface.isValid();
+ protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
+ return surface != null || shouldUseDummySurface(codecInfo.secure);
}
@Override
@@ -348,12 +393,34 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats);
MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround,
tunnelingAudioSessionId);
+ if (surface == null) {
+ Assertions.checkState(shouldUseDummySurface(codecInfo.secure));
+ if (dummySurface == null) {
+ dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure);
+ }
+ surface = dummySurface;
+ }
codec.configure(mediaFormat, surface, crypto, 0);
if (Util.SDK_INT >= 23 && tunneling) {
tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);
}
}
+ @Override
+ protected void releaseCodec() {
+ try {
+ super.releaseCodec();
+ } finally {
+ if (dummySurface != null) {
+ if (surface == dummySurface) {
+ surface = null;
+ }
+ dummySurface.release();
+ dummySurface = null;
+ }
+ }
+ }
+
@Override
protected void onCodecInitialized(String name, long initializedTimestampMs,
long initializationDurationMs) {
@@ -410,23 +477,42 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Format oldFormat, Format newFormat) {
return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat)
&& newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height
- && newFormat.maxInputSize <= codecMaxValues.inputSize;
+ && getMaxInputSize(newFormat) <= codecMaxValues.inputSize;
}
@Override
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs,
boolean shouldSkip) {
+ while (pendingOutputStreamOffsetCount != 0
+ && bufferPresentationTimeUs >= pendingOutputStreamOffsetsUs[0]) {
+ outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0];
+ pendingOutputStreamOffsetCount--;
+ System.arraycopy(pendingOutputStreamOffsetsUs, 1, pendingOutputStreamOffsetsUs, 0,
+ pendingOutputStreamOffsetCount);
+ }
+ long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;
+
if (shouldSkip) {
- skipOutputBuffer(codec, bufferIndex);
+ skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
+ long earlyUs = bufferPresentationTimeUs - positionUs;
+ if (surface == dummySurface) {
+ // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
+ if (isBufferLate(earlyUs)) {
+ skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
+ return true;
+ }
+ return false;
+ }
+
if (!renderedFirstFrame) {
if (Util.SDK_INT >= 21) {
- renderOutputBufferV21(codec, bufferIndex, System.nanoTime());
+ renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime());
} else {
- renderOutputBuffer(codec, bufferIndex);
+ renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
}
return true;
}
@@ -435,9 +521,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return false;
}
- // Compute how many microseconds it is until the buffer's presentation time.
+ // Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current
+ // iteration of the rendering loop.
long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;
- long earlyUs = bufferPresentationTimeUs - positionUs - elapsedSinceStartOfLoopUs;
+ earlyUs -= elapsedSinceStartOfLoopUs;
// Compute the buffer's desired release time in nanoseconds.
long systemTimeNs = System.nanoTime();
@@ -449,15 +536,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
- // We're more than 30ms late rendering the frame.
- dropOutputBuffer(codec, bufferIndex);
+ dropOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
if (Util.SDK_INT >= 21) {
// Let the underlying framework time the release.
if (earlyUs < 50000) {
- renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs);
+ renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs);
return true;
}
} else {
@@ -473,7 +559,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Thread.currentThread().interrupt();
}
}
- renderOutputBuffer(codec, bufferIndex);
+ renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
}
@@ -491,20 +577,33 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* measured at the start of the current iteration of the rendering loop.
*/
protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) {
- // Drop the frame if we're more than 30ms late rendering the frame.
- return earlyUs < -30000;
+ return isBufferLate(earlyUs);
}
- private void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
+ /**
+ * Skips the output buffer with the specified index.
+ *
+ * @param codec The codec that owns the output buffer.
+ * @param index The index of the output buffer to skip.
+ * @param presentationTimeUs The presentation time of the output buffer, in microseconds.
+ */
+ protected void skipOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {
TraceUtil.beginSection("skipVideoBuffer");
- codec.releaseOutputBuffer(bufferIndex, false);
+ codec.releaseOutputBuffer(index, false);
TraceUtil.endSection();
decoderCounters.skippedOutputBufferCount++;
}
- private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
+ /**
+ * Drops the output buffer with the specified index.
+ *
+ * @param codec The codec that owns the output buffer.
+ * @param index The index of the output buffer to drop.
+ * @param presentationTimeUs The presentation time of the output buffer, in microseconds.
+ */
+ protected void dropOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {
TraceUtil.beginSection("dropVideoBuffer");
- codec.releaseOutputBuffer(bufferIndex, false);
+ codec.releaseOutputBuffer(index, false);
TraceUtil.endSection();
decoderCounters.droppedOutputBufferCount++;
droppedFrames++;
@@ -516,27 +615,50 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
}
- private void renderOutputBuffer(MediaCodec codec, int bufferIndex) {
+ /**
+ * Renders the output buffer with the specified index. This method is only called if the platform
+ * API version of the device is less than 21.
+ *
+ * @param codec The codec that owns the output buffer.
+ * @param index The index of the output buffer to drop.
+ * @param presentationTimeUs The presentation time of the output buffer, in microseconds.
+ */
+ protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer");
- codec.releaseOutputBuffer(bufferIndex, true);
+ codec.releaseOutputBuffer(index, true);
TraceUtil.endSection();
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame();
}
+ /**
+ * Renders the output buffer with the specified index. This method is only called if the platform
+ * API version of the device is 21 or later.
+ *
+ * @param codec The codec that owns the output buffer.
+ * @param index The index of the output buffer to drop.
+ * @param presentationTimeUs The presentation time of the output buffer, in microseconds.
+ * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds.
+ */
@TargetApi(21)
- private void renderOutputBufferV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
+ protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs,
+ long releaseTimeNs) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer");
- codec.releaseOutputBuffer(bufferIndex, releaseTimeNs);
+ codec.releaseOutputBuffer(index, releaseTimeNs);
TraceUtil.endSection();
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame();
}
+ private boolean shouldUseDummySurface(boolean codecIsSecure) {
+ return Util.SDK_INT >= 23 && !tunneling
+ && (!codecIsSecure || DummySurface.isSecureSupported(context));
+ }
+
private void setJoiningDeadlineMs() {
joiningDeadlineMs = allowedJoiningTimeMs > 0
? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
@@ -608,6 +730,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
}
+ private static boolean isBufferLate(long earlyUs) {
+ // Class a buffer as late if it should have been presented more than 30ms ago.
+ return earlyUs < -30000;
+ }
+
@SuppressLint("InlinedApi")
private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues,
boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) {
@@ -727,18 +854,27 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
/**
- * Returns a maximum input size for a given format.
+ * Returns a maximum input buffer size for a given format.
*
* @param format The format.
- * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be
- * determined.
+ * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not
+ * be determined.
*/
private static int getMaxInputSize(Format format) {
if (format.maxInputSize != Format.NO_VALUE) {
- // The format defines an explicit maximum input size.
- return format.maxInputSize;
+ // The format defines an explicit maximum input size. Add the total size of initialization
+ // data buffers, as they may need to be queued in the same input buffer as the largest sample.
+ int totalInitializationDataSize = 0;
+ int initializationDataCount = format.initializationData.size();
+ for (int i = 0; i < initializationDataCount; i++) {
+ totalInitializationDataSize += format.initializationData.get(i).length;
+ }
+ return format.maxInputSize + totalInitializationDataSize;
+ } else {
+ // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of
+ // initialization data.
+ return getMaxInputSize(format.sampleMimeType, format.width, format.height);
}
- return getMaxInputSize(format.sampleMimeType, format.width, format.height);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
index 4771f2572c..ad489c2312 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
@@ -31,6 +31,7 @@ import com.google.android.exoplayer2.C;
@TargetApi(16)
public final class VideoFrameReleaseTimeHelper {
+ private static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1;
private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
@@ -52,26 +53,25 @@ public final class VideoFrameReleaseTimeHelper {
private long frameCount;
/**
- * Constructs an instance that smoothes frame release timestamps but does not align them with
+ * Constructs an instance that smooths frame release timestamps but does not align them with
* the default display's vsync signal.
*/
public VideoFrameReleaseTimeHelper() {
- this(-1 /* Value unused */, false);
+ this(DISPLAY_REFRESH_RATE_UNKNOWN);
}
/**
- * Constructs an instance that smoothes frame release timestamps and aligns them with the default
+ * Constructs an instance that smooths frame release timestamps and aligns them with the default
* display's vsync signal.
*
* @param context A context from which information about the default display can be retrieved.
*/
public VideoFrameReleaseTimeHelper(Context context) {
- this(getDefaultDisplayRefreshRate(context), true);
+ this(getDefaultDisplayRefreshRate(context));
}
- private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate,
- boolean useDefaultDisplayVsync) {
- this.useDefaultDisplayVsync = useDefaultDisplayVsync;
+ private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) {
+ useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN;
if (useDefaultDisplayVsync) {
vsyncSampler = VSyncSampler.getInstance();
vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate);
@@ -200,9 +200,10 @@ public final class VideoFrameReleaseTimeHelper {
return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
}
- private static float getDefaultDisplayRefreshRate(Context context) {
+ private static double getDefaultDisplayRefreshRate(Context context) {
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- return manager.getDefaultDisplay().getRefreshRate();
+ return manager.getDefaultDisplay() != null ? manager.getDefaultDisplay().getRefreshRate()
+ : DISPLAY_REFRESH_RATE_UNKNOWN;
}
/**
diff --git a/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg
new file mode 100644
index 0000000000..a364587320
--- /dev/null
+++ b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg
@@ -0,0 +1,51 @@
+
+
+
diff --git a/library/dash/build.gradle b/library/dash/build.gradle
index ebad5a8603..2220e5b250 100644
--- a/library/dash/build.gradle
+++ b/library/dash/build.gradle
@@ -11,6 +11,7 @@
// 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.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -22,23 +23,20 @@ android {
targetSdkVersion project.ext.targetSdkVersion
}
- sourceSets {
- androidTest {
- java.srcDirs += "../../testutils/src/main/java/"
- }
- }
-
buildTypes {
- debug {
- testCoverageEnabled = true
- }
+ // Re-enable test coverage when the following issue is fixed:
+ // https://issuetracker.google.com/issues/37019591
+ // debug {
+ // testCoverageEnabled = true
+ // }
}
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
compile 'com.android.support:support-annotations:' + supportLibraryVersion
compile 'com.android.support:support-core-utils:' + supportLibraryVersion
+ androidTestCompile project(modulePrefix + 'testutils')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
diff --git a/library/dash/src/androidTest/AndroidManifest.xml b/library/dash/src/androidTest/AndroidManifest.xml
index ac2511d3bd..a9b143253f 100644
--- a/library/dash/src/androidTest/AndroidManifest.xml
+++ b/library/dash/src/androidTest/AndroidManifest.xml
@@ -28,7 +28,6 @@
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java
index c9f1ca1030..bac1c272e8 100644
--- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java
+++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java
@@ -62,7 +62,7 @@ public final class DashUtilTest extends TestCase {
}
private static AdaptationSet newAdaptationSets(Representation... representations) {
- return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null);
+ return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null, null);
}
private static Representation newRepresentations(DrmInitData drmInitData) {
@@ -75,7 +75,7 @@ public final class DashUtilTest extends TestCase {
}
private static DrmInitData newDrmInitData() {
- return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType",
+ return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, null, "mimeType",
new byte[]{1, 4, 7, 0, 3, 6}));
}
diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java
index 5b8760f929..3ce4b37ec6 100644
--- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java
+++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java
@@ -115,12 +115,12 @@ public class DashManifestParserTest extends InstrumentationTestCase {
buildCea708AccessibilityDescriptors("Wrong format")));
}
- private static List buildCea608AccessibilityDescriptors(String value) {
- return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-608:2015", value));
+ private static List buildCea608AccessibilityDescriptors(String value) {
+ return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null));
}
- private static List buildCea708AccessibilityDescriptors(String value) {
- return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-708:2015", value));
+ private static List buildCea708AccessibilityDescriptors(String value) {
+ return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null));
}
}
diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java
index c796025b08..7d77ae82d9 100644
--- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java
+++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java
@@ -30,8 +30,6 @@ import junit.framework.TestCase;
public class DashManifestTest extends TestCase {
private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", "");
- private static final List DUMMY_ACCESSIBILITY_DESCRIPTORS =
- Collections.emptyList();
private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase();
private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0);
@@ -190,8 +188,7 @@ public class DashManifestTest extends TestCase {
}
private static AdaptationSet newAdaptationSet(int seed, Representation... representations) {
- return new AdaptationSet(++seed, ++seed, Arrays.asList(representations),
- DUMMY_ACCESSIBILITY_DESCRIPTORS);
+ return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null);
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java
index 72f728092c..4e25c0e333 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java
@@ -28,8 +28,8 @@ public interface DashChunkSource extends ChunkSource {
interface Factory {
DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
- DashManifest manifest, int periodIndex, int adaptationSetIndex,
- TrackSelection trackSelection, long elapsedRealtimeOffsetMs,
+ DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
+ TrackSelection trackSelection, int type, long elapsedRealtimeOffsetMs,
boolean enableEventMessageTrack, boolean enableCea608Track);
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
index 5e0541cb31..81b4a4ceed 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.dash;
import android.util.Pair;
+import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
@@ -30,13 +31,14 @@ import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.Descriptor;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
-import com.google.android.exoplayer2.source.dash.manifest.SchemeValuePair;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -55,7 +57,7 @@ import java.util.List;
private final LoaderErrorThrower manifestLoaderErrorThrower;
private final Allocator allocator;
private final TrackGroupArray trackGroups;
- private final EmbeddedTrackInfo[] embeddedTrackInfos;
+ private final TrackGroupInfo[] trackGroupInfos;
private Callback callback;
private ChunkSampleStream[] sampleStreams;
@@ -80,9 +82,9 @@ import java.util.List;
sampleStreams = newSampleStreamArray(0);
sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
adaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
- Pair result = buildTrackGroups(adaptationSets);
+ Pair result = buildTrackGroups(adaptationSets);
trackGroups = result.first;
- embeddedTrackInfos = result.second;
+ trackGroupInfos = result.second;
}
public void updateManifest(DashManifest manifest, int periodIndex) {
@@ -104,7 +106,7 @@ import java.util.List;
}
@Override
- public void prepare(Callback callback) {
+ public void prepare(Callback callback, long positionUs) {
this.callback = callback;
callback.onPrepared(this);
}
@@ -122,7 +124,6 @@ import java.util.List;
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
- int adaptationSetCount = adaptationSets.size();
HashMap> primarySampleStreams = new HashMap<>();
// First pass for primary tracks.
for (int i = 0; i < selections.length; i++) {
@@ -133,14 +134,15 @@ import java.util.List;
stream.release();
streams[i] = null;
} else {
- int adaptationSetIndex = trackGroups.indexOf(selections[i].getTrackGroup());
- primarySampleStreams.put(adaptationSetIndex, stream);
+ int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
+ primarySampleStreams.put(trackGroupIndex, stream);
}
}
if (streams[i] == null && selections[i] != null) {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
- if (trackGroupIndex < adaptationSetCount) {
- ChunkSampleStream stream = buildSampleStream(trackGroupIndex,
+ TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
+ if (trackGroupInfo.isPrimary) {
+ ChunkSampleStream stream = buildSampleStream(trackGroupInfo,
selections[i], positionUs);
primarySampleStreams.put(trackGroupIndex, stream);
streams[i] = stream;
@@ -160,11 +162,10 @@ import java.util.List;
// may have been replaced, selected or deselected.
if (selections[i] != null) {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
- if (trackGroupIndex >= adaptationSetCount) {
- int embeddedTrackIndex = trackGroupIndex - adaptationSetCount;
- EmbeddedTrackInfo embeddedTrackInfo = embeddedTrackInfos[embeddedTrackIndex];
- int adaptationSetIndex = embeddedTrackInfo.adaptationSetIndex;
- ChunkSampleStream> primaryStream = primarySampleStreams.get(adaptationSetIndex);
+ TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
+ if (!trackGroupInfo.isPrimary) {
+ ChunkSampleStream> primaryStream = primarySampleStreams.get(
+ trackGroupInfo.primaryTrackGroupIndex);
SampleStream stream = streams[i];
boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream
: (stream instanceof EmbeddedSampleStream
@@ -172,7 +173,7 @@ import java.util.List;
if (!mayRetainStream) {
releaseIfEmbeddedSampleStream(stream);
streams[i] = primaryStream == null ? new EmptySampleStream()
- : primaryStream.selectEmbeddedTrack(positionUs, embeddedTrackInfo.trackType);
+ : primaryStream.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType);
streamResetFlags[i] = true;
}
}
@@ -187,7 +188,7 @@ import java.util.List;
@Override
public void discardBuffer(long positionUs) {
for (ChunkSampleStream sampleStream : sampleStreams) {
- sampleStream.discardUnselectedEmbeddedTracksTo(positionUs);
+ sampleStream.discardEmbeddedTracksTo(positionUs);
}
}
@@ -208,14 +209,7 @@ import java.util.List;
@Override
public long getBufferedPositionUs() {
- long bufferedPositionUs = Long.MAX_VALUE;
- for (ChunkSampleStream sampleStream : sampleStreams) {
- long rendererBufferedPositionUs = sampleStream.getBufferedPositionUs();
- if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) {
- bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
- }
- }
- return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+ return sequenceableLoader.getBufferedPositionUs();
}
@Override
@@ -235,49 +229,114 @@ import java.util.List;
// Internal methods.
- private static Pair buildTrackGroups(
+ private static Pair buildTrackGroups(
List adaptationSets) {
- int adaptationSetCount = adaptationSets.size();
- int embeddedTrackCount = getEmbeddedTrackCount(adaptationSets);
- TrackGroup[] trackGroupArray = new TrackGroup[adaptationSetCount + embeddedTrackCount];
- EmbeddedTrackInfo[] embeddedTrackInfos = new EmbeddedTrackInfo[embeddedTrackCount];
+ int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
- int embeddedTrackIndex = 0;
- for (int i = 0; i < adaptationSetCount; i++) {
- AdaptationSet adaptationSet = adaptationSets.get(i);
- List representations = adaptationSet.representations;
+ int primaryGroupCount = groupedAdaptationSetIndices.length;
+ boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount];
+ boolean[] primaryGroupHasCea608TrackFlags = new boolean[primaryGroupCount];
+ int totalGroupCount = primaryGroupCount;
+ for (int i = 0; i < primaryGroupCount; i++) {
+ if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) {
+ primaryGroupHasEventMessageTrackFlags[i] = true;
+ totalGroupCount++;
+ }
+ if (hasCea608Track(adaptationSets, groupedAdaptationSetIndices[i])) {
+ primaryGroupHasCea608TrackFlags[i] = true;
+ totalGroupCount++;
+ }
+ }
+
+ TrackGroup[] trackGroups = new TrackGroup[totalGroupCount];
+ TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount];
+
+ int trackGroupCount = 0;
+ for (int i = 0; i < primaryGroupCount; i++) {
+ int[] adaptationSetIndices = groupedAdaptationSetIndices[i];
+ List representations = new ArrayList<>();
+ for (int adaptationSetIndex : adaptationSetIndices) {
+ representations.addAll(adaptationSets.get(adaptationSetIndex).representations);
+ }
Format[] formats = new Format[representations.size()];
for (int j = 0; j < formats.length; j++) {
formats[j] = representations.get(j).format;
}
- trackGroupArray[i] = new TrackGroup(formats);
- if (hasEventMessageTrack(adaptationSet)) {
- Format format = Format.createSampleFormat(adaptationSet.id + ":emsg",
+
+ AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]);
+ int primaryTrackGroupIndex = trackGroupCount;
+ boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i];
+ boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i];
+
+ trackGroups[trackGroupCount] = new TrackGroup(formats);
+ trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(firstAdaptationSet.type,
+ adaptationSetIndices, primaryTrackGroupIndex, true, hasEventMessageTrack, hasCea608Track);
+ if (hasEventMessageTrack) {
+ Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg",
MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null);
- trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format);
- embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_METADATA);
+ trackGroups[trackGroupCount] = new TrackGroup(format);
+ trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_METADATA,
+ adaptationSetIndices, primaryTrackGroupIndex, false, false, false);
}
- if (hasCea608Track(adaptationSet)) {
- Format format = Format.createTextSampleFormat(adaptationSet.id + ":cea608",
- MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null);
- trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format);
- embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_TEXT);
+ if (hasCea608Track) {
+ Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608",
+ MimeTypes.APPLICATION_CEA608, 0, null);
+ trackGroups[trackGroupCount] = new TrackGroup(format);
+ trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_TEXT,
+ adaptationSetIndices, primaryTrackGroupIndex, false, false, false);
}
}
- return Pair.create(new TrackGroupArray(trackGroupArray), embeddedTrackInfos);
+ return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos);
}
- private ChunkSampleStream buildSampleStream(int adaptationSetIndex,
+ private static int[][] getGroupedAdaptationSetIndices(List adaptationSets) {
+ int adaptationSetCount = adaptationSets.size();
+ SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount);
+ for (int i = 0; i < adaptationSetCount; i++) {
+ idToIndexMap.put(adaptationSets.get(i).id, i);
+ }
+
+ int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][];
+ boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount];
+
+ int groupCount = 0;
+ for (int i = 0; i < adaptationSetCount; i++) {
+ if (adaptationSetUsedFlags[i]) {
+ // This adaptation set has already been included in a group.
+ continue;
+ }
+ adaptationSetUsedFlags[i] = true;
+ Descriptor adaptationSetSwitchingProperty = findAdaptationSetSwitchingProperty(
+ adaptationSets.get(i).supplementalProperties);
+ if (adaptationSetSwitchingProperty == null) {
+ groupedAdaptationSetIndices[groupCount++] = new int[] {i};
+ } else {
+ String[] extraAdaptationSetIds = adaptationSetSwitchingProperty.value.split(",");
+ int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length];
+ adaptationSetIndices[0] = i;
+ for (int j = 0; j < extraAdaptationSetIds.length; j++) {
+ int extraIndex = idToIndexMap.get(Integer.parseInt(extraAdaptationSetIds[j]));
+ adaptationSetUsedFlags[extraIndex] = true;
+ adaptationSetIndices[1 + j] = extraIndex;
+ }
+ groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices;
+ }
+ }
+
+ return groupCount < adaptationSetCount
+ ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices;
+ }
+
+ private ChunkSampleStream buildSampleStream(TrackGroupInfo trackGroupInfo,
TrackSelection selection, long positionUs) {
- AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex);
int embeddedTrackCount = 0;
int[] embeddedTrackTypes = new int[2];
- boolean enableEventMessageTrack = hasEventMessageTrack(adaptationSet);
+ boolean enableEventMessageTrack = trackGroupInfo.hasEmbeddedEventMessageTrack;
if (enableEventMessageTrack) {
embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_METADATA;
}
- boolean enableCea608Track = hasCea608Track(adaptationSet);
+ boolean enableCea608Track = trackGroupInfo.hasEmbeddedCea608Track;
if (enableCea608Track) {
embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_TEXT;
}
@@ -285,45 +344,48 @@ import java.util.List;
embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount);
}
DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource(
- manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, selection,
- elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track);
- ChunkSampleStream stream = new ChunkSampleStream<>(adaptationSet.type,
+ manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices,
+ selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack,
+ enableCea608Track);
+ ChunkSampleStream stream = new ChunkSampleStream<>(trackGroupInfo.trackType,
embeddedTrackTypes, chunkSource, this, allocator, positionUs, minLoadableRetryCount,
eventDispatcher);
return stream;
}
- private static int getEmbeddedTrackCount(List adaptationSets) {
- int embeddedTrackCount = 0;
- for (int i = 0; i < adaptationSets.size(); i++) {
- AdaptationSet adaptationSet = adaptationSets.get(i);
- if (hasEventMessageTrack(adaptationSet)) {
- embeddedTrackCount++;
- }
- if (hasCea608Track(adaptationSet)) {
- embeddedTrackCount++;
+ private static Descriptor findAdaptationSetSwitchingProperty(List descriptors) {
+ for (int i = 0; i < descriptors.size(); i++) {
+ Descriptor descriptor = descriptors.get(i);
+ if ("urn:mpeg:dash:adaptation-set-switching:2016".equals(descriptor.schemeIdUri)) {
+ return descriptor;
}
}
- return embeddedTrackCount;
+ return null;
}
- private static boolean hasEventMessageTrack(AdaptationSet adaptationSet) {
- List representations = adaptationSet.representations;
- for (int i = 0; i < representations.size(); i++) {
- Representation representation = representations.get(i);
- if (!representation.inbandEventStreams.isEmpty()) {
- return true;
+ private static boolean hasEventMessageTrack(List adaptationSets,
+ int[] adaptationSetIndices) {
+ for (int i : adaptationSetIndices) {
+ List representations = adaptationSets.get(i).representations;
+ for (int j = 0; j < representations.size(); j++) {
+ Representation representation = representations.get(j);
+ if (!representation.inbandEventStreams.isEmpty()) {
+ return true;
+ }
}
}
return false;
}
- private static boolean hasCea608Track(AdaptationSet adaptationSet) {
- List descriptors = adaptationSet.accessibilityDescriptors;
- for (int i = 0; i < descriptors.size(); i++) {
- SchemeValuePair descriptor = descriptors.get(i);
- if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) {
- return true;
+ private static boolean hasCea608Track(List adaptationSets,
+ int[] adaptationSetIndices) {
+ for (int i : adaptationSetIndices) {
+ List descriptors = adaptationSets.get(i).accessibilityDescriptors;
+ for (int j = 0; j < descriptors.size(); j++) {
+ Descriptor descriptor = descriptors.get(j);
+ if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) {
+ return true;
+ }
}
}
return false;
@@ -340,14 +402,24 @@ import java.util.List;
}
}
- private static final class EmbeddedTrackInfo {
+ private static final class TrackGroupInfo {
- public final int adaptationSetIndex;
+ public final int[] adaptationSetIndices;
public final int trackType;
+ public final boolean isPrimary;
- public EmbeddedTrackInfo(int adaptationSetIndex, int trackType) {
- this.adaptationSetIndex = adaptationSetIndex;
+ public final int primaryTrackGroupIndex;
+ public final boolean hasEmbeddedEventMessageTrack;
+ public final boolean hasEmbeddedCea608Track;
+
+ public TrackGroupInfo(int trackType, int[] adaptationSetIndices, int primaryTrackGroupIndex,
+ boolean isPrimary, boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track) {
this.trackType = trackType;
+ this.adaptationSetIndices = adaptationSetIndices;
+ this.primaryTrackGroupIndex = primaryTrackGroupIndex;
+ this.isPrimary = isPrimary;
+ this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack;
+ this.hasEmbeddedCea608Track = hasEmbeddedCea608Track;
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
index cd995f3739..315e87dcd3 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
@@ -22,6 +22,7 @@ import android.util.Log;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
@@ -52,6 +53,10 @@ import java.util.TimeZone;
*/
public final class DashMediaSource implements MediaSource {
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.dash");
+ }
+
/**
* The default minimum number of times to retry loading data prior to failing.
*/
@@ -281,7 +286,8 @@ public final class DashMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int periodIndex, Allocator allocator, long positionUs) {
+ public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) {
+ int periodIndex = periodId.periodIndex;
EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs(
manifest.getPeriod(periodIndex).startMs);
DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest,
@@ -627,9 +633,9 @@ public final class DashMediaSource implements MediaSource {
private final long windowDefaultStartPositionUs;
private final DashManifest manifest;
- public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs,
- int firstPeriodId, long offsetInFirstPeriodUs, long windowDurationUs,
- long windowDefaultStartPositionUs, DashManifest manifest) {
+ public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs, int firstPeriodId,
+ long offsetInFirstPeriodUs, long windowDurationUs, long windowDefaultStartPositionUs,
+ DashManifest manifest) {
this.presentationStartTimeMs = presentationStartTimeMs;
this.windowStartTimeMs = windowStartTimeMs;
this.firstPeriodId = firstPeriodId;
@@ -652,7 +658,7 @@ public final class DashMediaSource implements MediaSource {
+ Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null;
return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex),
C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs)
- - offsetInFirstPeriodUs, false);
+ - offsetInFirstPeriodUs);
}
@Override
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
index e679ef635c..297052f65a 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
@@ -46,6 +46,7 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -69,20 +70,21 @@ public class DefaultDashChunkSource implements DashChunkSource {
@Override
public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
- DashManifest manifest, int periodIndex, int adaptationSetIndex,
- TrackSelection trackSelection, long elapsedRealtimeOffsetMs,
+ DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
+ TrackSelection trackSelection, int trackType, long elapsedRealtimeOffsetMs,
boolean enableEventMessageTrack, boolean enableCea608Track) {
DataSource dataSource = dataSourceFactory.createDataSource();
return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex,
- adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs,
+ adaptationSetIndices, trackSelection, trackType, dataSource, elapsedRealtimeOffsetMs,
maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track);
}
}
private final LoaderErrorThrower manifestLoaderErrorThrower;
- private final int adaptationSetIndex;
+ private final int[] adaptationSetIndices;
private final TrackSelection trackSelection;
+ private final int trackType;
private final RepresentationHolder[] representationHolders;
private final DataSource dataSource;
private final long elapsedRealtimeOffsetMs;
@@ -98,8 +100,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest.
* @param periodIndex The index of the period in the manifest.
- * @param adaptationSetIndex The index of the adaptation set in the period.
+ * @param adaptationSetIndices The indices of the adaptation sets in the period.
* @param trackSelection The track selection.
+ * @param trackType The type of the tracks in the selection.
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
* server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
@@ -112,26 +115,27 @@ public class DefaultDashChunkSource implements DashChunkSource {
* @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track.
*/
public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
- DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection,
- DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad,
- boolean enableEventMessageTrack, boolean enableCea608Track) {
+ DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
+ TrackSelection trackSelection, int trackType, DataSource dataSource,
+ long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack,
+ boolean enableCea608Track) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest;
- this.adaptationSetIndex = adaptationSetIndex;
+ this.adaptationSetIndices = adaptationSetIndices;
this.trackSelection = trackSelection;
+ this.trackType = trackType;
this.dataSource = dataSource;
this.periodIndex = periodIndex;
this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
this.maxSegmentsPerLoad = maxSegmentsPerLoad;
long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
- AdaptationSet adaptationSet = getAdaptationSet();
- List representations = adaptationSet.representations;
+ List representations = getRepresentations();
representationHolders = new RepresentationHolder[trackSelection.length()];
for (int i = 0; i < representationHolders.length; i++) {
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
representationHolders[i] = new RepresentationHolder(periodDurationUs, representation,
- enableEventMessageTrack, enableCea608Track, adaptationSet.type);
+ enableEventMessageTrack, enableCea608Track);
}
}
@@ -141,7 +145,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
manifest = newManifest;
periodIndex = newPeriodIndex;
long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
- List representations = getAdaptationSet().representations;
+ List representations = getRepresentations();
for (int i = 0; i < representationHolders.length; i++) {
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
representationHolders[i].updateRepresentation(periodDurationUs, representation);
@@ -248,9 +252,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);
- out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(),
- trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segmentNum,
- maxSegmentCount);
+ out.chunk = newMediaChunk(representationHolder, dataSource, trackType,
+ trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
+ trackSelection.getSelectionData(), segmentNum, maxSegmentCount);
}
@Override
@@ -298,8 +302,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
// Private methods.
- private AdaptationSet getAdaptationSet() {
- return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex);
+ private ArrayList getRepresentations() {
+ List manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets;
+ ArrayList representations = new ArrayList<>();
+ for (int adaptationSetIndex : adaptationSetIndices) {
+ representations.addAll(manifestAdapationSets.get(adaptationSetIndex).representations);
+ }
+ return representations;
}
private long getNowUnixTimeUs() {
@@ -332,7 +341,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
private static Chunk newMediaChunk(RepresentationHolder representationHolder,
- DataSource dataSource, Format trackFormat, int trackSelectionReason,
+ DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason,
Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) {
Representation representation = representationHolder.representation;
long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
@@ -343,8 +352,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),
segmentUri.start, segmentUri.length, representation.getCacheKey());
return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason,
- trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum,
- representationHolder.trackType, trackFormat);
+ trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackType, trackFormat);
} else {
int segmentCount = 1;
for (int i = 1; i < maxSegmentCount; i++) {
@@ -371,7 +379,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
protected static final class RepresentationHolder {
- public final int trackType;
public final ChunkExtractorWrapper extractorWrapper;
public Representation representation;
@@ -381,10 +388,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
private int segmentNumShift;
public RepresentationHolder(long periodDurationUs, Representation representation,
- boolean enableEventMessageTrack, boolean enableCea608Track, int trackType) {
+ boolean enableEventMessageTrack, boolean enableCea608Track) {
this.periodDurationUs = periodDurationUs;
this.representation = representation;
- this.trackType = trackType;
String containerMimeType = representation.format.containerMimeType;
if (mimeTypeIsRawText(containerMimeType)) {
extractorWrapper = null;
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
index 097676b89f..fd91a2f784 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
@@ -41,31 +41,40 @@ public class AdaptationSet {
public final int type;
/**
- * The {@link Representation}s in the adaptation set.
+ * {@link Representation}s in the adaptation set.
*/
public final List representations;
/**
- * The accessibility descriptors in the adaptation set.
+ * Accessibility descriptors in the adaptation set.
*/
- public final List accessibilityDescriptors;
+ public final List accessibilityDescriptors;
+
+ /**
+ * Supplemental properties in the adaptation set.
+ */
+ public final List supplementalProperties;
/**
* @param id A non-negative identifier for the adaptation set that's unique in the scope of its
* containing period, or {@link #ID_UNSET} if not specified.
* @param type The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}
* {@code TRACK_TYPE_*} constants.
- * @param representations The {@link Representation}s in the adaptation set.
- * @param accessibilityDescriptors The accessibility descriptors in the adaptation set.
+ * @param representations {@link Representation}s in the adaptation set.
+ * @param accessibilityDescriptors Accessibility descriptors in the adaptation set.
+ * @param supplementalProperties Supplemental properties in the adaptation set.
*/
public AdaptationSet(int id, int type, List representations,
- List accessibilityDescriptors) {
+ List accessibilityDescriptors, List supplementalProperties) {
this.id = id;
this.type = type;
this.representations = Collections.unmodifiableList(representations);
this.accessibilityDescriptors = accessibilityDescriptors == null
- ? Collections.emptyList()
+ ? Collections.emptyList()
: Collections.unmodifiableList(accessibilityDescriptors);
+ this.supplementalProperties = supplementalProperties == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(supplementalProperties);
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
index eb51c8312d..cd02e27fce 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
@@ -134,7 +134,8 @@ public class DashManifest {
} while(key.periodIndex == periodIndex && key.adaptationSetIndex == adaptationSetIndex);
copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type,
- copyRepresentations, adaptationSet.accessibilityDescriptors));
+ copyRepresentations, adaptationSet.accessibilityDescriptors,
+ adaptationSet.supplementalProperties));
} while(key.periodIndex == periodIndex);
// Add back the last key which doesn't belong to the period being processed
keys.addFirst(key);
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
index d4338fd812..53115a7a0e 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
@@ -239,8 +239,9 @@ public class DashManifestParser extends DefaultHandler
int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE);
String language = xpp.getAttributeValue(null, "lang");
ArrayList drmSchemeDatas = new ArrayList<>();
- ArrayList inbandEventStreams = new ArrayList<>();
- ArrayList accessibilityDescriptors = new ArrayList<>();
+ ArrayList inbandEventStreams = new ArrayList<>();
+ ArrayList accessibilityDescriptors = new ArrayList<>();
+ ArrayList supplementalProperties = new ArrayList<>();
List representationInfos = new ArrayList<>();
@C.SelectionFlags int selectionFlags = 0;
@@ -265,7 +266,9 @@ public class DashManifestParser extends DefaultHandler
} else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) {
audioChannels = parseAudioChannelConfiguration(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) {
- accessibilityDescriptors.add(parseAccessibility(xpp));
+ accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility"));
+ } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) {
+ supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty"));
} else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) {
RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs,
width, height, frameRate, audioChannels, audioSamplingRate, language,
@@ -280,7 +283,7 @@ public class DashManifestParser extends DefaultHandler
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase);
} else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
- inbandEventStreams.add(parseInbandEventStream(xpp));
+ inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
} else if (XmlPullParserUtil.isStartTag(xpp)) {
parseAdaptationSetChild(xpp);
}
@@ -293,12 +296,15 @@ public class DashManifestParser extends DefaultHandler
drmSchemeDatas, inbandEventStreams));
}
- return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors);
+ return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors,
+ supplementalProperties);
}
protected AdaptationSet buildAdaptationSet(int id, int contentType,
- List representations, List accessibilityDescriptors) {
- return new AdaptationSet(id, contentType, representations, accessibilityDescriptors);
+ List representations, List accessibilityDescriptors,
+ List supplementalProperties) {
+ return new AdaptationSet(id, contentType, representations, accessibilityDescriptors,
+ supplementalProperties);
}
protected int parseContentType(XmlPullParser xpp) {
@@ -337,6 +343,7 @@ public class DashManifestParser extends DefaultHandler
IOException {
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri);
+ String schemeType = xpp.getAttributeValue(null, "value");
byte[] data = null;
UUID uuid = null;
boolean requiresSecureDecoder = false;
@@ -362,34 +369,8 @@ public class DashManifestParser extends DefaultHandler
requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW");
}
} while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection"));
- return data != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder)
- : null;
- }
-
- /**
- * Parses an InbandEventStream element.
- *
- * @param xpp The parser from which to read.
- * @throws XmlPullParserException If an error occurs parsing the element.
- * @throws IOException If an error occurs reading the element.
- * @return A {@link SchemeValuePair} parsed from the element.
- */
- protected SchemeValuePair parseInbandEventStream(XmlPullParser xpp)
- throws XmlPullParserException, IOException {
- return parseSchemeValuePair(xpp, "InbandEventStream");
- }
-
- /**
- * Parses an Accessibility element.
- *
- * @param xpp The parser from which to read.
- * @throws XmlPullParserException If an error occurs parsing the element.
- * @throws IOException If an error occurs reading the element.
- * @return A {@link SchemeValuePair} parsed from the element.
- */
- protected SchemeValuePair parseAccessibility(XmlPullParser xpp)
- throws XmlPullParserException, IOException {
- return parseSchemeValuePair(xpp, "Accessibility");
+ return data != null
+ ? new SchemeData(uuid, schemeType, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null;
}
/**
@@ -429,7 +410,7 @@ public class DashManifestParser extends DefaultHandler
int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels,
int adaptationSetAudioSamplingRate, String adaptationSetLanguage,
@C.SelectionFlags int adaptationSetSelectionFlags,
- List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase)
+ List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase)
throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE);
@@ -442,7 +423,7 @@ public class DashManifestParser extends DefaultHandler
int audioChannels = adaptationSetAudioChannels;
int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate);
ArrayList drmSchemeDatas = new ArrayList<>();
- ArrayList inbandEventStreams = new ArrayList<>();
+ ArrayList inbandEventStreams = new ArrayList<>();
boolean seenFirstBaseUrl = false;
do {
@@ -466,7 +447,7 @@ public class DashManifestParser extends DefaultHandler
drmSchemeDatas.add(contentProtection);
}
} else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
- inbandEventStreams.add(parseInbandEventStream(xpp));
+ inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
}
} while (!XmlPullParserUtil.isEndTag(xpp, "Representation"));
@@ -480,7 +461,7 @@ public class DashManifestParser extends DefaultHandler
protected Format buildFormat(String id, String containerMimeType, int width, int height,
float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language,
- @C.SelectionFlags int selectionFlags, List accessibilityDescriptors,
+ @C.SelectionFlags int selectionFlags, List accessibilityDescriptors,
String codecs) {
String sampleMimeType = getSampleMimeType(containerMimeType, codecs);
if (sampleMimeType != null) {
@@ -509,14 +490,14 @@ public class DashManifestParser extends DefaultHandler
protected Representation buildRepresentation(RepresentationInfo representationInfo,
String contentId, ArrayList extraDrmSchemeDatas,
- ArrayList extraInbandEventStreams) {
+ ArrayList extraInbandEventStreams) {
Format format = representationInfo.format;
ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas;
drmSchemeDatas.addAll(extraDrmSchemeDatas);
if (!drmSchemeDatas.isEmpty()) {
format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas));
}
- ArrayList inbandEventStremas = representationInfo.inbandEventStreams;
+ ArrayList inbandEventStremas = representationInfo.inbandEventStreams;
inbandEventStremas.addAll(extraInbandEventStreams);
return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format,
representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas);
@@ -809,28 +790,29 @@ public class DashManifestParser extends DefaultHandler
}
/**
- * Parses a {@link SchemeValuePair} from an element.
+ * Parses a {@link Descriptor} from an element.
*
* @param xpp The parser from which to read.
* @param tag The tag of the element being parsed.
* @throws XmlPullParserException If an error occurs parsing the element.
* @throws IOException If an error occurs reading the element.
- * @return The parsed {@link SchemeValuePair}.
+ * @return The parsed {@link Descriptor}.
*/
- protected static SchemeValuePair parseSchemeValuePair(XmlPullParser xpp, String tag)
+ protected static Descriptor parseDescriptor(XmlPullParser xpp, String tag)
throws XmlPullParserException, IOException {
- String schemeIdUri = parseString(xpp, "schemeIdUri", null);
+ String schemeIdUri = parseString(xpp, "schemeIdUri", "");
String value = parseString(xpp, "value", null);
+ String id = parseString(xpp, "id", null);
do {
xpp.next();
} while (!XmlPullParserUtil.isEndTag(xpp, tag));
- return new SchemeValuePair(schemeIdUri, value);
+ return new Descriptor(schemeIdUri, value, id);
}
protected static int parseCea608AccessibilityChannel(
- List accessibilityDescriptors) {
+ List accessibilityDescriptors) {
for (int i = 0; i < accessibilityDescriptors.size(); i++) {
- SchemeValuePair descriptor = accessibilityDescriptors.get(i);
+ Descriptor descriptor = accessibilityDescriptors.get(i);
if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)
&& descriptor.value != null) {
Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value);
@@ -845,9 +827,9 @@ public class DashManifestParser extends DefaultHandler
}
protected static int parseCea708AccessibilityChannel(
- List accessibilityDescriptors) {
+ List accessibilityDescriptors) {
for (int i = 0; i < accessibilityDescriptors.size(); i++) {
- SchemeValuePair descriptor = accessibilityDescriptors.get(i);
+ Descriptor descriptor = accessibilityDescriptors.get(i);
if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri)
&& descriptor.value != null) {
Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value);
@@ -925,10 +907,10 @@ public class DashManifestParser extends DefaultHandler
public final String baseUrl;
public final SegmentBase segmentBase;
public final ArrayList drmSchemeDatas;
- public final ArrayList inbandEventStreams;
+ public final ArrayList inbandEventStreams;
public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase,
- ArrayList drmSchemeDatas, ArrayList inbandEventStreams) {
+ ArrayList drmSchemeDatas, ArrayList inbandEventStreams) {
this.format = format;
this.baseUrl = baseUrl;
this.segmentBase = segmentBase;
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java
similarity index 52%
rename from library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java
rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java
index 470bf0f989..18d0a937ab 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java
@@ -15,19 +15,37 @@
*/
package com.google.android.exoplayer2.source.dash.manifest;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
/**
- * A pair consisting of a scheme ID and value.
+ * A descriptor, as defined by ISO 23009-1, 2nd edition, 5.8.2.
*/
-public class SchemeValuePair {
+public final class Descriptor {
- public final String schemeIdUri;
- public final String value;
+ /**
+ * The scheme URI.
+ */
+ @NonNull public final String schemeIdUri;
+ /**
+ * The value, or null.
+ */
+ @Nullable public final String value;
+ /**
+ * The identifier, or null.
+ */
+ @Nullable public final String id;
- public SchemeValuePair(String schemeIdUri, String value) {
+ /**
+ * @param schemeIdUri The scheme URI.
+ * @param value The value, or null.
+ * @param id The identifier, or null.
+ */
+ public Descriptor(@NonNull String schemeIdUri, @Nullable String value, @Nullable String id) {
this.schemeIdUri = schemeIdUri;
this.value = value;
+ this.id = id;
}
@Override
@@ -38,14 +56,17 @@ public class SchemeValuePair {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
- SchemeValuePair other = (SchemeValuePair) obj;
- return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value);
+ Descriptor other = (Descriptor) obj;
+ return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value)
+ && Util.areEqual(id, other.id);
}
@Override
public int hashCode() {
- return 31 * (schemeIdUri != null ? schemeIdUri.hashCode() : 0)
- + (value != null ? value.hashCode() : 0);
+ int result = (schemeIdUri != null ? schemeIdUri.hashCode() : 0);
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ result = 31 * result + (id != null ? id.hashCode() : 0);
+ return result;
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java
index 5960d4d7ba..81e4602c1d 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java
@@ -65,7 +65,7 @@ public abstract class Representation {
/**
* The in-band event streams in the representation. Never null, but may be empty.
*/
- public final List inbandEventStreams;
+ public final List