Merge branch 'dev-v2-r2.5.0' into release-v2
38
README.md
|
|
@ -20,6 +20,11 @@ and extend, and can be updated through Play Store application updates.
|
|||
|
||||
## Using ExoPlayer ##
|
||||
|
||||
ExoPlayer modules can be obtained via jCenter. It's also possible to clone the
|
||||
repository and depend on the modules locally.
|
||||
|
||||
### Via jCenter ###
|
||||
|
||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||
dependency. You need to make sure you have the jcenter repository included in
|
||||
the `build.gradle` file in the root of your project:
|
||||
|
|
@ -64,6 +69,39 @@ latest versions, see the [Release notes][].
|
|||
[Bintray]: https://bintray.com/google/exoplayer
|
||||
[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||
|
||||
### Locally ###
|
||||
|
||||
Cloning the repository and depending on the modules locally is required when
|
||||
using some ExoPlayer extension modules. It's also a suitable approach if you
|
||||
want to make local changes to ExoPlayer, or if you want to use a development
|
||||
branch.
|
||||
|
||||
First, clone the repository into a local directory and checkout the desired
|
||||
branch:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
git checkout release-v2
|
||||
```
|
||||
|
||||
Next, add the following to your project's `settings.gradle` file, replacing
|
||||
`path/to/exoplayer` with the path to your local copy:
|
||||
|
||||
```gradle
|
||||
gradle.ext.exoplayerRoot = 'path/to/exoplayer'
|
||||
gradle.ext.exoplayerModulePrefix = 'exoplayer-'
|
||||
apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')
|
||||
```
|
||||
|
||||
You should now see the ExoPlayer modules appear as part of your project. You can
|
||||
depend on them as you would on any other local module, for example:
|
||||
|
||||
```gradle
|
||||
compile project(':exoplayer-library-core')
|
||||
compile project(':exoplayer-library-dash')
|
||||
compile project(':exoplayer-library-ui)
|
||||
```
|
||||
|
||||
## Developing ExoPlayer ##
|
||||
|
||||
#### Project branches ####
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
31
build.gradle
|
|
@ -16,8 +16,8 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.3.1'
|
||||
classpath 'com.novoda:bintray-release:0.4.0'
|
||||
classpath 'com.android.tools.build:gradle:2.3.3'
|
||||
classpath 'com.novoda:bintray-release:0.5.0'
|
||||
}
|
||||
// Workaround for the following test coverage issue. Remove when fixed:
|
||||
// https://code.google.com/p/android/issues/detail?id=226070
|
||||
|
|
@ -31,25 +31,12 @@ buildscript {
|
|||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
maven {
|
||||
url "https://maven.google.com"
|
||||
}
|
||||
}
|
||||
project.ext {
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 9 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality
|
||||
// provided by the library requires API level 16 or greater.
|
||||
minSdkVersion = 9
|
||||
compileSdkVersion = 25
|
||||
targetSdkVersion = 25
|
||||
buildToolsVersion = '25'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '25.3.1'
|
||||
dexmakerVersion = '1.2'
|
||||
mockitoVersion = '1.9.5'
|
||||
releaseRepoName = getBintrayRepo()
|
||||
releaseUserOrg = 'google'
|
||||
releaseGroupId = 'com.google.android.exoplayer'
|
||||
releaseVersion = 'r2.4.4'
|
||||
releaseWebsite = 'https://github.com/google/ExoPlayer'
|
||||
exoplayerPublishEnabled = true
|
||||
}
|
||||
if (it.hasProperty('externalBuildDir')) {
|
||||
if (!new File(externalBuildDir).isAbsolute()) {
|
||||
|
|
@ -59,10 +46,4 @@ allprojects {
|
|||
}
|
||||
}
|
||||
|
||||
def getBintrayRepo() {
|
||||
boolean publicRepo = hasProperty('publicRepo') &&
|
||||
property('publicRepo').toBoolean()
|
||||
return publicRepo ? 'exoplayer' : 'exoplayer-test'
|
||||
}
|
||||
|
||||
apply from: 'javadoc_combined.gradle'
|
||||
|
|
|
|||
32
constants.gradle
Normal 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
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
42
extensions/ima/build.gradle
Normal 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'
|
||||
5
extensions/ima/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
27
extensions/mediasession/README.md
Normal 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
|
||||
42
extensions/mediasession/build.gradle
Normal 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'
|
||||
17
extensions/mediasession/src/main/AndroidManifest.xml
Normal 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"/>
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 203 B |
|
After Width: | Height: | Size: 223 B |
|
After Width: | Height: | Size: 223 B |
|
After Width: | Height: | Size: 142 B |
|
After Width: | Height: | Size: 166 B |
|
After Width: | Height: | Size: 160 B |
|
After Width: | Height: | Size: 210 B |
|
After Width: | Height: | Size: 227 B |
|
After Width: | Height: | Size: 232 B |
|
After Width: | Height: | Size: 288 B |
|
After Width: | Height: | Size: 322 B |
|
After Width: | Height: | Size: 331 B |
|
After Width: | Height: | Size: 266 B |
|
After Width: | Height: | Size: 309 B |
|
After Width: | Height: | Size: 309 B |
21
extensions/mediasession/src/main/res/values-af/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-am/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-ar/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
21
extensions/mediasession/src/main/res/values-bg/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
21
extensions/mediasession/src/main/res/values-ca/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-cs/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-da/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-de/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-el/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
21
extensions/mediasession/src/main/res/values-es/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
21
extensions/mediasession/src/main/res/values-fa/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-fi/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
21
extensions/mediasession/src/main/res/values-fr/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
21
extensions/mediasession/src/main/res/values-hi/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-hr/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-hu/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
21
extensions/mediasession/src/main/res/values-in/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
21
extensions/mediasession/src/main/res/values-it/strings.xml
Normal 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>
|
||||
21
extensions/mediasession/src/main/res/values-iw/strings.xml
Normal 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>
|
||||