Merge branch 'dev-v2-r2.5.0' into release-v2

This commit is contained in:
Oliver Woodman 2017-08-07 14:31:45 +01:00
commit deb9b301b2
489 changed files with 19804 additions and 5593 deletions

View file

@ -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 ####

View file

@ -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

View file

@ -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'

32
constants.gradle Normal file
View file

@ -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
}
}

58
core_settings.gradle Normal file
View file

@ -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')
}

View file

@ -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')
}

View file

@ -16,14 +16,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2404"
android:versionName="2.4.4">
android:versionCode="2500"
android:versionName="2.5.0">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="24"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="25"/>
<application
android:label="@string/application_name"

View file

@ -138,28 +138,76 @@
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure SD & HD (MP4,H264)",
"name": "WV: Secure SD & HD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (MP4,H264)",
"name": "WV: Secure SD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (MP4,H264)",
"name": "WV: Secure HD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (MP4,H264)",
"name": "WV: Secure UHD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD & HD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD & HD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}
]
},
@ -277,18 +325,6 @@
}
]
},
{
"name": "ClearKey DASH",
"samples": [
{
"name": "Big Buck Bunny (CENC ClearKey)",
"uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd",
"extension": "mpd",
"drm_scheme": "cenc",
"drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json"
}
]
},
{
"name": "SmoothStreaming",
"samples": [
@ -341,7 +377,7 @@
"samples": [
{
"name": "Dizzy",
"uri": "http://html5demos.com/assets/dizzy.mp4"
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"name": "Apple AAC 10s",
@ -353,7 +389,7 @@
},
{
"name": "Android screens (Matroska)",
"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"
},
{
"name": "Big Buck Bunny (MP4 Video)",
@ -377,7 +413,7 @@
},
{
"name": "Google Play (MP3 Audio)",
"uri": "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
},
{
"name": "Google Play (Ogg/Vorbis Audio)",
@ -408,10 +444,10 @@
"name": "Cats -> 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="
}
]
}
]

View file

@ -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 "?";
}
}
}

View file

@ -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<FrameworkMediaCrypto> buildDrmSessionManager(UUID uuid,
private DrmSessionManager<FrameworkMediaCrypto> 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) {

View file

@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity {
String[] drmKeyRequestProperties = null;
boolean preferExtensionDecoders = false;
ArrayList<UriSample> 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);
}

View file

@ -56,4 +56,6 @@
<string name="sample_list_load_error">One or more sample lists failed to load</string>
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
</resources>

View file

@ -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.

View file

@ -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

View file

@ -28,7 +28,6 @@
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.google.android.exoplayer.ext.cronet"
tools:replace="android:targetPackage"/>
android:targetPackage="com.google.android.exoplayer.ext.cronet"/>
</manifest>

View file

@ -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() {

View file

@ -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) {

View file

@ -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<String> 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.
* <p>
* 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<String> 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.
* <p>
* 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<String> 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.
* <p>
* 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<String> 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.
* <p>
* 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<String> 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);
}

View file

@ -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<CronetProvider> 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<CronetProvider> {
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;
}
}
}

View file

@ -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="<path to Android NDK>"
```
@ -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

View file

@ -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 {

View file

@ -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;

View file

@ -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()) {

View file

@ -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="<path to Android NDK>"
```
@ -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

View file

@ -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 {

View file

@ -28,7 +28,6 @@
<instrumentation
android:targetPackage="com.google.android.exoplayer2.ext.flac.test"
android:name="android.test.InstrumentationTestRunner"
tools:replace="android:targetPackage"/>
android:name="android.test.InstrumentationTestRunner"/>
</manifest>

View file

@ -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();

View file

@ -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();

View file

@ -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;
}
}
}

View file

@ -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);

View file

@ -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

View file

@ -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'
}

View file

@ -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;

57
extensions/ima/README.md Normal file
View file

@ -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

View file

@ -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'

View file

@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.ext.ima">
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
</manifest>

View file

@ -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;
}
}

View file

@ -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<VideoAdPlayerCallback> 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<String, String> 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<Float> 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;
}
}

View file

@ -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<MediaPeriod, MediaSource> 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();
}
}
});
}
}
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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'

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest package="com.google.android.exoplayer2.ext.mediasession"/>

View file

@ -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}.
* <p>
* 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.
* <p>
* 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();
}
}

View file

@ -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}.
* <p>
* 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:
* <ul>
* <li>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.
* </li>
* <li>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.</li>
* <li>To enable editing of the media queue, you can set a {@link QueueEditor} by calling
* {@link #setQueueEditor(QueueEditor)}.</li>
* <li>An {@link ErrorMessageProvider} for providing human readable error messages and
* corresponding error codes can be set by calling
* {@link #setErrorMessageProvider(ErrorMessageProvider)}.</li>
* </ul>
*/
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.
* <p>
* 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<Integer, String> 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<String, CustomActionProvider> 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...)}.
* <p>
* 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...)}.
* <p>
* 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.
* <p>
* 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<String, CustomActionProvider> 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<Integer, String> 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<MediaSessionCompat.QueueItem> 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<String, CustomActionProvider> 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);
}
}
}
}

View file

@ -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.
* <p>
* 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();
}
}

View file

@ -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}.
* <p>
* 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.
* <p>
* 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.<MediaSessionCompat.QueueItem>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<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
for (int i = startIndex; i < startIndex + queueSize; i++) {
queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(i), i));
}
mediaSession.setQueue(queue);
activeQueueItemId = currentWindowIndex;
}
}

View file

@ -0,0 +1,23 @@
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>

View file

@ -0,0 +1,23 @@
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#4EFFFFFF"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>

View file

@ -0,0 +1,23 @@
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="32dp"
android:width="32dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Herhaal alles"</string>
<string name="exo_media_action_repeat_off_description">"Herhaal niks"</string>
<string name="exo_media_action_repeat_one_description">"Herhaal een"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"ሁሉንም ድገም"</string>
<string name="exo_media_action_repeat_off_description">"ምንም አትድገም"</string>
<string name="exo_media_action_repeat_one_description">"አንዱን ድገም"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"تكرار الكل"</string>
<string name="exo_media_action_repeat_off_description">"عدم التكرار"</string>
<string name="exo_media_action_repeat_one_description">"تكرار مقطع واحد"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Bütün təkrarlayın"</string>
<string name="exo_media_action_repeat_one_description">"Təkrar bir"</string>
<string name="exo_media_action_repeat_off_description">"Heç bir təkrar"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ponovi sve"</string>
<string name="exo_media_action_repeat_off_description">"Ne ponavljaj nijednu"</string>
<string name="exo_media_action_repeat_one_description">"Ponovi jednu"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Паўтарыць усё"</string>
<string name="exo_media_action_repeat_off_description">"Паўтараць ні"</string>
<string name="exo_media_action_repeat_one_description">"Паўтарыць адзін"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Повтаряне на всички"</string>
<string name="exo_media_action_repeat_off_description">"Без повтаряне"</string>
<string name="exo_media_action_repeat_one_description">"Повтаряне на един елемент"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"সবগুলির পুনরাবৃত্তি করুন"</string>
<string name="exo_media_action_repeat_off_description">"একটিরও পুনরাবৃত্তি করবেন না"</string>
<string name="exo_media_action_repeat_one_description">"একটির পুনরাবৃত্তি করুন"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ponovite sve"</string>
<string name="exo_media_action_repeat_off_description">"Ne ponavljaju"</string>
<string name="exo_media_action_repeat_one_description">"Ponovite jedan"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeteix-ho tot"</string>
<string name="exo_media_action_repeat_off_description">"No en repeteixis cap"</string>
<string name="exo_media_action_repeat_one_description">"Repeteix-ne un"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Opakovat vše"</string>
<string name="exo_media_action_repeat_off_description">"Neopakovat"</string>
<string name="exo_media_action_repeat_one_description">"Opakovat jednu položku"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Gentag alle"</string>
<string name="exo_media_action_repeat_off_description">"Gentag ingen"</string>
<string name="exo_media_action_repeat_one_description">"Gentag en"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Alle wiederholen"</string>
<string name="exo_media_action_repeat_off_description">"Keinen Titel wiederholen"</string>
<string name="exo_media_action_repeat_one_description">"Einen Titel wiederholen"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Επανάληψη όλων"</string>
<string name="exo_media_action_repeat_off_description">"Καμία επανάληψη"</string>
<string name="exo_media_action_repeat_one_description">"Επανάληψη ενός στοιχείου"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
<string name="exo_media_action_repeat_off_description">"No repetir"</string>
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
<string name="exo_media_action_repeat_off_description">"No repetir"</string>
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Korda kõike"</string>
<string name="exo_media_action_repeat_off_description">"Ära korda midagi"</string>
<string name="exo_media_action_repeat_one_description">"Korda ühte"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Errepikatu guztiak"</string>
<string name="exo_media_action_repeat_off_description">"Ez errepikatu"</string>
<string name="exo_media_action_repeat_one_description">"Errepikatu bat"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"تکرار همه"</string>
<string name="exo_media_action_repeat_off_description">"تکرار هیچ‌کدام"</string>
<string name="exo_media_action_repeat_one_description">"یک‌بار تکرار"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Toista kaikki"</string>
<string name="exo_media_action_repeat_off_description">"Toista ei mitään"</string>
<string name="exo_media_action_repeat_one_description">"Toista yksi"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Tout lire en boucle"</string>
<string name="exo_media_action_repeat_off_description">"Aucune répétition"</string>
<string name="exo_media_action_repeat_one_description">"Répéter un élément"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Tout lire en boucle"</string>
<string name="exo_media_action_repeat_off_description">"Ne rien lire en boucle"</string>
<string name="exo_media_action_repeat_one_description">"Lire en boucle un élément"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
<string name="exo_media_action_repeat_off_description">"Non repetir"</string>
<string name="exo_media_action_repeat_one_description">"Repetir un"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"બધા પુનરાવર્તન કરો"</string>
<string name="exo_media_action_repeat_off_description">"કંઈ પુનરાવર્તન કરો"</string>
<string name="exo_media_action_repeat_one_description">"એક પુનરાવર્તન કરો"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"सभी को दोहराएं"</string>
<string name="exo_media_action_repeat_off_description">"कुछ भी न दोहराएं"</string>
<string name="exo_media_action_repeat_one_description">"एक दोहराएं"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ponovi sve"</string>
<string name="exo_media_action_repeat_off_description">"Bez ponavljanja"</string>
<string name="exo_media_action_repeat_one_description">"Ponovi jedno"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Összes ismétlése"</string>
<string name="exo_media_action_repeat_off_description">"Nincs ismétlés"</string>
<string name="exo_media_action_repeat_one_description">"Egy ismétlése"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"կրկնել այն ամենը"</string>
<string name="exo_media_action_repeat_off_description">"Չկրկնել"</string>
<string name="exo_media_action_repeat_one_description">"Կրկնել մեկը"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ulangi Semua"</string>
<string name="exo_media_action_repeat_off_description">"Jangan Ulangi"</string>
<string name="exo_media_action_repeat_one_description">"Ulangi Satu"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Endurtaka allt"</string>
<string name="exo_media_action_repeat_off_description">"Endurtaka ekkert"</string>
<string name="exo_media_action_repeat_one_description">"Endurtaka eitt"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ripeti tutti"</string>
<string name="exo_media_action_repeat_off_description">"Non ripetere nessuno"</string>
<string name="exo_media_action_repeat_one_description">"Ripeti uno"</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.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.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"חזור על הכל"</string>
<string name="exo_media_action_repeat_off_description">"אל תחזור על כלום"</string>
<string name="exo_media_action_repeat_one_description">"חזור על פריט אחד"</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more