Merge branch 'dev-v2' into rtmp_client

This commit is contained in:
ojw28 2017-07-05 15:04:49 +01:00 committed by GitHub
commit 0ecbe5dc04
364 changed files with 14034 additions and 4415 deletions

View File

@ -20,6 +20,11 @@ and extend, and can be updated through Play Store application updates.
## Using ExoPlayer ## ## 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 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 dependency. You need to make sure you have the jcenter repository included in
the `build.gradle` file in the root of your project: 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 [Bintray]: https://bintray.com/google/exoplayer
[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md [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 ## ## Developing ExoPlayer ##
#### Project branches #### #### Project branches ####

View File

@ -1,5 +1,60 @@
# Release notes # # Release notes #
### r2.4.3 ###
* Audio: Workaround custom audio decoders misreporting their maximum supported
channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)).
* Audio: Workaround for broken MediaTek raw decoder on some devices
([#2873](https://github.com/google/ExoPlayer/issues/2873)).
* Captions: Fix TTML captions appearing at the top of the screen
([#2953](https://github.com/google/ExoPlayer/issues/2953)).
* Captions: Fix handling of some DVB subtitles
([#2957](https://github.com/google/ExoPlayer/issues/2957)).
* Track selection: Fix setSelectionOverride(index, tracks, null)
([#2988](https://github.com/google/ExoPlayer/issues/2988)).
* GVR extension: Add support for mono input
([#2710](https://github.com/google/ExoPlayer/issues/2710)).
* FLAC extension: Fix failing build
([#2977](https://github.com/google/ExoPlayer/pull/2977)).
* Misc bugfixes.
### r2.4.2 ###
* Stability: Work around Nexus 10 reboot when playing certain content
([#2806](https://github.com/google/ExoPlayer/issues/2806)).
* MP3: Correctly treat MP3s with INFO headers as constant bitrate
([#2895](https://github.com/google/ExoPlayer/issues/2895)).
* HLS: Use average rather than peak bandwidth when available
([#2863](https://github.com/google/ExoPlayer/issues/2863)).
* SmoothStreaming: Fix timeline for live streams
([#2760](https://github.com/google/ExoPlayer/issues/2760)).
* UI: Fix DefaultTimeBar invalidation
([#2871](https://github.com/google/ExoPlayer/issues/2871)).
* Misc bugfixes.
### r2.4.1 ###
* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media
([#2780](https://github.com/google/ExoPlayer/issues/2780)).
* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large codec
input buffer allocations on all devices
([#2607](https://github.com/google/ExoPlayer/issues/2607)).
* Variable speed playback: Fix interpolation for rate/pitch adjustment
([#2774](https://github.com/google/ExoPlayer/issues/2774)).
* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist.
* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE
([#2743](https://github.com/google/ExoPlayer/issues/2743)).
* HLS: Correctly propagate errors loading the media playlist
([#2623](https://github.com/google/ExoPlayer/issues/2623)).
* UI: DefaultTimeBar enhancements and bug fixes
([#2740](https://github.com/google/ExoPlayer/issues/2740)).
* Ogg: Fix failure to play some Ogg files
([#2782](https://github.com/google/ExoPlayer/issues/2782)).
* Captions: Don't select text tack with no language by default.
* Captions: TTML positioning fixes
([#2824](https://github.com/google/ExoPlayer/issues/2824)).
* Misc bugfixes.
### r2.4.0 ### ### r2.4.0 ###
* New modular library structure. You can read more about depending on individual * New modular library structure. You can read more about depending on individual

View File

@ -16,7 +16,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.3.0' classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'com.novoda:bintray-release:0.4.0' classpath 'com.novoda:bintray-release:0.4.0'
} }
// Workaround for the following test coverage issue. Remove when fixed: // Workaround for the following test coverage issue. Remove when fixed:
@ -33,23 +33,7 @@ allprojects {
jcenter() jcenter()
} }
project.ext { project.ext {
// Important: ExoPlayer specifies a minSdkVersion of 9 because various exoplayerPublishEnabled = true
// 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.0'
releaseWebsite = 'https://github.com/google/ExoPlayer'
} }
if (it.hasProperty('externalBuildDir')) { if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) { if (!new File(externalBuildDir).isAbsolute()) {
@ -59,10 +43,4 @@ allprojects {
} }
} }
def getBintrayRepo() {
boolean publicRepo = hasProperty('publicRepo') &&
property('publicRepo').toBoolean()
return publicRepo ? 'exoplayer' : 'exoplayer-test'
}
apply from: 'javadoc_combined.gradle' apply from: 'javadoc_combined.gradle'

32
constants.gradle Normal file
View File

@ -0,0 +1,32 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
project.ext {
// Important: ExoPlayer specifies a minSdkVersion of 9 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided
// by the library requires API level 16 or greater.
minSdkVersion = 9
compileSdkVersion = 25
targetSdkVersion = 25
buildToolsVersion = '25'
testSupportLibraryVersion = '0.5'
supportLibraryVersion = '25.3.1'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
releaseVersion = 'r2.4.3'
modulePrefix = ':';
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
}
}

54
core_settings.gradle Normal file
View File

@ -0,0 +1,54 @@
// 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-okhttp'
include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9'
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-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')
if (gradle.ext.has('exoplayerIncludeCronetExtension')
&& gradle.ext.exoplayerIncludeCronetExtension) {
include modulePrefix + 'extension-cronet'
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
}

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../constants.gradle'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
@ -45,13 +46,14 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
compile project(':library-dash') compile project(modulePrefix + 'library-dash')
compile project(':library-hls') compile project(modulePrefix + 'library-hls')
compile project(':library-smoothstreaming') compile project(modulePrefix + 'library-smoothstreaming')
compile project(':library-ui') compile project(modulePrefix + 'library-ui')
withExtensionsCompile project(path: ':extension-ffmpeg') withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg')
withExtensionsCompile project(path: ':extension-flac') withExtensionsCompile project(path: modulePrefix + 'extension-flac')
withExtensionsCompile project(path: ':extension-opus') withExtensionsCompile project(path: modulePrefix + 'extension-ima')
withExtensionsCompile project(path: ':extension-vp9') withExtensionsCompile project(path: modulePrefix + 'extension-opus')
withExtensionsCompile project(path: modulePrefix + 'extension-vp9')
} }

View File

@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" package="com.google.android.exoplayer2.demo"
android:versionCode="2400" android:versionCode="2403"
android:versionName="2.4.0"> android:versionName="2.4.3">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

View File

@ -138,28 +138,76 @@
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd" "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", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "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", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "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", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "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", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "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"
} }
] ]
}, },
@ -341,7 +389,7 @@
"samples": [ "samples": [
{ {
"name": "Dizzy", "name": "Dizzy",
"uri": "http://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"name": "Apple AAC 10s", "name": "Apple AAC 10s",
@ -353,7 +401,7 @@
}, },
{ {
"name": "Android screens (Matroska)", "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)", "name": "Big Buck Bunny (MP4 Video)",
@ -377,7 +425,7 @@
}, },
{ {
"name": "Google Play (MP3 Audio)", "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)", "name": "Google Play (Ogg/Vorbis Audio)",
@ -408,10 +456,10 @@
"name": "Cats -> Dogs", "name": "Cats -> Dogs",
"playlist": [ "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 +470,7 @@
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" "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" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
@ -435,13 +483,13 @@
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
"playlist": [ "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": "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" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
@ -452,5 +500,85 @@
] ]
} }
] ]
},
{
"name": "IMA sample ad tags",
"samples": [
{
"name": "Single inline linear",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
},
{
"name": "Single skippable inline",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator="
},
{
"name": "Single redirect linear",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator="
},
{
"name": "Single redirect error",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator="
},
{
"name": "Single redirect broken (fallback)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator="
},
{
"name": "VMAP pre-roll",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll + bumper",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP post-roll",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP post-roll + bumper",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-, mid- and post-rolls, single ads",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
}
]
} }
] ]

View File

@ -95,6 +95,11 @@ import java.util.Locale;
+ getStateString(state) + "]"); + getStateString(state) + "]");
} }
@Override
public void onRepeatModeChanged(@ExoPlayer.RepeatMode int repeatMode) {
Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]");
}
@Override @Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
Log.d(TAG, "positionDiscontinuity"); Log.d(TAG, "positionDiscontinuity");
@ -276,7 +281,7 @@ import java.util.Locale;
@Override @Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) { float pixelWidthHeightRatio) {
// Do nothing. Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]");
} }
@Override @Override
@ -461,4 +466,16 @@ import java.util.Locale;
return enabled ? "[X]" : "[ ]"; return enabled ? "[X]" : "[ ]";
} }
private static String getRepeatModeString(@ExoPlayer.RepeatMode int repeatMode) {
switch (repeatMode) {
case ExoPlayer.REPEAT_MODE_OFF:
return "OFF";
case ExoPlayer.REPEAT_MODE_ONE:
return "ONE";
case ExoPlayer.REPEAT_MODE_ALL:
return "ALL";
default:
return "?";
}
}
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
@ -26,7 +27,9 @@ import android.text.TextUtils;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -69,6 +72,7 @@ import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.reflect.Constructor;
import java.net.CookieHandler; import java.net.CookieHandler;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.CookiePolicy; import java.net.CookiePolicy;
@ -92,6 +96,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
"com.google.android.exoplayer.demo.action.VIEW_LIST"; "com.google.android.exoplayer.demo.action.VIEW_LIST";
public static final String URI_LIST_EXTRA = "uri_list"; public static final String URI_LIST_EXTRA = "uri_list";
public static final String EXTENSION_LIST_EXTRA = "extension_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 DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final CookieManager DEFAULT_COOKIE_MANAGER; private static final CookieManager DEFAULT_COOKIE_MANAGER;
@ -200,10 +205,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public boolean dispatchKeyEvent(KeyEvent event) { public boolean dispatchKeyEvent(KeyEvent event) {
// Show the controls on any key event. // If the event was not handled then see if the player view can handle it.
simpleExoPlayerView.showController(); return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchKeyEvent(event);
// 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);
} }
// OnClickListener methods // OnClickListener methods
@ -234,7 +237,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
Intent intent = getIntent(); Intent intent = getIntent();
boolean needNewPlayer = player == null; boolean needNewPlayer = player == null;
if (needNewPlayer) { if (needNewPlayer) {
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); TrackSelection.Factory adaptiveTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
lastSeenTrackGroupArray = null;
eventLogger = new EventLogger(trackSelector);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null; DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
@ -253,6 +262,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
} }
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers() ((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
@ -261,16 +271,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
drmSessionManager, extensionRendererMode); drmSessionManager, extensionRendererMode);
TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
lastSeenTrackGroupArray = null;
player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
player.addListener(this); player.addListener(this);
eventLogger = new EventLogger(trackSelector);
player.addListener(eventLogger); player.addListener(eventLogger);
player.setAudioDebugListener(eventLogger); player.setAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger); player.setVideoDebugListener(eventLogger);
@ -312,6 +314,26 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources); : new ConcatenatingMediaSource(mediaSources);
String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
if (adTagUriString != null) {
Uri adTagUri = Uri.parse(adTagUriString);
ViewGroup adOverlayViewGroup = new FrameLayout(this);
// Load the extension source using reflection so that demo app doesn't have to depend on it.
try {
Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource");
Constructor<?> constructor = clazz.getConstructor(MediaSource.class,
DataSource.Factory.class, Context.class, Uri.class, ViewGroup.class);
mediaSource = (MediaSource) constructor.newInstance(mediaSource,
mediaDataSourceFactory, this, adTagUri, adOverlayViewGroup);
// The demo app has a non-null overlay frame layout.
simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup);
// Show a multi-window time bar, which will include ad position markers.
simpleExoPlayerView.setShowMultiWindowTimeBar(true);
} catch (Exception e) {
// Throw if the media source class was not found, or there was an error instantiating it.
showToast(R.string.ima_not_loaded);
}
}
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
if (haveResumePosition) { if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition); player.seekTo(resumeWindow, resumePosition);
@ -424,6 +446,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
updateButtonVisibilities(); updateButtonVisibilities();
} }
@Override
public void onRepeatModeChanged(int repeatMode) {
// Do nothing.
}
@Override @Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
if (needRetrySource) { if (needRetrySource) {

View File

@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity {
String[] drmKeyRequestProperties = null; String[] drmKeyRequestProperties = null;
boolean preferExtensionDecoders = false; boolean preferExtensionDecoders = false;
ArrayList<UriSample> playlistSamples = null; ArrayList<UriSample> playlistSamples = null;
String adTagUri = null;
reader.beginObject(); reader.beginObject();
while (reader.hasNext()) { while (reader.hasNext()) {
@ -233,6 +234,9 @@ public class SampleChooserActivity extends Activity {
} }
reader.endArray(); reader.endArray();
break; break;
case "ad_tag_uri":
adTagUri = reader.nextString();
break;
default: default:
throw new ParserException("Unsupported attribute name: " + name); throw new ParserException("Unsupported attribute name: " + name);
} }
@ -246,7 +250,7 @@ public class SampleChooserActivity extends Activity {
preferExtensionDecoders, playlistSamplesArray); preferExtensionDecoders, playlistSamplesArray);
} else { } else {
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, 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 uri;
public final String extension; public final String extension;
public final String adTagUri;
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
String extension) { String extension, String adTagUri) {
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
this.uri = uri; this.uri = uri;
this.extension = extension; this.extension = extension;
this.adTagUri = adTagUri;
} }
@Override @Override
@ -416,6 +422,7 @@ public class SampleChooserActivity extends Activity {
return super.buildIntent(context) return super.buildIntent(context)
.setData(Uri.parse(uri)) .setData(Uri.parse(uri))
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
.setAction(PlayerActivity.ACTION_VIEW); .setAction(PlayerActivity.ACTION_VIEW);
} }

View File

@ -58,4 +58,6 @@
<string name="sample_list_load_error">One or more sample lists failed to load</string> <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> </resources>

View File

@ -11,13 +11,10 @@ The Cronet Extension is an [HttpDataSource][] implementation using [Cronet][].
## Build Instructions ## ## Build Instructions ##
* Checkout ExoPlayer along with Extensions: 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
git clone https://github.com/google/ExoPlayer.git and enable the extension:
```
* Get the Cronet libraries:
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet` 1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
directory directory
@ -27,6 +24,12 @@ git clone https://github.com/google/ExoPlayer.git
1. Copy the content of the downloaded `libs` directory into the `jniLibs` 1. Copy the content of the downloaded `libs` directory into the `jniLibs`
directory of this extension 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 [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
@ -29,11 +30,11 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
compile files('libs/cronet_api.jar') compile files('libs/cronet_api.jar')
compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_common_java.jar')
compile files('libs/cronet_impl_native_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:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion

View File

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

View File

@ -16,9 +16,11 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import com.google.android.exoplayer2.upstream.DataSource; 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;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; 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.upstream.TransferListener;
import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Predicate;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -34,43 +36,143 @@ public final class CronetDataSourceFactory extends BaseFactory {
*/ */
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS =
CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS; CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
/** /**
* The default read timeout, in milliseconds. * The default read timeout, in milliseconds.
*/ */
public static final int DEFAULT_READ_TIMEOUT_MILLIS = public static final int DEFAULT_READ_TIMEOUT_MILLIS =
CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS; CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
private final CronetEngine cronetEngine; private final CronetEngineWrapper cronetEngineWrapper;
private final Executor executor; private final Executor executor;
private final Predicate<String> contentTypePredicate; private final Predicate<String> contentTypePredicate;
private final TransferListener<? super DataSource> transferListener; private final TransferListener<? super DataSource> transferListener;
private final int connectTimeoutMs; private final int connectTimeoutMs;
private final int readTimeoutMs; private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects; 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, Executor executor, Predicate<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener) { TransferListener<? super DataSource> transferListener,
this(cronetEngine, executor, contentTypePredicate, transferListener, HttpDataSource.Factory fallbackFactory) {
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false); 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, Executor executor, Predicate<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener, int connectTimeoutMs, TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
int readTimeoutMs, boolean resetTimeoutOnRedirects) { int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) {
this.cronetEngine = cronetEngine; 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.executor = executor;
this.contentTypePredicate = contentTypePredicate; this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener; this.transferListener = transferListener;
this.connectTimeoutMs = connectTimeoutMs; this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs; this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
this.fallbackFactory = fallbackFactory;
} }
@Override @Override
protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties
defaultRequestProperties) { defaultRequestProperties) {
CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();
if (cronetEngine == null) {
return fallbackFactory.createDataSource();
}
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties); connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
} }

View File

@ -0,0 +1,238 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cronet;
import android.content.Context;
import android.support.annotation.IntDef;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.chromium.net.CronetEngine;
import org.chromium.net.CronetProvider;
/**
* A wrapper class for a {@link CronetEngine}.
*/
public final class CronetEngineWrapper {
private static final String TAG = "CronetEngineWrapper";
private final CronetEngine cronetEngine;
private final @CronetEngineSource int cronetEngineSource;
/**
* Source of {@link CronetEngine}.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})
public @interface CronetEngineSource {}
/**
* Natively bundled Cronet implementation.
*/
public static final int SOURCE_NATIVE = 0;
/**
* Cronet implementation from GMSCore.
*/
public static final int SOURCE_GMS = 1;
/**
* Other (unknown) Cronet implementation.
*/
public static final int SOURCE_UNKNOWN = 2;
/**
* User-provided Cronet engine.
*/
public static final int SOURCE_USER_PROVIDED = 3;
/**
* No Cronet implementation available. Fallback Http provider is used if possible.
*/
public static final int SOURCE_UNAVAILABLE = 4;
/**
* Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
* {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet
* if both are available.
*
* @param context A context.
*/
public CronetEngineWrapper(Context context) {
this(context, false);
}
/**
* Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
* {@link CronetProvider} based on user preference.
*
* @param context A context.
* @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively
* bundled Cronet if both are available.
*/
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
CronetEngine cronetEngine = null;
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
List<CronetProvider> cronetProviders = CronetProvider.getAllProviders(context);
// Remove disabled and fallback Cronet providers from list
for (int i = cronetProviders.size() - 1; i >= 0; i--) {
if (!cronetProviders.get(i).isEnabled()
|| CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) {
cronetProviders.remove(i);
}
}
// Sort remaining providers by type and version.
CronetProviderComparator providerComparator = new CronetProviderComparator(preferGMSCoreCronet);
Collections.sort(cronetProviders, providerComparator);
for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) {
String providerName = cronetProviders.get(i).getName();
try {
cronetEngine = cronetProviders.get(i).createBuilder().build();
if (providerComparator.isNativeProvider(providerName)) {
cronetEngineSource = SOURCE_NATIVE;
} else if (providerComparator.isGMSCoreProvider(providerName)) {
cronetEngineSource = SOURCE_GMS;
} else {
cronetEngineSource = SOURCE_UNKNOWN;
}
Log.d(TAG, "CronetEngine built using " + providerName);
} catch (SecurityException e) {
Log.w(TAG, "Failed to build CronetEngine. Please check if current process has "
+ "android.permission.ACCESS_NETWORK_STATE.");
} catch (UnsatisfiedLinkError e) {
Log.w(TAG, "Failed to link Cronet binaries. Please check if native Cronet binaries are "
+ "bundled into your app.");
}
}
if (cronetEngine == null) {
Log.w(TAG, "Cronet not available. Using fallback provider.");
}
this.cronetEngine = cronetEngine;
this.cronetEngineSource = cronetEngineSource;
}
/**
* Creates a wrapper for an existing CronetEngine.
*
* @param cronetEngine An existing CronetEngine.
*/
public CronetEngineWrapper(CronetEngine cronetEngine) {
this.cronetEngine = cronetEngine;
this.cronetEngineSource = SOURCE_USER_PROVIDED;
}
/**
* Returns the source of the wrapped {@link CronetEngine}.
*
* @return A {@link CronetEngineSource} value.
*/
public @CronetEngineSource int getCronetEngineSource() {
return cronetEngineSource;
}
/**
* Returns the wrapped {@link CronetEngine}.
*
* @return The CronetEngine, or null if no CronetEngine is available.
*/
/* package */ CronetEngine getCronetEngine() {
return cronetEngine;
}
private static class CronetProviderComparator implements Comparator<CronetProvider> {
private final String gmsCoreCronetName;
private final boolean preferGMSCoreCronet;
public CronetProviderComparator(boolean preferGMSCoreCronet) {
// GMSCore CronetProvider classes are only available in some configurations.
// Thus, we use reflection to copy static name.
String gmsCoreVersionString = null;
try {
Class<?> cronetProviderInstallerClass =
Class.forName("com.google.android.gms.net.CronetProviderInstaller");
Field providerNameField = cronetProviderInstallerClass.getDeclaredField("PROVIDER_NAME");
gmsCoreVersionString = (String) providerNameField.get(null);
} catch (ClassNotFoundException e) {
// GMSCore CronetProvider not available.
} catch (NoSuchFieldException e) {
// GMSCore CronetProvider not available.
} catch (IllegalAccessException e) {
// GMSCore CronetProvider not available.
}
gmsCoreCronetName = gmsCoreVersionString;
this.preferGMSCoreCronet = preferGMSCoreCronet;
}
@Override
public int compare(CronetProvider providerLeft, CronetProvider providerRight) {
int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName());
int typePreferenceRight = evaluateCronetProviderType(providerRight.getName());
if (typePreferenceLeft != typePreferenceRight) {
return typePreferenceLeft - typePreferenceRight;
}
return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion());
}
public boolean isNativeProvider(String providerName) {
return CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName);
}
public boolean isGMSCoreProvider(String providerName) {
return gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName);
}
/**
* Convert Cronet provider name into a sortable preference value.
* Smaller values are preferred.
*/
private int evaluateCronetProviderType(String providerName) {
if (isNativeProvider(providerName)) {
return 1;
}
if (isGMSCoreProvider(providerName)) {
return preferGMSCoreCronet ? 0 : 2;
}
// Unknown provider type.
return -1;
}
/**
* Compares version strings of format "12.123.35.23".
*/
private static int compareVersionStrings(String versionLeft, String versionRight) {
if (versionLeft == null || versionRight == null) {
return 0;
}
String[] versionStringsLeft = versionLeft.split("\\.");
String[] versionStringsRight = versionRight.split("\\.");
int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);
for (int i = 0; i < minLength; i++) {
if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
try {
int versionIntLeft = Integer.parseInt(versionStringsLeft[i]);
int versionIntRight = Integer.parseInt(versionStringsRight[i]);
return versionIntLeft - versionIntRight;
} catch (NumberFormatException e) {
return 0;
}
}
}
return 0;
}
}
}

View File

@ -9,11 +9,10 @@ audio.
## Build instructions ## ## Build instructions ##
* Checkout ExoPlayer along with Extensions 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
git clone https://github.com/google/ExoPlayer.git native components as follows:
```
* Set the following environment variables: * 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: * 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>" 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 ${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 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
like this: [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
```
// 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.

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
@ -30,7 +31,7 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
} }
ext { ext {

View File

@ -10,11 +10,10 @@ ExoPlayer to play Flac audio on Android devices.
## Build Instructions ## ## Build Instructions ##
* Checkout ExoPlayer along with Extensions: 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
git clone https://github.com/google/ExoPlayer.git native components as follows:
```
* Set the following environment variables: * 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: * 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>" NDK_PATH="<path to Android NDK>"
``` ```
@ -47,20 +44,5 @@ cd "${FLAC_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4 ${NDK_PATH}/ndk-build APP_ABI=all -j4
``` ```
* In your project, you can add a dependency to the Flac Extension by using a [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
rule like this: [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
```
// 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.

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
@ -30,8 +31,8 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
androidTestCompile project(':testutils') androidTestCompile project(modulePrefix + 'testutils')
} }
ext { ext {

View File

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

View File

@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.flac;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; 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}. * Unit test for {@link FlacExtractor}.
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public class FlacExtractorTest extends InstrumentationTestCase { public class FlacExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new FlacExtractor(); return new FlacExtractor();

View File

@ -126,6 +126,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
} }
} }
@Override
public void onRepeatModeChanged(int repeatMode) {
// Do nothing.
}
private void releasePlayerAndQuitLooper() { private void releasePlayerAndQuitLooper() {
player.release(); player.release();
Looper.myLooper().quit(); Looper.myLooper().quit();

View File

@ -22,6 +22,7 @@
#include <cassert> #include <cassert>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#define LOG_TAG "FLACParser" #define LOG_TAG "FLACParser"
#define ALOGE(...) \ #define ALOGE(...) \

View File

@ -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 GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
of surround sound and ambisonic soundfields. 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 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` 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 where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used. 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 * If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
return a GvrAudioProcessor. return a GvrAudioProcessor.
* If constructing renderers directly, pass a GvrAudioProcessor to * If constructing renderers directly, pass a GvrAudioProcessor to
MediaCodecAudioRenderer's constructor. MediaCodecAudioRenderer's constructor.
[Google VR SDK for Android]: https://developers.google.com/vr/android/ [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
@ -24,8 +25,8 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
compile 'com.google.vr:sdk-audio:1.30.0' compile 'com.google.vr:sdk-audio:1.60.1'
} }
ext { ext {

View File

@ -82,6 +82,9 @@ public final class GvrAudioProcessor implements AudioProcessor {
maybeReleaseGvrAudioSurround(); maybeReleaseGvrAudioSurround();
int surroundFormat; int surroundFormat;
switch (channelCount) { switch (channelCount) {
case 1:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
break;
case 2: case 2:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO; surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
break; break;

43
extensions/ima/README.md Normal file
View File

@ -0,0 +1,43 @@
# 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://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
## Getting the extension ##
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][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
## Using the extension ##
Pass a single-window content `MediaSource` to `ImaAdsMediaSource`'s constructor,
along with a `ViewGroup` that is on top of the player and the ad tag URI to
show. The IMA documentation includes some [sample ad tags][] for testing. Then
pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`.
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.
[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
## Known issues ##
This is a preview version with some known issues:
* Tapping the 'More info' button on an ad in the demo app will pause the
activity, which destroys the ImaAdsMediaSource. Played ad breaks will be
shown to the user again if the demo app returns to the foreground.
* Ad loading timeouts are currently propagated as player errors, rather than
being silently handled by resuming content.

View File

@ -0,0 +1,25 @@
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')
compile 'com.android.support:support-annotations:' + supportLibraryVersion
compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
compile 'com.google.android.gms:play-services-ads:11.0.1'
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
}

View File

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

View File

@ -0,0 +1,614 @@
/*
* 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.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.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;
/**
* Loads ads using the IMA SDK. All methods are called on the main thread.
*/
/* package */ final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlayer,
ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener {
private static final boolean DEBUG = false;
private static final String TAG = "ImaAdsLoader";
/**
* Listener for ad loader events. All methods are called on the main thread.
*/
public interface EventListener {
/**
* Called when the times of ad groups are known.
*
* @param adGroupTimesUs The times of ad groups, in microseconds.
*/
void onAdGroupTimesUsLoaded(long[] adGroupTimesUs);
/**
* Called when an ad group has been played to the end.
*
* @param adGroupIndex The index of the ad group.
*/
void onAdGroupPlayedToEnd(int adGroupIndex);
/**
* Called when the URI for the media of an ad has been loaded.
*
* @param adGroupIndex The index of the ad group containing the ad with the media URI.
* @param adIndexInAdGroup The index of the ad in its ad group.
* @param uri The URI for the ad's media.
*/
void onAdUriLoaded(int adGroupIndex, int adIndexInAdGroup, Uri uri);
/**
* Called when an ad group has loaded.
*
* @param adGroupIndex The index of the ad group containing the ad.
* @param adCountInAdGroup The number of ads in the ad group.
*/
void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup);
/**
* Called when there was an error loading ads.
*
* @param error The error.
*/
void onLoadError(IOException error);
}
/**
* 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 EventListener eventListener;
private final ExoPlayer player;
private final Timeline.Period period;
private final List<VideoAdPlayerCallback> adCallbacks;
private final AdsLoader adsLoader;
private AdsManager adsManager;
private long[] adGroupTimesUs;
private int[] adsLoadedInAdGroup;
private Timeline timeline;
private long contentDurationMs;
private boolean released;
// Fields tracking IMA's state.
/**
* The index of the current ad group that IMA is loading.
*/
private int adGroupIndex;
/**
* If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #playAd()} and not
* {@link #stopAd()}.
*/
private boolean playingAd;
/**
* If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #pauseAd()} since
* a preceding call to {@link #playAd()} for the current ad.
*/
private boolean pausedInAd;
/**
* Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback.
*/
private boolean sentContentComplete;
// Fields tracking the player/loader state.
/**
* If the player is playing an ad, stores the ad group index. {@link C#INDEX_UNSET} otherwise.
*/
private int playingAdGroupIndex;
/**
* 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;
/**
* 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;
/**
* 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 adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
* @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.
* @param player The player instance that will play the loaded ad schedule.
* @param eventListener Listener for ad loader events.
*/
public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup,
ImaSdkSettings imaSdkSettings, ExoPlayer player, EventListener eventListener) {
this.eventListener = eventListener;
this.player = player;
period = new Timeline.Period();
adCallbacks = new ArrayList<>(1);
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
pendingContentPositionMs = C.TIME_UNSET;
adGroupIndex = C.INDEX_UNSET;
contentDurationMs = C.TIME_UNSET;
player.addListener(this);
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
AdDisplayContainer adDisplayContainer = imaSdkFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(this);
adDisplayContainer.setAdContainer(adUiViewGroup);
if (imaSdkSettings == null) {
imaSdkSettings = imaSdkFactory.createImaSdkSettings();
}
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
AdsRequest request = imaSdkFactory.createAdsRequest();
request.setAdTagUrl(adTagUri.toString());
request.setAdDisplayContainer(adDisplayContainer);
request.setContentProgressProvider(this);
adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings);
adsLoader.addAdErrorListener(this);
adsLoader.addAdsLoadedListener(this);
adsLoader.requestAds(request);
}
/**
* Releases the loader. Must be called when the instance is no longer needed.
*/
public void release() {
if (adsManager != null) {
adsManager.destroy();
adsManager = null;
}
player.removeListener(this);
released = true;
}
// AdsLoader.AdsLoadedListener implementation.
@Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
adsManager = adsManagerLoadedEvent.getAdsManager();
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");
}
}
adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
adsLoadedInAdGroup = new int[adGroupTimesUs.length];
eventListener.onAdGroupTimesUsLoaded(adGroupTimesUs);
}
// AdEvent.AdEventListener implementation.
@Override
public void onAdEvent(AdEvent adEvent) {
Ad ad = adEvent.getAd();
if (DEBUG) {
Log.d(TAG, "onAdEvent " + adEvent.getType());
}
if (released) {
// The ads manager may pass CONTENT_RESUME_REQUESTED after it is destroyed.
return;
}
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 ? adGroupTimesUs.length - 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);
}
eventListener.onAdGroupLoaded(adGroupIndex, adCountInAdGroup);
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.
pauseContentInternal();
break;
case SKIPPED: // Fall through.
case CONTENT_RESUME_REQUESTED:
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);
}
IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError());
eventListener.onLoadError(exception);
// TODO: Provide a timeline to the player if it doesn't have one yet, so the content can play.
}
// ContentProgressProvider implementation.
@Override
public VideoProgressUpdate getContentProgress() {
if (pendingContentPositionMs != C.TIME_UNSET) {
sentPendingContentPositionMs = true;
return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs);
}
if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long adGroupTimeMs = C.usToMs(adGroupTimesUs[adGroupIndex]);
if (adGroupTimeMs == C.TIME_END_OF_SOURCE) {
adGroupTimeMs = contentDurationMs;
}
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
return new VideoProgressUpdate(adGroupTimeMs + elapsedSinceEndMs, contentDurationMs);
}
if (player.isPlayingAd() || contentDurationMs == C.TIME_UNSET) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
}
// VideoAdPlayer implementation.
@Override
public VideoProgressUpdate getAdProgress() {
if (!player.isPlayingAd()) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration());
}
@Override
public void loadAd(String adUriString) {
int adIndexInAdGroup = adsLoadedInAdGroup[adGroupIndex]++;
if (DEBUG) {
Log.d(TAG, "loadAd at index " + adIndexInAdGroup + " in ad group " + adGroupIndex);
}
eventListener.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString));
}
@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 (playingAd && !pausedInAd) {
// Work around an issue where IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
if (DEBUG) {
Log.d(TAG, "Unexpected playAd without stopAd");
}
stopAdInternal();
}
player.setPlayWhenReady(true);
if (!playingAd) {
playingAd = true;
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onPlay();
}
} else if (pausedInAd) {
pausedInAd = false;
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onResume();
}
}
}
@Override
public void stopAd() {
if (!playingAd) {
if (DEBUG) {
Log.d(TAG, "Ignoring unexpected stopAd");
}
return;
}
if (DEBUG) {
Log.d(TAG, "stopAd");
}
stopAdInternal();
}
@Override
public void pauseAd() {
if (DEBUG) {
Log.d(TAG, "pauseAd");
}
if (released || !playingAd) {
// This method is called after content is resumed, and may also be called after release.
return;
}
pausedInAd = true;
player.setPlayWhenReady(false);
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onPause();
}
}
@Override
public void resumeAd() {
// This method is never called. See [Internal: b/18931719].
throw new IllegalStateException();
}
// ExoPlayer.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);
if (player.isPlayingAd()) {
playingAdGroupIndex = player.getCurrentAdGroupIndex();
playingAdIndexInAdGroup = player.getCurrentAdIndexInAdGroup();
}
}
@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 (!playingAd && playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) {
checkForContentComplete();
} else if (playingAd && playbackState == ExoPlayer.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 (player.isPlayingAd()) {
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onError();
}
}
}
@Override
public void onPositionDiscontinuity() {
if (!player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) {
long positionUs = C.msToUs(player.getCurrentPosition());
int adGroupIndex = timeline.getPeriod(0, period).getAdGroupIndexForPositionUs(positionUs);
if (adGroupIndex != C.INDEX_UNSET) {
sentPendingContentPositionMs = false;
pendingContentPositionMs = player.getCurrentPosition();
}
return;
}
boolean adFinished = (!player.isPlayingAd() && playingAdGroupIndex != C.INDEX_UNSET)
|| (player.isPlayingAd() && 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 (player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) {
player.setPlayWhenReady(false);
// IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET);
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
if (adGroupIndex == adGroupTimesUs.length - 1) {
adsLoader.contentComplete();
if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete");
}
}
}
boolean isPlayingAd = player.isPlayingAd();
playingAdGroupIndex = isPlayingAd ? player.getCurrentAdGroupIndex() : C.INDEX_UNSET;
playingAdIndexInAdGroup = isPlayingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
// Do nothing.
}
// Internal methods.
/**
* Resumes the player, ensuring the current period is a content period by seeking if necessary.
*/
private void resumeContentInternal() {
if (contentDurationMs != C.TIME_UNSET) {
if (playingAd) {
// Work around an issue where IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
if (DEBUG) {
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
}
stopAdInternal();
}
}
player.setPlayWhenReady(true);
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;
player.setPlayWhenReady(false);
clearFlags();
}
private void stopAdInternal() {
Assertions.checkState(playingAd);
player.setPlayWhenReady(false);
if (!player.isPlayingAd()) {
eventListener.onAdGroupPlayedToEnd(adGroupIndex);
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.
playingAd = false;
pausedInAd = false;
}
private void checkForContentComplete() {
if (contentDurationMs != C.TIME_UNSET
&& player.getCurrentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
&& !sentContentComplete) {
adsLoader.contentComplete();
if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete");
}
sentContentComplete = true;
}
}
private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.
return new long[] {0};
}
int count = cuePoints.size();
long[] adGroupTimesUs = new long[count];
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
adGroupTimesUs[i] =
cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint);
}
return adGroupTimesUs;
}
}

View File

@ -0,0 +1,360 @@
/*
* 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.Handler;
import android.os.Looper;
import android.view.ViewGroup;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
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 {
private final MediaSource contentMediaSource;
private final DataSource.Factory dataSourceFactory;
private final Context context;
private final Uri adTagUri;
private final ViewGroup adUiViewGroup;
private final ImaSdkSettings imaSdkSettings;
private final Handler mainHandler;
private final AdListener adLoaderListener;
private final Map<MediaPeriod, MediaSource> adMediaSourceByMediaPeriod;
private final Timeline.Period period;
private Handler playerHandler;
private ExoPlayer player;
private volatile boolean released;
// Accessed on the player thread.
private Timeline contentTimeline;
private Object contentManifest;
private long[] adGroupTimesUs;
private boolean[] hasPlayedAdGroup;
private int[] adCounts;
private MediaSource[][] adGroupMediaSources;
private boolean[][] isAdAvailable;
private long[][] adDurationsUs;
private MediaSource.Listener listener;
private IOException adLoadError;
// Accessed on the main thread.
private ImaAdsLoader imaAdsLoader;
/**
* 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 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 adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad user
* interface.
*/
public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
Context context, Uri adTagUri, ViewGroup adUiViewGroup) {
this(contentMediaSource, dataSourceFactory, context, adTagUri, adUiViewGroup, 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 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 adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
* @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 ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
Context context, Uri adTagUri, ViewGroup adUiViewGroup, ImaSdkSettings imaSdkSettings) {
this.contentMediaSource = contentMediaSource;
this.dataSourceFactory = dataSourceFactory;
this.context = context;
this.adTagUri = adTagUri;
this.adUiViewGroup = adUiViewGroup;
this.imaSdkSettings = imaSdkSettings;
mainHandler = new Handler(Looper.getMainLooper());
adLoaderListener = new AdListener();
adMediaSourceByMediaPeriod = new HashMap<>();
period = new Timeline.Period();
adGroupMediaSources = new MediaSource[0][];
isAdAvailable = new boolean[0][];
adDurationsUs = new long[0][];
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
Assertions.checkArgument(isTopLevelSource);
this.listener = listener;
this.player = player;
playerHandler = new Handler();
mainHandler.post(new Runnable() {
@Override
public void run() {
imaAdsLoader = new ImaAdsLoader(context, adTagUri, adUiViewGroup, imaSdkSettings,
ImaAdsMediaSource.this.player, adLoaderListener);
}
});
contentMediaSource.prepareSource(player, false, new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
ImaAdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest);
}
});
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (adLoadError != null) {
throw adLoadError;
}
contentMediaSource.maybeThrowSourceInfoRefreshError();
for (MediaSource[] mediaSources : adGroupMediaSources) {
for (MediaSource mediaSource : mediaSources) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
if (id.isAd()) {
MediaSource mediaSource = adGroupMediaSources[id.adGroupIndex][id.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;
adLoadError = null;
contentMediaSource.releaseSource();
for (MediaSource[] mediaSources : adGroupMediaSources) {
for (MediaSource mediaSource : mediaSources) {
mediaSource.releaseSource();
}
}
mainHandler.post(new Runnable() {
@Override
public void run() {
// TODO: The source will be released when the application is paused/stopped, which can occur
// if the user taps on the ad. In this case, we should keep the ads manager alive but pause
// it, instead of destroying it.
imaAdsLoader.release();
imaAdsLoader = null;
}
});
}
// Internal methods.
private void onAdGroupTimesUsLoaded(long[] adGroupTimesUs) {
Assertions.checkState(this.adGroupTimesUs == null);
int adGroupCount = adGroupTimesUs.length;
this.adGroupTimesUs = adGroupTimesUs;
hasPlayedAdGroup = new boolean[adGroupCount];
adCounts = new int[adGroupCount];
Arrays.fill(adCounts, C.LENGTH_UNSET);
adGroupMediaSources = new MediaSource[adGroupCount][];
Arrays.fill(adGroupMediaSources, new MediaSource[0]);
isAdAvailable = new boolean[adGroupCount][];
Arrays.fill(isAdAvailable, new boolean[0]);
adDurationsUs = new long[adGroupCount][];
Arrays.fill(adDurationsUs, new long[0]);
maybeUpdateSourceInfo();
}
private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) {
contentTimeline = timeline;
contentManifest = manifest;
maybeUpdateSourceInfo();
}
private void onAdGroupPlayedToEnd(int adGroupIndex) {
hasPlayedAdGroup[adGroupIndex] = true;
maybeUpdateSourceInfo();
}
private void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, Uri uri) {
MediaSource adMediaSource = new ExtractorMediaSource(uri, dataSourceFactory,
new DefaultExtractorsFactory(), mainHandler, adLoaderListener);
int oldAdCount = adGroupMediaSources[adGroupIndex].length;
if (adIndexInAdGroup >= oldAdCount) {
int adCount = adIndexInAdGroup + 1;
adGroupMediaSources[adGroupIndex] = Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);
isAdAvailable[adGroupIndex] = Arrays.copyOf(isAdAvailable[adGroupIndex], adCount);
adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount);
Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET);
}
adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
isAdAvailable[adGroupIndex][adIndexInAdGroup] = true;
adMediaSource.prepareSource(player, false, new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline);
}
});
}
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 onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup) {
if (adCounts[adGroupIndex] == C.LENGTH_UNSET) {
adCounts[adGroupIndex] = adCountInAdGroup;
maybeUpdateSourceInfo();
}
}
private void maybeUpdateSourceInfo() {
if (adGroupTimesUs != null && contentTimeline != null) {
SinglePeriodAdTimeline timeline = new SinglePeriodAdTimeline(contentTimeline, adGroupTimesUs,
hasPlayedAdGroup, adCounts, isAdAvailable, adDurationsUs);
listener.onSourceInfoRefreshed(timeline, contentManifest);
}
}
/**
* Listener for ad loading events. All methods are called on the main thread.
*/
private final class AdListener implements ImaAdsLoader.EventListener,
ExtractorMediaSource.EventListener {
@Override
public void onAdGroupTimesUsLoaded(final long[] adGroupTimesUs) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdGroupTimesUsLoaded(adGroupTimesUs);
}
});
}
@Override
public void onAdGroupPlayedToEnd(final int adGroupIndex) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdGroupPlayedToEnd(adGroupIndex);
}
});
}
@Override
public void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, final Uri uri) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, uri);
}
});
}
@Override
public void onAdGroupLoaded(final int adGroupIndex, final int adCountInAdGroup) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdGroupLoaded(adGroupIndex, adCountInAdGroup);
}
});
}
@Override
public void onLoadError(final IOException error) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
adLoadError = error;
}
});
}
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.
*/
public final class SinglePeriodAdTimeline extends Timeline {
private final Timeline contentTimeline;
private final long[] adGroupTimesUs;
private final boolean[] hasPlayedAdGroup;
private final int[] adCounts;
private final boolean[][] isAdAvailable;
private final long[][] adDurationsUs;
/**
* 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 hasPlayedAdGroup Whether each ad group has been played.
* @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 isAdAvailable Whether each ad in each ad group is available.
* @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.
*/
public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs,
boolean[] hasPlayedAdGroup, int[] adCounts, boolean[][] isAdAvailable,
long[][] adDurationsUs) {
Assertions.checkState(contentTimeline.getPeriodCount() == 1);
Assertions.checkState(contentTimeline.getWindowCount() == 1);
this.contentTimeline = contentTimeline;
this.adGroupTimesUs = adGroupTimesUs;
this.hasPlayedAdGroup = hasPlayedAdGroup;
this.adCounts = adCounts;
this.isAdAvailable = isAdAvailable;
this.adDurationsUs = adDurationsUs;
}
@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, hasPlayedAdGroup, adCounts,
isAdAvailable, adDurationsUs);
return period;
}
@Override
public int getIndexOfPeriod(Object uid) {
return contentTimeline.getIndexOfPeriod(uid);
}
}

View File

@ -5,19 +5,12 @@
The OkHttp Extension is an [HttpDataSource][] implementation using Square's The OkHttp Extension is an [HttpDataSource][] implementation using Square's
[OkHttp][]. [OkHttp][].
## Using the extension ## [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[OkHttp]: https://square.github.io/okhttp/
The easiest way to use the extension is to add it as a gradle dependency. You ## Getting the extension ##
need to make sure you have the jcenter repository included in the `build.gradle`
file in the root of your project:
```gradle The easiest way to use the extension is to add it as a gradle dependency:
repositories {
jcenter()
}
```
Next, include the following in your module's `build.gradle` file:
```gradle ```gradle
compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X' compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
@ -26,5 +19,8 @@ compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
where `rX.X.X` is the version, which must match the version of the ExoPlayer where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used. library being used.
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html Alternatively, you can clone the ExoPlayer repository and depend on the module
[OkHttp]: https://square.github.io/okhttp/ locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
@ -29,7 +30,7 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
compile('com.squareup.okhttp3:okhttp:3.6.0') { compile('com.squareup.okhttp3:okhttp:3.6.0') {
exclude group: 'org.json' exclude group: 'org.json'
} }

View File

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.ext.okhttp; package com.google.android.exoplayer2.ext.okhttp;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
@ -45,13 +47,14 @@ public class OkHttpDataSource implements HttpDataSource {
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>(); private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
private final Call.Factory callFactory; @NonNull private final Call.Factory callFactory;
private final String userAgent; @NonNull private final RequestProperties requestProperties;
private final Predicate<String> contentTypePredicate;
private final TransferListener<? super OkHttpDataSource> listener; @Nullable private final String userAgent;
private final CacheControl cacheControl; @Nullable private final Predicate<String> contentTypePredicate;
private final RequestProperties defaultRequestProperties; @Nullable private final TransferListener<? super OkHttpDataSource> listener;
private final RequestProperties requestProperties; @Nullable private final CacheControl cacheControl;
@Nullable private final RequestProperties defaultRequestProperties;
private DataSpec dataSpec; private DataSpec dataSpec;
private Response response; private Response response;
@ -67,33 +70,34 @@ public class OkHttpDataSource implements HttpDataSource {
/** /**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source. * by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent An optional User-Agent string.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
*/ */
public OkHttpDataSource(Call.Factory callFactory, String userAgent, public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
Predicate<String> contentTypePredicate) { @Nullable Predicate<String> contentTypePredicate) {
this(callFactory, userAgent, contentTypePredicate, null); this(callFactory, userAgent, contentTypePredicate, null);
} }
/** /**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source. * by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent An optional User-Agent string.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from * predicate then a {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}. * {@link #open(DataSpec)}.
* @param listener An optional listener. * @param listener An optional listener.
*/ */
public OkHttpDataSource(Call.Factory callFactory, String userAgent, public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener) { @Nullable Predicate<String> contentTypePredicate,
@Nullable TransferListener<? super OkHttpDataSource> listener) {
this(callFactory, userAgent, contentTypePredicate, listener, null, null); this(callFactory, userAgent, contentTypePredicate, listener, null, null);
} }
/** /**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source. * by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent An optional User-Agent string.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from * predicate then a {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}. * {@link #open(DataSpec)}.
@ -102,11 +106,12 @@ public class OkHttpDataSource implements HttpDataSource {
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
* the server as HTTP headers on every request. * the server as HTTP headers on every request.
*/ */
public OkHttpDataSource(Call.Factory callFactory, String userAgent, public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener, @Nullable Predicate<String> contentTypePredicate,
CacheControl cacheControl, RequestProperties defaultRequestProperties) { @Nullable TransferListener<? super OkHttpDataSource> listener,
@Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) {
this.callFactory = Assertions.checkNotNull(callFactory); this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = Assertions.checkNotEmpty(userAgent); this.userAgent = userAgent;
this.contentTypePredicate = contentTypePredicate; this.contentTypePredicate = contentTypePredicate;
this.listener = listener; this.listener = listener;
this.cacheControl = cacheControl; this.cacheControl = cacheControl;
@ -280,7 +285,10 @@ public class OkHttpDataSource implements HttpDataSource {
} }
builder.addHeader("Range", rangeRequest); builder.addHeader("Range", rangeRequest);
} }
builder.addHeader("User-Agent", userAgent); if (userAgent != null) {
builder.addHeader("User-Agent", userAgent);
}
if (!allowGzip) { if (!allowGzip) {
builder.addHeader("Accept-Encoding", "identity"); builder.addHeader("Accept-Encoding", "identity");
} }

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.okhttp; package com.google.android.exoplayer2.ext.okhttp;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
@ -28,31 +30,32 @@ import okhttp3.Call;
*/ */
public final class OkHttpDataSourceFactory extends BaseFactory { public final class OkHttpDataSourceFactory extends BaseFactory {
private final Call.Factory callFactory; @NonNull private final Call.Factory callFactory;
private final String userAgent; @Nullable private final String userAgent;
private final TransferListener<? super DataSource> listener; @Nullable private final TransferListener<? super DataSource> listener;
private final CacheControl cacheControl; @Nullable private final CacheControl cacheControl;
/** /**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory. * by the sources created by the factory.
* @param userAgent The User-Agent string that should be used. * @param userAgent An optional User-Agent string.
* @param listener An optional listener. * @param listener An optional listener.
*/ */
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
TransferListener<? super DataSource> listener) { @Nullable TransferListener<? super DataSource> listener) {
this(callFactory, userAgent, listener, null); this(callFactory, userAgent, listener, null);
} }
/** /**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory. * by the sources created by the factory.
* @param userAgent The User-Agent string that should be used. * @param userAgent An optional User-Agent string.
* @param listener An optional listener. * @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
*/ */
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
TransferListener<? super DataSource> listener, CacheControl cacheControl) { @Nullable TransferListener<? super DataSource> listener,
@Nullable CacheControl cacheControl) {
this.callFactory = callFactory; this.callFactory = callFactory;
this.userAgent = userAgent; this.userAgent = userAgent;
this.listener = listener; this.listener = listener;

View File

@ -10,11 +10,10 @@ ExoPlayer to play Opus audio on Android devices.
## Build Instructions ## ## Build Instructions ##
* Checkout ExoPlayer along with Extensions: 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
git clone https://github.com/google/ExoPlayer.git native components as follows:
```
* Set the following environment variables: * Set the following environment variables:
@ -26,8 +25,6 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main"
* Download the [Android NDK][] and set its location in an environment variable: * 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>" NDK_PATH="<path to Android NDK>"
``` ```
@ -52,23 +49,8 @@ cd "${OPUS_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4 ${NDK_PATH}/ndk-build APP_ABI=all -j4
``` ```
* In your project, you can add a dependency to the Opus Extension by using a [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
rule like this: [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
```
// in settings.gradle
include ':..:ExoPlayer:library'
include ':..:ExoPlayer:extension-opus'
// in build.gradle
dependencies {
compile project(':..:ExoPlayer:library')
compile project(':..:ExoPlayer:extension-opus')
}
```
* Now, when you build your app, the Opus extension will be built and the native
libraries will be packaged along with the APK.
## Notes ## ## Notes ##

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
@ -30,7 +31,7 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
} }
ext { ext {

View File

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

View File

@ -126,6 +126,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
} }
} }
@Override
public void onRepeatModeChanged(int repeatMode) {
// Do nothing.
}
private void releasePlayerAndQuitLooper() { private void releasePlayerAndQuitLooper() {
player.release(); player.release();
Looper.myLooper().quit(); Looper.myLooper().quit();

View File

@ -10,11 +10,10 @@ VP9 video on Android devices.
## Build Instructions ## ## Build Instructions ##
* Checkout ExoPlayer along with Extensions: 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
git clone https://github.com/google/ExoPlayer.git native components as follows:
```
* Set the following environment variables: * Set the following environment variables:
@ -26,8 +25,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
* Download the [Android NDK][] and set its location in an environment variable: * 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>" NDK_PATH="<path to Android NDK>"
``` ```
@ -66,23 +63,8 @@ cd "${VP9_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4 ${NDK_PATH}/ndk-build APP_ABI=all -j4
``` ```
* In your project, you can add a dependency to the VP9 Extension by using a the [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
following rule: [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
```
// in settings.gradle
include ':..:ExoPlayer:library'
include ':..:ExoPlayer:extension-vp9'
// in build.gradle
dependencies {
compile project(':..:ExoPlayer:library')
compile project(':..:ExoPlayer:extension-vp9')
}
```
* Now, when you build your app, the VP9 extension will be built and the native
libraries will be packaged along with the APK.
## Notes ## ## Notes ##
@ -94,4 +76,3 @@ dependencies {
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
please note that `generate_libvpx_android_configs.sh` and the makefiles need please note that `generate_libvpx_android_configs.sh` and the makefiles need
to be modified to work with arbitrary versions of libvpx and libyuv. to be modified to work with arbitrary versions of libvpx and libyuv.

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
@ -30,7 +31,7 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
} }
ext { ext {

View File

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

View File

@ -158,6 +158,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
} }
} }
@Override
public void onRepeatModeChanged(int repeatMode) {
// Do nothing.
}
private void releasePlayerAndQuitLooper() { private void releasePlayerAndQuitLooper() {
player.release(); player.release();
Looper.myLooper().quit(); Looper.myLooper().quit();

View File

@ -20,6 +20,7 @@ import android.graphics.Canvas;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.annotation.IntDef;
import android.view.Surface; import android.view.Surface;
import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
@ -30,6 +31,7 @@ import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -38,12 +40,35 @@ import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** /**
* Decodes and renders video using the native VP9 decoder. * Decodes and renders video using the native VP9 decoder.
*/ */
public final class LibvpxVideoRenderer extends BaseRenderer { public final class LibvpxVideoRenderer extends BaseRenderer {
@Retention(RetentionPolicy.SOURCE)
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
REINITIALIZATION_STATE_WAIT_END_OF_STREAM})
private @interface ReinitializationState {}
/**
* The decoder does not need to be re-initialized.
*/
private static final int REINITIALIZATION_STATE_NONE = 0;
/**
* The input format has changed in a way that requires the decoder to be re-initialized, but we
* haven't yet signaled an end of stream to the existing decoder. We need to do so in order to
* ensure that it outputs any remaining buffers before we release it.
*/
private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
/**
* The input format has changed in a way that requires the decoder to be re-initialized, and we've
* signaled an end of stream to the existing decoder. We're waiting for the decoder to output an
* end of stream signal to indicate that it has output any remaining buffers before we release it.
*/
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
/** /**
* The type of a message that can be passed to an instance of this class via * The type of a message that can be passed to an instance of this class via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
@ -71,12 +96,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private DecoderCounters decoderCounters; private DecoderCounters decoderCounters;
private Format format; private Format format;
private VpxDecoder decoder; private VpxDecoder decoder;
private DecoderInputBuffer inputBuffer; private VpxInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer; private VpxOutputBuffer outputBuffer;
private VpxOutputBuffer nextOutputBuffer; private VpxOutputBuffer nextOutputBuffer;
private DrmSession<ExoMediaCrypto> drmSession; private DrmSession<ExoMediaCrypto> drmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession; private DrmSession<ExoMediaCrypto> pendingDrmSession;
@ReinitializationState
private int decoderReinitializationState;
private boolean decoderReceivedBuffers;
private Bitmap bitmap; private Bitmap bitmap;
private boolean renderedFirstFrame; private boolean renderedFirstFrame;
private long joiningDeadlineMs; private long joiningDeadlineMs;
@ -153,6 +182,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
outputMode = VpxDecoder.OUTPUT_MODE_NONE; outputMode = VpxDecoder.OUTPUT_MODE_NONE;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
} }
@Override @Override
@ -185,49 +215,25 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
} }
} }
// We have a format. // If we don't have a decoder yet, we need to instantiate one.
drmSession = pendingDrmSession; maybeInitDecoder();
ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) { if (decoder != null) {
int drmSessionState = drmSession.getState(); try {
if (drmSessionState == DrmSession.STATE_ERROR) { // Rendering loop.
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); TraceUtil.beginSection("drainAndFeed");
} else if (drmSessionState == DrmSession.STATE_OPENED while (drainOutputBuffer(positionUs)) {}
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { while (feedInputBuffer()) {}
mediaCrypto = drmSession.getMediaCrypto();
} else {
// The drm session isn't open yet.
return;
}
}
try {
if (decoder == null) {
// If we don't have a decoder yet, we need to instantiate one.
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createVpxDecoder");
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
decoder.setOutputMode(outputMode);
TraceUtil.endSection(); TraceUtil.endSection();
long codecInitializedTimestamp = SystemClock.elapsedRealtime(); } catch (VpxDecoderException e) {
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, throw ExoPlaybackException.createForRenderer(e, getIndex());
codecInitializedTimestamp - codecInitializingTimestamp);
decoderCounters.decoderInitCount++;
} }
TraceUtil.beginSection("drainAndFeed"); decoderCounters.ensureUpdated();
while (drainOutputBuffer(positionUs)) {}
while (feedInputBuffer()) {}
TraceUtil.endSection();
} catch (VpxDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
} }
decoderCounters.ensureUpdated();
} }
private boolean drainOutputBuffer(long positionUs) throws VpxDecoderException { private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException,
if (outputStreamEnded) { VpxDecoderException {
return false;
}
// Acquire outputBuffer either from nextOutputBuffer or from the decoder. // Acquire outputBuffer either from nextOutputBuffer or from the decoder.
if (outputBuffer == null) { if (outputBuffer == null) {
if (nextOutputBuffer != null) { if (nextOutputBuffer != null) {
@ -247,15 +253,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
} }
if (outputBuffer.isEndOfStream()) { if (outputBuffer.isEndOfStream()) {
outputStreamEnded = true; if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
outputBuffer.release(); // We're waiting to re-initialize the decoder, and have now processed all final buffers.
outputBuffer = null; releaseDecoder();
maybeInitDecoder();
} else {
outputBuffer.release();
outputBuffer = null;
outputStreamEnded = true;
}
return false; return false;
} }
if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes. // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
if (outputBuffer.timeUs <= positionUs) { if (isBufferLate(outputBuffer.timeUs - positionUs)) {
skipBuffer(); skipBuffer();
return true; return true;
} }
@ -280,23 +292,20 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return false; return false;
} }
/** /**
* Returns whether the current frame should be dropped. * Returns whether the current frame should be dropped.
* *
* @param outputBufferTimeUs The timestamp of the current output buffer. * @param outputBufferTimeUs The timestamp of the current output buffer.
* @param nextOutputBufferTimeUs The timestamp of the next output buffer or * @param nextOutputBufferTimeUs The timestamp of the next output buffer or
* {@link TIME_UNSET} if the next output buffer is unavailable. * {@link C#TIME_UNSET} if the next output buffer is unavailable.
* @param positionUs The current playback position. * @param positionUs The current playback position.
* @param joiningDeadlineMs The joining deadline. * @param joiningDeadlineMs The joining deadline.
* @return Returns whether to drop the current output buffer. * @return Returns whether to drop the current output buffer.
*/ */
protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs, protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs,
long positionUs, long joiningDeadlineMs) { long positionUs, long joiningDeadlineMs) {
// Drop the frame if we're joining and are more than 30ms late, or if we have the next frame return isBufferLate(outputBufferTimeUs - positionUs)
// and that's also late. Else we'll render what we have. && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
return (joiningDeadlineMs != C.TIME_UNSET && outputBufferTimeUs < positionUs - 30000)
|| (nextOutputBufferTimeUs != C.TIME_UNSET && nextOutputBufferTimeUs < positionUs);
} }
private void renderBuffer() { private void renderBuffer() {
@ -356,7 +365,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
} }
private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException { private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
if (inputStreamEnded) { if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
|| inputStreamEnded) {
// We need to reinitialize the decoder or the input stream has ended.
return false; return false;
} }
@ -367,6 +378,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
} }
} }
if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
decoder.queueInputBuffer(inputBuffer);
inputBuffer = null;
decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
return false;
}
int result; int result;
if (waitingForKeys) { if (waitingForKeys) {
// We've already read an encrypted sample into buffer, and are waiting for keys. // We've already read an encrypted sample into buffer, and are waiting for keys.
@ -394,36 +413,43 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return false; return false;
} }
inputBuffer.flip(); inputBuffer.flip();
inputBuffer.colorInfo = formatHolder.format.colorInfo;
decoder.queueInputBuffer(inputBuffer); decoder.queueInputBuffer(inputBuffer);
decoderReceivedBuffers = true;
decoderCounters.inputBufferCount++; decoderCounters.inputBufferCount++;
inputBuffer = null; inputBuffer = null;
return true; return true;
} }
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null) { if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false; return false;
} }
int drmSessionState = drmSession.getState(); @DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) { if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} }
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
} }
private void flushDecoder() { private void flushDecoder() throws ExoPlaybackException {
inputBuffer = null;
waitingForKeys = false; waitingForKeys = false;
if (outputBuffer != null) { if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
outputBuffer.release(); releaseDecoder();
outputBuffer = null; maybeInitDecoder();
} else {
inputBuffer = null;
if (outputBuffer != null) {
outputBuffer.release();
outputBuffer = null;
}
if (nextOutputBuffer != null) {
nextOutputBuffer.release();
nextOutputBuffer = null;
}
decoder.flush();
decoderReceivedBuffers = false;
} }
if (nextOutputBuffer != null) {
nextOutputBuffer.release();
nextOutputBuffer = null;
}
decoder.flush();
} }
@Override @Override
@ -461,7 +487,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
} }
@Override @Override
protected void onPositionReset(long positionUs, boolean joining) { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
clearRenderedFirstFrame(); clearRenderedFirstFrame();
@ -480,18 +506,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
protected void onStarted() { protected void onStarted() {
droppedFrames = 0; droppedFrames = 0;
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
joiningDeadlineMs = C.TIME_UNSET;
} }
@Override @Override
protected void onStopped() { protected void onStopped() {
joiningDeadlineMs = C.TIME_UNSET;
maybeNotifyDroppedFrames(); maybeNotifyDroppedFrames();
} }
@Override @Override
protected void onDisabled() { protected void onDisabled() {
inputBuffer = null;
outputBuffer = null;
format = null; format = null;
waitingForKeys = false; waitingForKeys = false;
clearReportedVideoSize(); clearReportedVideoSize();
@ -518,20 +542,53 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
} }
} }
private void releaseDecoder() { private void maybeInitDecoder() throws ExoPlaybackException {
if (decoder != null) { if (decoder != null) {
decoder.release(); return;
decoder = null; }
decoderCounters.decoderReleaseCount++;
waitingForKeys = false; drmSession = pendingDrmSession;
if (drmSession != null && pendingDrmSession != drmSession) { ExoMediaCrypto mediaCrypto = null;
try { if (drmSession != null) {
drmSessionManager.releaseSession(drmSession); mediaCrypto = drmSession.getMediaCrypto();
} finally { if (mediaCrypto == null) {
drmSession = null; DrmSessionException drmError = drmSession.getError();
if (drmError != null) {
throw ExoPlaybackException.createForRenderer(drmError, getIndex());
} }
// The drm session isn't open yet.
return;
} }
} }
try {
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createVpxDecoder");
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
decoder.setOutputMode(outputMode);
TraceUtil.endSection();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp);
decoderCounters.decoderInitCount++;
} catch (VpxDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
}
private void releaseDecoder() {
if (decoder == null) {
return;
}
inputBuffer = null;
outputBuffer = null;
nextOutputBuffer = null;
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false;
} }
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
@ -555,6 +612,17 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
} }
} }
if (pendingDrmSession != drmSession) {
if (decoderReceivedBuffers) {
// Signal end of stream and wait for any final output buffers before re-initialization.
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
} else {
// There aren't any final output buffers, so release the decoder immediately.
releaseDecoder();
maybeInitDecoder();
}
}
eventDispatcher.inputFormatChanged(format); eventDispatcher.inputFormatChanged(format);
} }
@ -654,4 +722,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
} }
} }
private static boolean isBufferLate(long earlyUs) {
// Class a buffer as late if it should have been presented more than 30ms ago.
return earlyUs < -30000;
}
} }

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DecryptionException; import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
@ -27,7 +26,7 @@ import java.nio.ByteBuffer;
* Vpx decoder. * Vpx decoder.
*/ */
/* package */ final class VpxDecoder extends /* package */ final class VpxDecoder extends
SimpleDecoder<DecoderInputBuffer, VpxOutputBuffer, VpxDecoderException> { SimpleDecoder<VpxInputBuffer, VpxOutputBuffer, VpxDecoderException> {
public static final int OUTPUT_MODE_NONE = -1; public static final int OUTPUT_MODE_NONE = -1;
public static final int OUTPUT_MODE_YUV = 0; public static final int OUTPUT_MODE_YUV = 0;
@ -54,7 +53,7 @@ import java.nio.ByteBuffer;
*/ */
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException { ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) { if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries."); throw new VpxDecoderException("Failed to load decoder native libraries.");
} }
@ -85,8 +84,8 @@ import java.nio.ByteBuffer;
} }
@Override @Override
protected DecoderInputBuffer createInputBuffer() { protected VpxInputBuffer createInputBuffer() {
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); return new VpxInputBuffer();
} }
@Override @Override
@ -100,7 +99,7 @@ import java.nio.ByteBuffer;
} }
@Override @Override
protected VpxDecoderException decode(DecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
boolean reset) { boolean reset) {
ByteBuffer inputData = inputBuffer.data; ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit(); int inputSize = inputData.limit();
@ -128,6 +127,7 @@ import java.nio.ByteBuffer;
} else if (getFrameResult == -1) { } else if (getFrameResult == -1) {
return new VpxDecoderException("Buffer initialization failed."); return new VpxDecoderException("Buffer initialization failed.");
} }
outputBuffer.colorInfo = inputBuffer.colorInfo;
return null; return null;
} }

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.video.ColorInfo;
/**
* Input buffer to a {@link VpxDecoder}.
*/
/* package */ final class VpxInputBuffer extends DecoderInputBuffer {
public ColorInfo colorInfo;
public VpxInputBuffer() {
super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
}
}

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.vp9; package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.decoder.OutputBuffer; import com.google.android.exoplayer2.decoder.OutputBuffer;
import com.google.android.exoplayer2.video.ColorInfo;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
@ -37,6 +38,8 @@ import java.nio.ByteBuffer;
public ByteBuffer data; public ByteBuffer data;
public int width; public int width;
public int height; public int height;
public ColorInfo colorInfo;
/** /**
* YUV planes for YUV mode. * YUV planes for YUV mode.
*/ */

View File

@ -51,8 +51,7 @@ config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
config[3]+=" --disable-avx2 --enable-pic" config[3]+=" --disable-avx2 --enable-pic"
arch[4]="arm64-v8a" arch[4]="arm64-v8a"
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --disable-neon" config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
config[4]+=" --disable-neon-asm"
arch[5]="x86_64" arch[5]="x86_64"
config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2" config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
@ -24,11 +25,11 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
compile project(':library-dash') compile project(modulePrefix + 'library-dash')
compile project(':library-hls') compile project(modulePrefix + 'library-hls')
compile project(':library-smoothstreaming') compile project(modulePrefix + 'library-smoothstreaming')
compile project(':library-ui') compile project(modulePrefix + 'library-ui')
} }
ext { ext {

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply from: '../../constants.gradle'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
@ -22,6 +23,7 @@ android {
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
// Workaround to prevent circular dependency on project :testutils.
sourceSets { sourceSets {
androidTest { androidTest {
java.srcDirs += "../../testutils/src/main/java/" java.srcDirs += "../../testutils/src/main/java/"

View File

@ -24,11 +24,13 @@
android:allowBackup="false" android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"> tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<uses-library android:name="android.test.runner"/> <uses-library android:name="android.test.runner"/>
<provider
android:authorities="com.google.android.exoplayer2.core.test"
android:name="com.google.android.exoplayer2.upstream.ContentDataSourceTest$TestContentProvider"/>
</application> </application>
<instrumentation <instrumentation
android:targetPackage="com.google.android.exoplayer2.core.test" android:targetPackage="com.google.android.exoplayer2.core.test"
android:name="android.test.InstrumentationTestRunner" android:name="android.test.InstrumentationTestRunner"/>
tools:replace="android:targetPackage"/>
</manifest> </manifest>

View File

@ -30,5 +30,6 @@ track 1:
time = 0 time = 0
flags = 1073741824 flags = 1073741824
data = length 39, hash B7FE77F4 data = length 39, hash B7FE77F4
crypto mode = 1
encryption key = length 16, hash 4CE944CF encryption key = length 16, hash 4CE944CF
tracksEnded = true tracksEnded = true

View File

@ -30,5 +30,6 @@ track 1:
time = 0 time = 0
flags = 1073741824 flags = 1073741824
data = length 24, hash E58668B1 data = length 24, hash E58668B1
crypto mode = 1
encryption key = length 16, hash 4CE944CF encryption key = length 16, hash 4CE944CF
tracksEnded = true tracksEnded = true

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -8,7 +8,7 @@ track 0:
bitrate = -1 bitrate = -1
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/x-flac sampleMimeType = audio/flac
maxInputSize = 768000 maxInputSize = 768000
width = -1 width = -1
height = -1 height = -1

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -15,28 +15,20 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.testutil.ExoPlayerWrapper;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.testutil.FakeRenderer;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import junit.framework.TestCase; import junit.framework.TestCase;
/** /**
@ -62,7 +54,7 @@ public final class ExoPlayerTest extends TestCase {
* error. * error.
*/ */
public void testPlayEmptyTimeline() throws Exception { public void testPlayEmptyTimeline() throws Exception {
PlayerWrapper playerWrapper = new PlayerWrapper(); ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = Timeline.EMPTY; Timeline timeline = Timeline.EMPTY;
MediaSource mediaSource = new FakeMediaSource(timeline, null); MediaSource mediaSource = new FakeMediaSource(timeline, null);
FakeRenderer renderer = new FakeRenderer(null); FakeRenderer renderer = new FakeRenderer(null);
@ -79,7 +71,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes a single period. * Tests playback of a source that exposes a single period.
*/ */
public void testPlaySinglePeriodTimeline() throws Exception { public void testPlaySinglePeriodTimeline() throws Exception {
PlayerWrapper playerWrapper = new PlayerWrapper(); ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
Object manifest = new Object(); Object manifest = new Object();
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT); MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
@ -98,7 +90,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes three periods. * Tests playback of a source that exposes three periods.
*/ */
public void testPlayMultiPeriodTimeline() throws Exception { public void testPlayMultiPeriodTimeline() throws Exception {
PlayerWrapper playerWrapper = new PlayerWrapper(); ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline( Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0),
@ -119,7 +111,7 @@ public final class ExoPlayerTest extends TestCase {
* source. * source.
*/ */
public void testReadAheadToEndDoesNotResetRenderer() throws Exception { public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
final PlayerWrapper playerWrapper = new PlayerWrapper(); final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline( Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 10), new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10), new TimelineWindowDefinition(false, false, 10),
@ -166,7 +158,7 @@ public final class ExoPlayerTest extends TestCase {
} }
public void testRepreparationGivesFreshSourceInfo() throws Exception { public void testRepreparationGivesFreshSourceInfo() throws Exception {
PlayerWrapper playerWrapper = new PlayerWrapper(); ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
@ -218,501 +210,54 @@ public final class ExoPlayerTest extends TestCase {
Pair.create(timeline, thirdSourceManifest)); Pair.create(timeline, thirdSourceManifest));
} }
/** public void testRepeatModeChanges() throws Exception {
* Wraps a player with its own handler thread. Timeline timeline = new FakeTimeline(
*/ new TimelineWindowDefinition(true, false, 100000),
private static final class PlayerWrapper implements ExoPlayer.EventListener { new TimelineWindowDefinition(true, false, 100000),
new TimelineWindowDefinition(true, false, 100000));
private final CountDownLatch sourceInfoCountDownLatch; final int[] actionSchedule = { // 0 -> 1
private final CountDownLatch endedCountDownLatch; ExoPlayer.REPEAT_MODE_ONE, // 1 -> 1
private final HandlerThread playerThread; ExoPlayer.REPEAT_MODE_OFF, // 1 -> 2
private final Handler handler; ExoPlayer.REPEAT_MODE_ONE, // 2 -> 2
private final LinkedList<Pair<Timeline, Object>> sourceInfos; ExoPlayer.REPEAT_MODE_ALL, // 2 -> 0
ExoPlayer.REPEAT_MODE_ONE, // 0 -> 0
private ExoPlayer player; -1, // 0 -> 0
private TrackGroupArray trackGroups; ExoPlayer.REPEAT_MODE_OFF, // 0 -> 1
private Exception exception; -1, // 1 -> 2
-1 // 2 -> ended
// Written only on the main thread. };
private volatile int positionDiscontinuityCount; int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2};
final LinkedList<Integer> windowIndices = new LinkedList<>();
public PlayerWrapper() { final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length);
sourceInfoCountDownLatch = new CountDownLatch(1); ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() {
endedCountDownLatch = new CountDownLatch(1); @Override
playerThread = new HandlerThread("ExoPlayerTest thread"); @SuppressWarnings("ResourceType")
playerThread.start(); public void onPositionDiscontinuity() {
handler = new Handler(playerThread.getLooper()); super.onPositionDiscontinuity();
sourceInfos = new LinkedList<>(); int actionIndex = actionSchedule.length - (int) actionCounter.getCount();
} if (actionSchedule[actionIndex] != -1) {
player.setRepeatMode(actionSchedule[actionIndex]);
// Called on the test thread.
public void blockUntilEnded(long timeoutMs) throws Exception {
if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
exception = new TimeoutException("Test playback timed out waiting for playback to end.");
}
release();
// Throw any pending exception (from playback, timing out or releasing).
if (exception != null) {
throw exception;
}
}
public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception {
if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("Test playback timed out waiting for source info.");
}
}
public void setup(final MediaSource mediaSource, final Renderer... renderers) {
handler.post(new Runnable() {
@Override
public void run() {
try {
player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector());
player.addListener(PlayerWrapper.this);
player.setPlayWhenReady(true);
player.prepare(mediaSource);
} catch (Exception e) {
handleError(e);
}
} }
}); windowIndices.add(player.getCurrentWindowIndex());
} actionCounter.countDown();
public void prepare(final MediaSource mediaSource) {
handler.post(new Runnable() {
@Override
public void run() {
try {
player.prepare(mediaSource);
} catch (Exception e) {
handleError(e);
}
}
});
}
public void release() throws InterruptedException {
handler.post(new Runnable() {
@Override
public void run() {
try {
if (player != null) {
player.release();
}
} catch (Exception e) {
handleError(e);
} finally {
playerThread.quit();
}
}
});
playerThread.join();
}
private void handleError(Exception exception) {
if (this.exception == null) {
this.exception = exception;
} }
endedCountDownLatch.countDown(); };
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
playerWrapper.setup(mediaSource, renderer);
boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
playerWrapper.release();
assertTrue("Test playback timed out waiting for action schedule to end.", finished);
if (playerWrapper.exception != null) {
throw playerWrapper.exception;
} }
assertEquals(expectedWindowIndices.length, windowIndices.size());
@SafeVarargs for (int i = 0; i < expectedWindowIndices.length; i++) {
public final void assertSourceInfosEquals(Pair<Timeline, Object>... sourceInfos) { assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue());
assertEquals(sourceInfos.length, this.sourceInfos.size());
for (Pair<Timeline, Object> sourceInfo : sourceInfos) {
assertEquals(sourceInfo, this.sourceInfos.remove());
}
} }
assertEquals(9, playerWrapper.positionDiscontinuityCount);
// ExoPlayer.EventListener implementation. assertTrue(renderer.isEnded);
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
@Override
public void onLoadingChanged(boolean isLoading) {
// Do nothing.
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_ENDED) {
endedCountDownLatch.countDown();
}
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
sourceInfos.add(Pair.create(timeline, manifest));
sourceInfoCountDownLatch.countDown();
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
this.trackGroups = trackGroups;
}
@Override
public void onPlayerError(ExoPlaybackException exception) {
handleError(exception);
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void onPositionDiscontinuity() {
positionDiscontinuityCount++;
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
// Do nothing.
}
}
private static final class TimelineWindowDefinition {
public final boolean isSeekable;
public final boolean isDynamic;
public final long durationUs;
public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) {
this.isSeekable = isSeekable;
this.isDynamic = isDynamic;
this.durationUs = durationUs;
}
}
private static final class FakeTimeline extends Timeline {
private final TimelineWindowDefinition[] windowDefinitions;
public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {
this.windowDefinitions = windowDefinitions;
}
@Override
public int getWindowCount() {
return windowDefinitions.length;
}
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
Object id = setIds ? windowIndex : null;
return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable,
windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0);
}
@Override
public int getPeriodCount() {
return windowDefinitions.length;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
Object id = setIds ? periodIndex : null;
return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false);
}
@Override
public int getIndexOfPeriod(Object uid) {
if (!(uid instanceof Integer)) {
return C.INDEX_UNSET;
}
int index = (Integer) uid;
return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET;
}
}
/**
* Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating
* the period will return a {@link FakeMediaPeriod}.
*/
private static class FakeMediaSource implements MediaSource {
private final Timeline timeline;
private final Object manifest;
private final TrackGroupArray trackGroupArray;
private final ArrayList<FakeMediaPeriod> activeMediaPeriods;
private boolean preparedSource;
private boolean releasedSource;
public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) {
this.timeline = timeline;
this.manifest = manifest;
TrackGroup[] trackGroups = new TrackGroup[formats.length];
for (int i = 0; i < formats.length; i++) {
trackGroups[i] = new TrackGroup(formats[i]);
}
trackGroupArray = new TrackGroupArray(trackGroups);
activeMediaPeriods = new ArrayList<>();
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
assertFalse(preparedSource);
preparedSource = true;
listener.onSourceInfoRefreshed(timeline, manifest);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
assertTrue(preparedSource);
}
@Override
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
Assertions.checkIndex(index, 0, timeline.getPeriodCount());
assertTrue(preparedSource);
assertFalse(releasedSource);
assertEquals(0, positionUs);
FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
activeMediaPeriods.add(mediaPeriod);
return mediaPeriod;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
assertTrue(preparedSource);
assertFalse(releasedSource);
FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod;
assertTrue(activeMediaPeriods.remove(fakeMediaPeriod));
fakeMediaPeriod.release();
}
@Override
public void releaseSource() {
assertTrue(preparedSource);
assertFalse(releasedSource);
assertTrue(activeMediaPeriods.isEmpty());
releasedSource = true;
}
}
/**
* Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that
* track will give the player a {@link FakeSampleStream}.
*/
private static final class FakeMediaPeriod implements MediaPeriod {
private final TrackGroupArray trackGroupArray;
private boolean preparedPeriod;
public FakeMediaPeriod(TrackGroupArray trackGroupArray) {
this.trackGroupArray = trackGroupArray;
}
public void release() {
preparedPeriod = false;
}
@Override
public void prepare(Callback callback) {
assertFalse(preparedPeriod);
preparedPeriod = true;
callback.onPrepared(this);
}
@Override
public void maybeThrowPrepareError() throws IOException {
assertTrue(preparedPeriod);
}
@Override
public TrackGroupArray getTrackGroups() {
assertTrue(preparedPeriod);
return trackGroupArray;
}
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
assertTrue(preparedPeriod);
int rendererCount = selections.length;
for (int i = 0; i < rendererCount; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
streams[i] = null;
}
}
for (int i = 0; i < rendererCount; i++) {
if (streams[i] == null && selections[i] != null) {
TrackSelection selection = selections[i];
assertEquals(1, selection.length());
assertEquals(0, selection.getIndexInTrackGroup(0));
TrackGroup trackGroup = selection.getTrackGroup();
assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET);
streams[i] = new FakeSampleStream(trackGroup.getFormat(0));
streamResetFlags[i] = true;
}
}
return 0;
}
@Override
public void discardBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() {
assertTrue(preparedPeriod);
return C.TIME_UNSET;
}
@Override
public long getBufferedPositionUs() {
assertTrue(preparedPeriod);
return C.TIME_END_OF_SOURCE;
}
@Override
public long seekToUs(long positionUs) {
assertTrue(preparedPeriod);
assertEquals(0, positionUs);
return positionUs;
}
@Override
public long getNextLoadPositionUs() {
assertTrue(preparedPeriod);
return C.TIME_END_OF_SOURCE;
}
@Override
public boolean continueLoading(long positionUs) {
assertTrue(preparedPeriod);
return false;
}
}
/**
* Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag
* on its input buffer.
*/
private static final class FakeSampleStream implements SampleStream {
private final Format format;
private boolean readFormat;
public FakeSampleStream(Format format) {
this.format = format;
}
@Override
public boolean isReady() {
return true;
}
@Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
boolean formatRequired) {
if (formatRequired || !readFormat) {
formatHolder.format = format;
readFormat = true;
return C.RESULT_FORMAT_READ;
} else {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
}
}
@Override
public void maybeThrowError() throws IOException {
// Do nothing.
}
@Override
public void skipData(long positionUs) {
// Do nothing.
}
}
/**
* Fake {@link Renderer} that supports any format with the matching MIME type. The renderer
* verifies that it reads a given {@link Format}.
*/
private static class FakeRenderer extends BaseRenderer {
private final Format expectedFormat;
public int positionResetCount;
public int formatReadCount;
public int bufferReadCount;
public boolean isEnded;
public FakeRenderer(Format expectedFormat) {
super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN
: MimeTypes.getTrackType(expectedFormat.sampleMimeType));
this.expectedFormat = expectedFormat;
}
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
positionResetCount++;
isEnded = false;
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (isEnded) {
return;
}
// Verify the format matches the expected format.
FormatHolder formatHolder = new FormatHolder();
DecoderInputBuffer buffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
int result = readSource(formatHolder, buffer, false);
if (result == C.RESULT_FORMAT_READ) {
formatReadCount++;
assertEquals(expectedFormat, formatHolder.format);
} else if (result == C.RESULT_BUFFER_READ) {
bufferReadCount++;
if (buffer.isEndOfStream()) {
isEnded = true;
}
}
}
@Override
public boolean isReady() {
return isSourceReady();
}
@Override
public boolean isEnded() {
return isEnded;
}
@Override
public int supportsFormat(Format format) throws ExoPlaybackException {
return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED
: FORMAT_UNSUPPORTED_TYPE;
}
}
private abstract static class FakeMediaClockRenderer extends FakeRenderer implements MediaClock {
public FakeMediaClockRenderer(Format expectedFormat) {
super(expectedFormat);
}
@Override
public MediaClock getMediaClock() {
return this;
}
} }
} }

View File

@ -53,9 +53,9 @@ public final class FormatTest extends TestCase {
} }
public void testParcelable() { public void testParcelable() {
DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4, DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4,
TestUtil.buildTestData(128, 1 /* data seed */)); TestUtil.buildTestData(128, 1 /* data seed */));
DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM, DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM,
TestUtil.buildTestData(128, 1 /* data seed */)); TestUtil.buildTestData(128, 1 /* data seed */));
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
byte[] projectionData = new byte[] {1, 2, 3}; byte[] projectionData = new byte[] {1, 2, 3};

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import junit.framework.TestCase;
/**
* Unit test for {@link Timeline}.
*/
public class TimelineTest extends TestCase {
public void testEmptyTimeline() {
TimelineAsserts.assertEmpty(Timeline.EMPTY);
}
public void testSinglePeriodTimeline() {
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111));
TimelineAsserts.assertWindowIds(timeline, 111);
TimelineAsserts.assertPeriodCounts(timeline, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
}
public void testMultiPeriodTimeline() {
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111));
TimelineAsserts.assertWindowIds(timeline, 111);
TimelineAsserts.assertPeriodCounts(timeline, 5);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
}
}

View File

@ -31,16 +31,16 @@ import junit.framework.TestCase;
*/ */
public class DrmInitDataTest extends TestCase { public class DrmInitDataTest extends TestCase {
private static final SchemeData DATA_1 = private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); TestUtil.buildTestData(128, 1 /* data seed */));
private static final SchemeData DATA_2 = private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */)); TestUtil.buildTestData(128, 2 /* data seed */));
private static final SchemeData DATA_1B = private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); TestUtil.buildTestData(128, 1 /* data seed */));
private static final SchemeData DATA_2B = private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */)); TestUtil.buildTestData(128, 2 /* data seed */));
private static final SchemeData DATA_UNIVERSAL = private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, null, VIDEO_MP4,
new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */)); TestUtil.buildTestData(128, 3 /* data seed */));
public void testParcelable() { public void testParcelable() {
DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2); DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2);

View File

@ -154,7 +154,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
} }
private static DrmInitData newDrmInitData() { private static DrmInitData newDrmInitData() {
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "cenc", "mimeType",
new byte[] {1, 4, 7, 0, 3, 6})); new byte[] {1, 4, 7, 0, 3, 6}));
} }

View File

@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.flv;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Unit test for {@link FlvExtractor}. * Unit test for {@link FlvExtractor}.
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class FlvExtractorTest extends InstrumentationTestCase { public final class FlvExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new FlvExtractor(); return new FlvExtractor();

View File

@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mkv;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Tests for {@link MatroskaExtractor}. * Tests for {@link MatroskaExtractor}.
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class MatroskaExtractorTest extends InstrumentationTestCase { public final class MatroskaExtractorTest extends InstrumentationTestCase {
public void testMkvSample() throws Exception { public void testMkvSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new MatroskaExtractor(); return new MatroskaExtractor();
@ -34,7 +35,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
} }
public void testWebmSubsampleEncryption() throws Exception { public void testWebmSubsampleEncryption() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new MatroskaExtractor(); return new MatroskaExtractor();
@ -43,7 +44,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
} }
public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception { public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new MatroskaExtractor(); return new MatroskaExtractor();

View File

@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mp3;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Unit test for {@link Mp3Extractor}. * Unit test for {@link Mp3Extractor}.
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class Mp3ExtractorTest extends InstrumentationTestCase { public final class Mp3ExtractorTest extends InstrumentationTestCase {
public void testMp3Sample() throws Exception { public void testMp3Sample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new Mp3Extractor(); return new Mp3Extractor();
@ -34,7 +35,7 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase {
} }
public void testTrimmedMp3Sample() throws Exception { public void testTrimmedMp3Sample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new Mp3Extractor(); return new Mp3Extractor();

View File

@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Unit test for {@link FragmentedMp4Extractor}. * Unit test for {@link FragmentedMp4Extractor}.
@ -26,26 +27,28 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation()); ExtractorAsserts
.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
} }
public void testSampleWithSeiPayloadParsing() throws Exception { public void testSampleWithSeiPayloadParsing() throws Exception {
// Enabling the CEA-608 track enables SEI payload parsing. // Enabling the CEA-608 track enables SEI payload parsing.
TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), ExtractorAsserts.assertOutput(
getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
"mp4/sample_fragmented_sei.mp4", getInstrumentation()); "mp4/sample_fragmented_sei.mp4", getInstrumentation());
} }
public void testAtomWithZeroSize() throws Exception { public void testAtomWithZeroSize() throws Exception {
TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4", ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
getInstrumentation(), ParserException.class); getInstrumentation(), ParserException.class);
} }
private static TestUtil.ExtractorFactory getExtractorFactory() { private static ExtractorFactory getExtractorFactory() {
return getExtractorFactory(0); return getExtractorFactory(0);
} }
private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) { private static ExtractorFactory getExtractorFactory(final int flags) {
return new TestUtil.ExtractorFactory() { return new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new FragmentedMp4Extractor(flags, null); return new FragmentedMp4Extractor(flags, null);

View File

@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Tests for {@link Mp4Extractor}. * Tests for {@link Mp4Extractor}.
@ -27,7 +28,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class Mp4ExtractorTest extends InstrumentationTestCase { public final class Mp4ExtractorTest extends InstrumentationTestCase {
public void testMp4Sample() throws Exception { public void testMp4Sample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new Mp4Extractor(); return new Mp4Extractor();

View File

@ -17,9 +17,10 @@ package com.google.android.exoplayer2.extractor.ogg;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.testutil.TestUtil.ExtractorFactory;
import java.io.IOException; import java.io.IOException;
/** /**
@ -35,20 +36,21 @@ public final class OggExtractorTest extends InstrumentationTestCase {
}; };
public void testOpus() throws Exception { public void testOpus() throws Exception {
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation());
} }
public void testFlac() throws Exception { public void testFlac() throws Exception {
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation()); ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation());
} }
public void testFlacNoSeektable() throws Exception { public void testFlacNoSeektable() throws Exception {
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg",
getInstrumentation()); getInstrumentation());
} }
public void testVorbis() throws Exception { public void testVorbis() throws Exception {
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", getInstrumentation()); ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg",
getInstrumentation());
} }
public void testSniffVorbis() throws Exception { public void testSniffVorbis() throws Exception {

View File

@ -19,7 +19,8 @@ import android.annotation.TargetApi;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
/** /**
@ -29,8 +30,8 @@ import com.google.android.exoplayer2.util.MimeTypes;
public final class RawCcExtractorTest extends InstrumentationTestCase { public final class RawCcExtractorTest extends InstrumentationTestCase {
public void testRawCcSample() throws Exception { public void testRawCcSample() throws Exception {
TestUtil.assertOutput( ExtractorAsserts.assertOutput(
new TestUtil.ExtractorFactory() { new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new RawCcExtractor( return new RawCcExtractor(

View File

@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Unit test for {@link Ac3Extractor}. * Unit test for {@link Ac3Extractor}.
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class Ac3ExtractorTest extends InstrumentationTestCase { public final class Ac3ExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new Ac3Extractor(); return new Ac3Extractor();

View File

@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Unit test for {@link AdtsExtractor}. * Unit test for {@link AdtsExtractor}.
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class AdtsExtractorTest extends InstrumentationTestCase { public final class AdtsExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new AdtsExtractor(); return new AdtsExtractor();

View File

@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Unit test for {@link PsExtractor}. * Unit test for {@link PsExtractor}.
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class PsExtractorTest extends InstrumentationTestCase { public final class PsExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new PsExtractor(); return new PsExtractor();

View File

@ -25,6 +25,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput;
@ -43,7 +45,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new TsExtractor(); return new TsExtractor();
@ -65,7 +67,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1); writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
fileData = out.toByteArray(); fileData = out.toByteArray();
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new TsExtractor(); return new TsExtractor();
@ -75,7 +77,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomPesReader() throws Exception { public void testCustomPesReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0),
factory); factory);
FakeExtractorInput input = new FakeExtractorInput.Builder() FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
@ -100,7 +102,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomInitialSectionReader() throws Exception { public void testCustomInitialSectionReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0),
factory); factory);
FakeExtractorInput input = new FakeExtractorInput.Builder() FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts")) .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))

View File

@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.wav;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Unit test for {@link WavExtractor}. * Unit test for {@link WavExtractor}.
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class WavExtractorTest extends InstrumentationTestCase { public final class WavExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new WavExtractor(); return new WavExtractor();

View File

@ -15,20 +15,17 @@
*/ */
package com.google.android.exoplayer2.source; package com.google.android.exoplayer2.source;
import static org.mockito.Mockito.doAnswer;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import org.mockito.Mock; import com.google.android.exoplayer2.testutil.TimelineAsserts;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/** /**
* Unit tests for {@link ClippingMediaSource}. * Unit tests for {@link ClippingMediaSource}.
@ -38,15 +35,11 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
private static final long TEST_PERIOD_DURATION_US = 1000000; private static final long TEST_PERIOD_DURATION_US = 1000000;
private static final long TEST_CLIP_AMOUNT_US = 300000; private static final long TEST_CLIP_AMOUNT_US = 300000;
@Mock
private MediaSource mockMediaSource;
private Timeline clippedTimeline;
private Window window; private Window window;
private Period period; private Period period;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
TestUtil.setUpMockito(this);
window = new Timeline.Window(); window = new Timeline.Window();
period = new Timeline.Period(); period = new Timeline.Period();
} }
@ -109,35 +102,30 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
clippedTimeline.getPeriod(0, period).getDurationUs()); clippedTimeline.getPeriod(0, period).getDurationUs());
} }
public void testWindowAndPeriodIndices() {
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US));
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
TimelineAsserts.assertWindowIds(clippedTimeline, 111);
TimelineAsserts.assertPeriodCounts(clippedTimeline, 1);
TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF,
C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0);
TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF,
C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0);
}
/** /**
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/ */
private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
mockMediaSourceSourceWithTimeline(timeline); MediaSource mediaSource = new FakeMediaSource(timeline, null);
new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true, return TestUtil.extractTimelineFromMediaSource(
new Listener() { new ClippingMediaSource(mediaSource, startMs, endMs));
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
clippedTimeline = timeline;
}
});
return clippedTimeline;
}
/**
* Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info.
*/
private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) {
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2];
listener.onSourceInfoRefreshed(timeline, null);
return null;
}
}).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(),
Mockito.any(MediaSource.Listener.class));
return mockMediaSource;
} }
} }

View File

@ -0,0 +1,117 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import junit.framework.TestCase;
/**
* Unit tests for {@link ConcatenatingMediaSource}.
*/
public final class ConcatenatingMediaSourceTest extends TestCase {
public void testSingleMediaSource() {
Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111));
TimelineAsserts.assertWindowIds(timeline, 111);
TimelineAsserts.assertPeriodCounts(timeline, 3);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111));
TimelineAsserts.assertWindowIds(timeline, 111);
TimelineAsserts.assertPeriodCounts(timeline, 3);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
}
public void testMultipleMediaSources() {
Timeline[] timelines = { createFakeTimeline(3, 111), createFakeTimeline(1, 222),
createFakeTimeline(3, 333) };
Timeline timeline = getConcatenatedTimeline(false, timelines);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET,
0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
timeline = getConcatenatedTimeline(true, timelines);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
}
public void testNestedMediaSources() {
Timeline timeline = getConcatenatedTimeline(false,
getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)),
getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444)));
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444);
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
C.INDEX_UNSET, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 3, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
1, 2, 3, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 3, 0);
}
/**
* Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns
* the concatenated timeline.
*/
private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic,
Timeline... timelines) {
MediaSource[] mediaSources = new MediaSource[timelines.length];
for (int i = 0; i < timelines.length; i++) {
mediaSources[i] = new FakeMediaSource(timelines[i], null);
}
return TestUtil.extractTimelineFromMediaSource(
new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources));
}
private static FakeTimeline createFakeTimeline(int periodCount, int windowId) {
return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId));
}
}

View File

@ -0,0 +1,533 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource.Listener;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
import java.util.Arrays;
import junit.framework.TestCase;
/**
* Unit tests for {@link DynamicConcatenatingMediaSource}
*/
public final class DynamicConcatenatingMediaSourceTest extends TestCase {
private static final int TIMEOUT_MS = 10000;
private Timeline timeline;
private boolean timelineUpdated;
public void testPlaylistChangesAfterPreparation() throws InterruptedException {
timeline = null;
FakeMediaSource[] childSources = createMediaSources(7);
DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
prepareAndListenToTimelineUpdates(mediaSource);
waitForTimelineUpdate();
TimelineAsserts.assertEmpty(timeline);
// Add first source.
mediaSource.addMediaSource(childSources[0]);
waitForTimelineUpdate();
assertNotNull(timeline);
TimelineAsserts.assertPeriodCounts(timeline, 1);
TimelineAsserts.assertWindowIds(timeline, 111);
// Add at front of queue.
mediaSource.addMediaSource(0, childSources[1]);
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 2, 1);
TimelineAsserts.assertWindowIds(timeline, 222, 111);
// Add at back of queue.
mediaSource.addMediaSource(childSources[2]);
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
// Add in the middle.
mediaSource.addMediaSource(1, childSources[3]);
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333);
// Add bulk.
mediaSource.addMediaSources(3, Arrays.asList((MediaSource) childSources[4],
(MediaSource) childSources[5], (MediaSource) childSources[6]));
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
// Remove in the middle.
mediaSource.removeMediaSource(3);
waitForTimelineUpdate();
mediaSource.removeMediaSource(3);
waitForTimelineUpdate();
mediaSource.removeMediaSource(3);
waitForTimelineUpdate();
mediaSource.removeMediaSource(1);
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
for (int i = 3; i <= 6; i++) {
childSources[i].assertReleased();
}
// Assert correct next and previous indices behavior after some insertions and removals.
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
// Remove at front of queue.
mediaSource.removeMediaSource(0);
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 111, 333);
childSources[1].assertReleased();
// Remove at back of queue.
mediaSource.removeMediaSource(1);
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 1);
TimelineAsserts.assertWindowIds(timeline, 111);
childSources[2].assertReleased();
// Remove last source.
mediaSource.removeMediaSource(0);
waitForTimelineUpdate();
TimelineAsserts.assertEmpty(timeline);
childSources[3].assertReleased();
}
public void testPlaylistChangesBeforePreparation() throws InterruptedException {
timeline = null;
FakeMediaSource[] childSources = createMediaSources(4);
DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
mediaSource.addMediaSource(childSources[0]);
mediaSource.addMediaSource(childSources[1]);
mediaSource.addMediaSource(0, childSources[2]);
mediaSource.removeMediaSource(1);
mediaSource.addMediaSource(1, childSources[3]);
assertNull(timeline);
prepareAndListenToTimelineUpdates(mediaSource);
waitForTimelineUpdate();
assertNotNull(timeline);
TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2);
TimelineAsserts.assertWindowIds(timeline, 333, 444, 222);
mediaSource.releaseSource();
for (int i = 1; i < 4; i++) {
childSources[i].assertReleased();
}
}
public void testPlaylistWithLazyMediaSource() throws InterruptedException {
timeline = null;
FakeMediaSource[] childSources = createMediaSources(2);
LazyMediaSource[] lazySources = new LazyMediaSource[4];
for (int i = 0; i < 4; i++) {
lazySources[i] = new LazyMediaSource();
}
//Add lazy sources before preparation
DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
mediaSource.addMediaSource(lazySources[0]);
mediaSource.addMediaSource(0, childSources[0]);
mediaSource.removeMediaSource(1);
mediaSource.addMediaSource(1, lazySources[1]);
assertNull(timeline);
prepareAndListenToTimelineUpdates(mediaSource);
waitForTimelineUpdate();
assertNotNull(timeline);
TimelineAsserts.assertPeriodCounts(timeline, 1, 1);
TimelineAsserts.assertWindowIds(timeline, 111, null);
TimelineAsserts.assertWindowIsDynamic(timeline, false, true);
lazySources[1].triggerTimelineUpdate(createFakeTimeline(8));
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 1, 9);
TimelineAsserts.assertWindowIds(timeline, 111, 999);
TimelineAsserts.assertWindowIsDynamic(timeline, false, false);
//Add lazy sources after preparation
mediaSource.addMediaSource(1, lazySources[2]);
waitForTimelineUpdate();
mediaSource.addMediaSource(2, childSources[1]);
waitForTimelineUpdate();
mediaSource.addMediaSource(0, lazySources[3]);
waitForTimelineUpdate();
mediaSource.removeMediaSource(2);
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9);
TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999);
TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false);
lazySources[3].triggerTimelineUpdate(createFakeTimeline(7));
waitForTimelineUpdate();
TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9);
TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999);
TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false);
mediaSource.releaseSource();
childSources[0].assertReleased();
childSources[1].assertReleased();
}
public void testIllegalArguments() {
DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null);
// Null sources.
try {
mediaSource.addMediaSource(null);
fail("Null mediaSource not allowed.");
} catch (NullPointerException e) {
// Expected.
}
MediaSource[] mediaSources = { validSource, null };
try {
mediaSource.addMediaSources(Arrays.asList(mediaSources));
fail("Null mediaSource not allowed.");
} catch (NullPointerException e) {
// Expected.
}
// Duplicate sources.
mediaSource.addMediaSource(validSource);
try {
mediaSource.addMediaSource(validSource);
fail("Duplicate mediaSource not allowed.");
} catch (IllegalArgumentException e) {
// Expected.
}
mediaSources = new MediaSource[] {
new FakeMediaSource(createFakeTimeline(2), null), validSource };
try {
mediaSource.addMediaSources(Arrays.asList(mediaSources));
fail("Duplicate mediaSource not allowed.");
} catch (IllegalArgumentException e) {
// Expected.
}
}
private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) {
mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
timeline = newTimeline;
synchronized (DynamicConcatenatingMediaSourceTest.this) {
timelineUpdated = true;
DynamicConcatenatingMediaSourceTest.this.notify();
}
}
});
}
private synchronized void waitForTimelineUpdate() throws InterruptedException {
long timeoutMs = System.currentTimeMillis() + TIMEOUT_MS;
while (!timelineUpdated) {
wait(TIMEOUT_MS);
if (System.currentTimeMillis() >= timeoutMs) {
fail("No timeline update occurred within timeout.");
}
}
timelineUpdated = false;
}
private static FakeMediaSource[] createMediaSources(int count) {
FakeMediaSource[] sources = new FakeMediaSource[count];
for (int i = 0; i < count; i++) {
sources[i] = new FakeMediaSource(createFakeTimeline(i), null);
}
return sources;
}
private static FakeTimeline createFakeTimeline(int index) {
return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111));
}
private static class LazyMediaSource implements MediaSource {
private Listener listener;
public void triggerTimelineUpdate(Timeline timeline) {
listener.onSourceInfoRefreshed(timeline, null);
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
this.listener = listener;
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
return null;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
}
@Override
public void releaseSource() {
}
}
/**
* Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread.
*/
private static class StubExoPlayer implements ExoPlayer, Handler.Callback {
private final Handler handler;
public StubExoPlayer() {
HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread");
handlerThread.start();
handler = new Handler(handlerThread.getLooper(), this);
}
@Override
public Looper getPlaybackLooper() {
throw new UnsupportedOperationException();
}
@Override
public void addListener(EventListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void removeListener(EventListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int getPlaybackState() {
throw new UnsupportedOperationException();
}
@Override
public void prepare(MediaSource mediaSource) {
throw new UnsupportedOperationException();
}
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
throw new UnsupportedOperationException();
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
throw new UnsupportedOperationException();
}
@Override
public boolean getPlayWhenReady() {
throw new UnsupportedOperationException();
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
throw new UnsupportedOperationException();
}
@Override
public int getRepeatMode() {
throw new UnsupportedOperationException();
}
@Override
public boolean isLoading() {
throw new UnsupportedOperationException();
}
@Override
public void seekToDefaultPosition() {
throw new UnsupportedOperationException();
}
@Override
public void seekToDefaultPosition(int windowIndex) {
throw new UnsupportedOperationException();
}
@Override
public void seekTo(long positionMs) {
throw new UnsupportedOperationException();
}
@Override
public void seekTo(int windowIndex, long positionMs) {
throw new UnsupportedOperationException();
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
throw new UnsupportedOperationException();
}
@Override
public PlaybackParameters getPlaybackParameters() {
throw new UnsupportedOperationException();
}
@Override
public void stop() {
throw new UnsupportedOperationException();
}
@Override
public void release() {
throw new UnsupportedOperationException();
}
@Override
public void sendMessages(ExoPlayerMessage... messages) {
handler.obtainMessage(0, messages).sendToTarget();
}
@Override
public void blockingSendMessages(ExoPlayerMessage... messages) {
throw new UnsupportedOperationException();
}
@Override
public int getRendererCount() {
throw new UnsupportedOperationException();
}
@Override
public int getRendererType(int index) {
throw new UnsupportedOperationException();
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
throw new UnsupportedOperationException();
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
throw new UnsupportedOperationException();
}
@Override
public Object getCurrentManifest() {
throw new UnsupportedOperationException();
}
@Override
public Timeline getCurrentTimeline() {
throw new UnsupportedOperationException();
}
@Override
public int getCurrentPeriodIndex() {
throw new UnsupportedOperationException();
}
@Override
public int getCurrentWindowIndex() {
throw new UnsupportedOperationException();
}
@Override
public long getDuration() {
throw new UnsupportedOperationException();
}
@Override
public long getCurrentPosition() {
throw new UnsupportedOperationException();
}
@Override
public long getBufferedPosition() {
throw new UnsupportedOperationException();
}
@Override
public int getBufferedPercentage() {
throw new UnsupportedOperationException();
}
@Override
public boolean isCurrentWindowDynamic() {
throw new UnsupportedOperationException();
}
@Override
public boolean isCurrentWindowSeekable() {
throw new UnsupportedOperationException();
}
@Override
public boolean isPlayingAd() {
throw new UnsupportedOperationException();
}
@Override
public int getCurrentAdGroupIndex() {
throw new UnsupportedOperationException();
}
@Override
public int getCurrentAdIndexInAdGroup() {
throw new UnsupportedOperationException();
}
@Override
public boolean handleMessage(Message msg) {
ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj;
for (ExoPlayerMessage message : messages) {
try {
message.target.handleMessage(message.messageType, message.message);
} catch (ExoPlaybackException e) {
fail("Unexpected ExoPlaybackException.");
}
}
return true;
}
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import junit.framework.TestCase;
/**
* Unit tests for {@link LoopingMediaSource}.
*/
public class LoopingMediaSourceTest extends TestCase {
private final Timeline multiWindowTimeline;
public LoopingMediaSourceTest() {
multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource(
new FakeTimeline(new TimelineWindowDefinition(1, 111),
new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null));
}
public void testSingleLoop() {
Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
}
public void testMultiLoop() {
Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE,
0, 1, 2, 3, 4, 5, 6, 7, 8);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL,
8, 0, 1, 2, 3, 4, 5, 6, 7);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE,
0, 1, 2, 3, 4, 5, 6, 7, 8);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL,
1, 2, 3, 4, 5, 6, 7, 8, 0);
}
public void testInfiniteLoop() {
Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
}
/**
* Wraps the specified timeline in a {@link LoopingMediaSource} and returns
* the looping timeline.
*/
private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) {
MediaSource mediaSource = new FakeMediaSource(timeline, null);
return TestUtil.extractTimelineFromMediaSource(
new LoopingMediaSource(mediaSource, loopCount));
}
}

View File

@ -0,0 +1,688 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.Arrays;
import junit.framework.TestCase;
/**
* Test for {@link SampleQueue}.
*/
public class SampleQueueTest extends TestCase {
private static final int ALLOCATION_SIZE = 16;
private static final Format TEST_FORMAT_1 = Format.createSampleFormat("1", "mimeType", 0);
private static final Format TEST_FORMAT_2 = Format.createSampleFormat("2", "mimeType", 0);
private static final Format TEST_FORMAT_1_COPY = Format.createSampleFormat("1", "mimeType", 0);
private static final byte[] TEST_DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10);
/*
* TEST_SAMPLE_SIZES and TEST_SAMPLE_OFFSETS are intended to test various boundary cases (with
* respect to the allocation size). TEST_SAMPLE_OFFSETS values are defined as the backward offsets
* (as expected by SampleQueue.sampleMetadata) assuming that TEST_DATA has been written to the
* sampleQueue in full. The allocations are filled as follows, where | indicates a boundary
* between allocations and x indicates a byte that doesn't belong to a sample:
*
* x<s1>|x<s2>x|x<s3>|<s4>x|<s5>|<s6|s6>|x<s7|s7>x|<s8>
*/
private static final int[] TEST_SAMPLE_SIZES = new int[] {
ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 2, ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 1,
ALLOCATION_SIZE, ALLOCATION_SIZE * 2, ALLOCATION_SIZE * 2 - 2, ALLOCATION_SIZE
};
private static final int[] TEST_SAMPLE_OFFSETS = new int[] {
ALLOCATION_SIZE * 9, ALLOCATION_SIZE * 8 + 1, ALLOCATION_SIZE * 7, ALLOCATION_SIZE * 6 + 1,
ALLOCATION_SIZE * 5, ALLOCATION_SIZE * 3, ALLOCATION_SIZE + 1, 0
};
private static final long[] TEST_SAMPLE_TIMESTAMPS = new long[] {
0, 1000, 2000, 3000, 4000, 5000, 6000, 7000
};
private static final long LAST_SAMPLE_TIMESTAMP =
TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1];
private static final int[] TEST_SAMPLE_FLAGS = new int[] {
C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0, C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0
};
private static final Format[] TEST_SAMPLE_FORMATS = new Format[] {
TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_2, TEST_FORMAT_2,
TEST_FORMAT_2, TEST_FORMAT_2
};
private static final int TEST_DATA_SECOND_KEYFRAME_INDEX = 4;
private Allocator allocator;
private SampleQueue sampleQueue;
private FormatHolder formatHolder;
private DecoderInputBuffer inputBuffer;
@Override
public void setUp() throws Exception {
super.setUp();
allocator = new DefaultAllocator(false, ALLOCATION_SIZE);
sampleQueue = new SampleQueue(allocator);
formatHolder = new FormatHolder();
inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
}
@Override
public void tearDown() throws Exception {
super.tearDown();
allocator = null;
sampleQueue = null;
formatHolder = null;
inputBuffer = null;
}
public void testResetReleasesAllocations() {
writeTestData();
assertAllocationCount(10);
sampleQueue.reset();
assertAllocationCount(0);
}
public void testReadWithoutWrite() {
assertNoSamplesToRead(null);
}
public void testReadFormatDeduplicated() {
sampleQueue.format(TEST_FORMAT_1);
assertReadFormat(false, TEST_FORMAT_1);
// If the same format is input then it should be de-duplicated (i.e. not output again).
sampleQueue.format(TEST_FORMAT_1);
assertNoSamplesToRead(TEST_FORMAT_1);
// The same applies for a format that's equal (but a different object).
sampleQueue.format(TEST_FORMAT_1_COPY);
assertNoSamplesToRead(TEST_FORMAT_1);
}
public void testReadSingleSamples() {
sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
assertAllocationCount(1);
// Nothing to read.
assertNoSamplesToRead(null);
sampleQueue.format(TEST_FORMAT_1);
// Read the format.
assertReadFormat(false, TEST_FORMAT_1);
// Nothing to read.
assertNoSamplesToRead(TEST_FORMAT_1);
sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
// If formatRequired, should read the format rather than the sample.
assertReadFormat(true, TEST_FORMAT_1);
// Otherwise should read the sample.
assertSampleRead(1000, true, TEST_DATA, 0, ALLOCATION_SIZE);
// Allocation should still be held.
assertAllocationCount(1);
sampleQueue.discardToRead();
// The allocation should have been released.
assertAllocationCount(0);
// Nothing to read.
assertNoSamplesToRead(TEST_FORMAT_1);
// Write a second sample followed by one byte that does not belong to it.
sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
sampleQueue.sampleMetadata(2000, 0, ALLOCATION_SIZE - 1, 1, null);
// If formatRequired, should read the format rather than the sample.
assertReadFormat(true, TEST_FORMAT_1);
// Read the sample.
assertSampleRead(2000, false, TEST_DATA, 0, ALLOCATION_SIZE - 1);
// Allocation should still be held.
assertAllocationCount(1);
sampleQueue.discardToRead();
// The last byte written to the sample queue may belong to a sample whose metadata has yet to be
// written, so an allocation should still be held.
assertAllocationCount(1);
// Write metadata for a third sample containing the remaining byte.
sampleQueue.sampleMetadata(3000, 0, 1, 0, null);
// If formatRequired, should read the format rather than the sample.
assertReadFormat(true, TEST_FORMAT_1);
// Read the sample.
assertSampleRead(3000, false, TEST_DATA, ALLOCATION_SIZE - 1, 1);
// Allocation should still be held.
assertAllocationCount(1);
sampleQueue.discardToRead();
// The allocation should have been released.
assertAllocationCount(0);
}
public void testReadMultiSamples() {
writeTestData();
assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
assertAllocationCount(10);
assertReadTestData();
assertAllocationCount(10);
sampleQueue.discardToRead();
assertAllocationCount(0);
}
public void testReadMultiSamplesTwice() {
writeTestData();
writeTestData();
assertAllocationCount(20);
assertReadTestData(TEST_FORMAT_2);
assertReadTestData(TEST_FORMAT_2);
assertAllocationCount(20);
sampleQueue.discardToRead();
assertAllocationCount(0);
}
public void testReadMultiWithRewind() {
writeTestData();
assertReadTestData();
assertEquals(8, sampleQueue.getReadIndex());
assertAllocationCount(10);
// Rewind.
sampleQueue.rewind();
assertAllocationCount(10);
// Read again.
assertEquals(0, sampleQueue.getReadIndex());
assertReadTestData();
}
public void testRewindAfterDiscard() {
writeTestData();
assertReadTestData();
sampleQueue.discardToRead();
assertAllocationCount(0);
// Rewind.
sampleQueue.rewind();
assertAllocationCount(0);
// Can't read again.
assertEquals(8, sampleQueue.getReadIndex());
assertReadEndOfStream(false);
}
public void testAdvanceToEnd() {
writeTestData();
sampleQueue.advanceToEnd();
assertAllocationCount(10);
sampleQueue.discardToRead();
assertAllocationCount(0);
// Despite skipping all samples, we should still read the last format, since this is the
// expected format for a subsequent sample.
assertReadFormat(false, TEST_FORMAT_2);
// Once the format has been read, there's nothing else to read.
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testAdvanceToEndRetainsUnassignedData() {
sampleQueue.format(TEST_FORMAT_1);
sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
sampleQueue.advanceToEnd();
assertAllocationCount(1);
sampleQueue.discardToRead();
// Skipping shouldn't discard data that may belong to a sample whose metadata has yet to be
// written.
assertAllocationCount(1);
// We should be able to read the format.
assertReadFormat(false, TEST_FORMAT_1);
// Once the format has been read, there's nothing else to read.
assertNoSamplesToRead(TEST_FORMAT_1);
sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
// Once the metadata has been written, check the sample can be read as expected.
assertSampleRead(0, true, TEST_DATA, 0, ALLOCATION_SIZE);
assertNoSamplesToRead(TEST_FORMAT_1);
assertAllocationCount(1);
sampleQueue.discardToRead();
assertAllocationCount(0);
}
public void testAdvanceToBeforeBuffer() {
writeTestData();
boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false);
// Should fail and have no effect.
assertFalse(result);
assertReadTestData();
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testAdvanceToStartOfBuffer() {
writeTestData();
boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false);
// Should succeed but have no effect (we're already at the first frame).
assertTrue(result);
assertReadTestData();
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testAdvanceToEndOfBuffer() {
writeTestData();
boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
// Should succeed and skip to 2nd keyframe.
assertTrue(result);
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testAdvanceToAfterBuffer() {
writeTestData();
boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false);
// Should fail and have no effect.
assertFalse(result);
assertReadTestData();
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testAdvanceToAfterBufferAllowed() {
writeTestData();
boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
// Should succeed and skip to 2nd keyframe.
assertTrue(result);
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testDiscardToEnd() {
writeTestData();
// Should discard everything.
sampleQueue.discardToEnd();
assertEquals(8, sampleQueue.getReadIndex());
assertAllocationCount(0);
// We should still be able to read the upstream format.
assertReadFormat(false, TEST_FORMAT_2);
// We should be able to write and read subsequent samples.
writeTestData();
assertReadTestData(TEST_FORMAT_2);
}
public void testDiscardToStopAtReadPosition() {
writeTestData();
// Shouldn't discard anything.
sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);
assertEquals(0, sampleQueue.getReadIndex());
assertAllocationCount(10);
// Read the first sample.
assertReadTestData(null, 0, 1);
// Shouldn't discard anything.
sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, true);
assertEquals(1, sampleQueue.getReadIndex());
assertAllocationCount(10);
// Should discard the read sample.
sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, true);
assertAllocationCount(9);
// Shouldn't discard anything.
sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);
assertAllocationCount(9);
// Should be able to read the remaining samples.
assertReadTestData(TEST_FORMAT_1, 1, 7);
assertEquals(8, sampleQueue.getReadIndex());
// Should discard up to the second last sample
sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP - 1, false, true);
assertAllocationCount(3);
// Should discard up the last sample
sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);
assertAllocationCount(1);
}
public void testDiscardToDontStopAtReadPosition() {
writeTestData();
// Shouldn't discard anything.
sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, false);
assertEquals(0, sampleQueue.getReadIndex());
assertAllocationCount(10);
// Should discard the first sample.
sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, false);
assertEquals(1, sampleQueue.getReadIndex());
assertAllocationCount(9);
// Should be able to read the remaining samples.
assertReadTestData(TEST_FORMAT_1, 1, 7);
}
public void testDiscardUpstream() {
writeTestData();
sampleQueue.discardUpstreamSamples(8);
assertAllocationCount(10);
sampleQueue.discardUpstreamSamples(7);
assertAllocationCount(9);
sampleQueue.discardUpstreamSamples(6);
assertAllocationCount(7);
sampleQueue.discardUpstreamSamples(5);
assertAllocationCount(5);
sampleQueue.discardUpstreamSamples(4);
assertAllocationCount(4);
sampleQueue.discardUpstreamSamples(3);
assertAllocationCount(3);
sampleQueue.discardUpstreamSamples(2);
assertAllocationCount(2);
sampleQueue.discardUpstreamSamples(1);
assertAllocationCount(1);
sampleQueue.discardUpstreamSamples(0);
assertAllocationCount(0);
assertReadFormat(false, TEST_FORMAT_2);
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testDiscardUpstreamMulti() {
writeTestData();
sampleQueue.discardUpstreamSamples(4);
assertAllocationCount(4);
sampleQueue.discardUpstreamSamples(0);
assertAllocationCount(0);
assertReadFormat(false, TEST_FORMAT_2);
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testDiscardUpstreamBeforeRead() {
writeTestData();
sampleQueue.discardUpstreamSamples(4);
assertAllocationCount(4);
assertReadTestData(null, 0, 4);
assertReadFormat(false, TEST_FORMAT_2);
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testDiscardUpstreamAfterRead() {
writeTestData();
assertReadTestData(null, 0, 3);
sampleQueue.discardUpstreamSamples(8);
assertAllocationCount(10);
sampleQueue.discardToRead();
assertAllocationCount(7);
sampleQueue.discardUpstreamSamples(7);
assertAllocationCount(6);
sampleQueue.discardUpstreamSamples(6);
assertAllocationCount(4);
sampleQueue.discardUpstreamSamples(5);
assertAllocationCount(2);
sampleQueue.discardUpstreamSamples(4);
assertAllocationCount(1);
sampleQueue.discardUpstreamSamples(3);
assertAllocationCount(0);
assertReadFormat(false, TEST_FORMAT_2);
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testLargestQueuedTimestampWithDiscardUpstream() {
writeTestData();
assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 1);
// Discarding from upstream should reduce the largest timestamp.
assertEquals(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 2],
sampleQueue.getLargestQueuedTimestampUs());
sampleQueue.discardUpstreamSamples(0);
// Discarding everything from upstream without reading should unset the largest timestamp.
assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs());
}
public void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() {
long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000};
writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, decodeOrderTimestamps,
TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS);
assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs());
sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 2);
// Discarding the last two samples should not change the largest timestamp, due to the decode
// ordering of the timestamps.
assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs());
sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 3);
// Once a third sample is discarded, the largest timestamp should have changed.
assertEquals(4000, sampleQueue.getLargestQueuedTimestampUs());
sampleQueue.discardUpstreamSamples(0);
// Discarding everything from upstream without reading should unset the largest timestamp.
assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs());
}
public void testLargestQueuedTimestampWithRead() {
writeTestData();
assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
assertReadTestData();
// Reading everything should not reduce the largest timestamp.
assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
}
// Internal methods.
/**
* Writes standard test data to {@code sampleQueue}.
*/
@SuppressWarnings("ReferenceEquality")
private void writeTestData() {
writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, TEST_SAMPLE_TIMESTAMPS,
TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS);
}
/**
* Writes the specified test data to {@code sampleQueue}.
*
*
*/
@SuppressWarnings("ReferenceEquality")
private void writeTestData(byte[] data, int[] sampleSizes, int[] sampleOffsets,
long[] sampleTimestamps, Format[] sampleFormats, int[] sampleFlags) {
sampleQueue.sampleData(new ParsableByteArray(data), data.length);
Format format = null;
for (int i = 0; i < sampleTimestamps.length; i++) {
if (sampleFormats[i] != format) {
sampleQueue.format(sampleFormats[i]);
format = sampleFormats[i];
}
sampleQueue.sampleMetadata(sampleTimestamps[i], sampleFlags[i], sampleSizes[i],
sampleOffsets[i], null);
}
}
/**
* Asserts correct reading of standard test data from {@code sampleQueue}.
*/
private void assertReadTestData() {
assertReadTestData(null, 0);
}
/**
* Asserts correct reading of standard test data from {@code sampleQueue}.
*
* @param startFormat The format of the last sample previously read from {@code sampleQueue}.
*/
private void assertReadTestData(Format startFormat) {
assertReadTestData(startFormat, 0);
}
/**
* Asserts correct reading of standard test data from {@code sampleQueue}.
*
* @param startFormat The format of the last sample previously read from {@code sampleQueue}.
* @param firstSampleIndex The index of the first sample that's expected to be read.
*/
private void assertReadTestData(Format startFormat, int firstSampleIndex) {
assertReadTestData(startFormat, firstSampleIndex,
TEST_SAMPLE_TIMESTAMPS.length - firstSampleIndex);
}
/**
* Asserts correct reading of standard test data from {@code sampleQueue}.
*
* @param startFormat The format of the last sample previously read from {@code sampleQueue}.
* @param firstSampleIndex The index of the first sample that's expected to be read.
* @param sampleCount The number of samples to read.
*/
private void assertReadTestData(Format startFormat, int firstSampleIndex, int sampleCount) {
Format format = startFormat;
for (int i = firstSampleIndex; i < firstSampleIndex + sampleCount; i++) {
// Use equals() on the read side despite using referential equality on the write side, since
// sampleQueue de-duplicates written formats using equals().
if (!TEST_SAMPLE_FORMATS[i].equals(format)) {
// If the format has changed, we should read it.
assertReadFormat(false, TEST_SAMPLE_FORMATS[i]);
format = TEST_SAMPLE_FORMATS[i];
}
// If we require the format, we should always read it.
assertReadFormat(true, TEST_SAMPLE_FORMATS[i]);
// Assert the sample is as expected.
assertSampleRead(TEST_SAMPLE_TIMESTAMPS[i],
(TEST_SAMPLE_FLAGS[i] & C.BUFFER_FLAG_KEY_FRAME) != 0,
TEST_DATA,
TEST_DATA.length - TEST_SAMPLE_OFFSETS[i] - TEST_SAMPLE_SIZES[i],
TEST_SAMPLE_SIZES[i]);
}
}
/**
* Asserts {@link SampleQueue#read} is behaving correctly, given there are no samples to read and
* the last format to be written to the sample queue is {@code endFormat}.
*
* @param endFormat The last format to be written to the sample queue, or null of no format has
* been written.
*/
private void assertNoSamplesToRead(Format endFormat) {
// If not formatRequired or loadingFinished, should read nothing.
assertReadNothing(false);
// If formatRequired, should read the end format if set, else read nothing.
if (endFormat == null) {
assertReadNothing(true);
} else {
assertReadFormat(true, endFormat);
}
// If loadingFinished, should read end of stream.
assertReadEndOfStream(false);
assertReadEndOfStream(true);
// Having read end of stream should not affect other cases.
assertReadNothing(false);
if (endFormat == null) {
assertReadNothing(true);
} else {
assertReadFormat(true, endFormat);
}
}
/**
* Asserts {@link SampleQueue#read} returns {@link C#RESULT_NOTHING_READ}.
*
* @param formatRequired The value of {@code formatRequired} passed to readData.
*/
private void assertReadNothing(boolean formatRequired) {
clearFormatHolderAndInputBuffer();
int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0);
assertEquals(C.RESULT_NOTHING_READ, result);
// formatHolder should not be populated.
assertNull(formatHolder.format);
// inputBuffer should not be populated.
assertInputBufferContainsNoSampleData();
assertInputBufferHasNoDefaultFlagsSet();
}
/**
* Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the
* {@link DecoderInputBuffer#isEndOfStream()} is set.
*
* @param formatRequired The value of {@code formatRequired} passed to readData.
*/
private void assertReadEndOfStream(boolean formatRequired) {
clearFormatHolderAndInputBuffer();
int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, true, 0);
assertEquals(C.RESULT_BUFFER_READ, result);
// formatHolder should not be populated.
assertNull(formatHolder.format);
// inputBuffer should not contain sample data, but end of stream flag should be set.
assertInputBufferContainsNoSampleData();
assertTrue(inputBuffer.isEndOfStream());
assertFalse(inputBuffer.isDecodeOnly());
assertFalse(inputBuffer.isEncrypted());
}
/**
* Asserts {@link SampleQueue#read} returns {@link C#RESULT_FORMAT_READ} and that the format
* holder is filled with a {@link Format} that equals {@code format}.
*
* @param formatRequired The value of {@code formatRequired} passed to readData.
* @param format The expected format.
*/
private void assertReadFormat(boolean formatRequired, Format format) {
clearFormatHolderAndInputBuffer();
int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0);
assertEquals(C.RESULT_FORMAT_READ, result);
// formatHolder should be populated.
assertEquals(format, formatHolder.format);
// inputBuffer should not be populated.
assertInputBufferContainsNoSampleData();
assertInputBufferHasNoDefaultFlagsSet();
}
/**
* Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the buffer is
* filled with the specified sample data.
*
* @param timeUs The expected buffer timestamp.
* @param isKeyframe The expected keyframe flag.
* @param sampleData An array containing the expected sample data.
* @param offset The offset in {@code sampleData} of the expected sample data.
* @param length The length of the expected sample data.
*/
private void assertSampleRead(long timeUs, boolean isKeyframe, byte[] sampleData, int offset,
int length) {
clearFormatHolderAndInputBuffer();
int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0);
assertEquals(C.RESULT_BUFFER_READ, result);
// formatHolder should not be populated.
assertNull(formatHolder.format);
// inputBuffer should be populated.
assertEquals(timeUs, inputBuffer.timeUs);
assertEquals(isKeyframe, inputBuffer.isKeyFrame());
assertFalse(inputBuffer.isDecodeOnly());
assertFalse(inputBuffer.isEncrypted());
inputBuffer.flip();
assertEquals(length, inputBuffer.data.limit());
byte[] readData = new byte[length];
inputBuffer.data.get(readData);
MoreAsserts.assertEquals(Arrays.copyOfRange(sampleData, offset, offset + length), readData);
}
/**
* Asserts the number of allocations currently in use by {@code sampleQueue}.
*
* @param count The expected number of allocations.
*/
private void assertAllocationCount(int count) {
assertEquals(ALLOCATION_SIZE * count, allocator.getTotalBytesAllocated());
}
/**
* Asserts {@code inputBuffer} does not contain any sample data.
*/
private void assertInputBufferContainsNoSampleData() {
if (inputBuffer.data == null) {
return;
}
inputBuffer.flip();
assertEquals(0, inputBuffer.data.limit());
}
private void assertInputBufferHasNoDefaultFlagsSet() {
assertFalse(inputBuffer.isEndOfStream());
assertFalse(inputBuffer.isDecodeOnly());
assertFalse(inputBuffer.isEncrypted());
}
private void clearFormatHolderAndInputBuffer() {
formatHolder.format = null;
inputBuffer.clear();
}
}

View File

@ -157,39 +157,43 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
assertEquals(2, output.size()); assertEquals(2, output.size());
Cue ttmlCue = output.get(0); Cue ttmlCue = output.get(0);
assertEquals("lorem", ttmlCue.text.toString()); assertEquals("lorem", ttmlCue.text.toString());
assertEquals(10.f / 100.f, ttmlCue.position); assertEquals(10f / 100f, ttmlCue.position);
assertEquals(10.f / 100.f, ttmlCue.line); assertEquals(10f / 100f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size); assertEquals(20f / 100f, ttmlCue.size);
ttmlCue = output.get(1); ttmlCue = output.get(1);
assertEquals("amet", ttmlCue.text.toString()); assertEquals("amet", ttmlCue.text.toString());
assertEquals(60.f / 100.f, ttmlCue.position); assertEquals(60f / 100f, ttmlCue.position);
assertEquals(10.f / 100.f, ttmlCue.line); assertEquals(10f / 100f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size); assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(5000000); output = subtitle.getCues(5000000);
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("ipsum", ttmlCue.text.toString()); assertEquals("ipsum", ttmlCue.text.toString());
assertEquals(40.f / 100.f, ttmlCue.position); assertEquals(40f / 100f, ttmlCue.position);
assertEquals(40.f / 100.f, ttmlCue.line); assertEquals(40f / 100f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size); assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(9000000); output = subtitle.getCues(9000000);
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("dolor", ttmlCue.text.toString()); assertEquals("dolor", ttmlCue.text.toString());
assertEquals(10.f / 100.f, ttmlCue.position); assertEquals(Cue.DIMEN_UNSET, ttmlCue.position);
assertEquals(80.f / 100.f, ttmlCue.line); assertEquals(Cue.DIMEN_UNSET, ttmlCue.line);
assertEquals(Cue.DIMEN_UNSET, ttmlCue.size); assertEquals(Cue.DIMEN_UNSET, ttmlCue.size);
// TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed.
// assertEquals(10f / 100f, ttmlCue.position);
// assertEquals(80f / 100f, ttmlCue.line);
// assertEquals(1f, ttmlCue.size);
output = subtitle.getCues(21000000); output = subtitle.getCues(21000000);
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("She first said this", ttmlCue.text.toString()); assertEquals("She first said this", ttmlCue.text.toString());
assertEquals(45.f / 100.f, ttmlCue.position); assertEquals(45f / 100f, ttmlCue.position);
assertEquals(45.f / 100.f, ttmlCue.line); assertEquals(45f / 100f, ttmlCue.line);
assertEquals(35.f / 100.f, ttmlCue.size); assertEquals(35f / 100f, ttmlCue.size);
output = subtitle.getCues(25000000); output = subtitle.getCues(25000000);
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("She first said this\nThen this", ttmlCue.text.toString()); assertEquals("She first said this\nThen this", ttmlCue.text.toString());
@ -197,8 +201,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString()); assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString());
assertEquals(45.f / 100.f, ttmlCue.position); assertEquals(45f / 100f, ttmlCue.position);
assertEquals(45.f / 100.f, ttmlCue.line); assertEquals(45f / 100f, ttmlCue.line);
} }
public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException { public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException {

View File

@ -125,25 +125,25 @@ public final class CssParserTest extends InstrumentationTestCase {
String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n"; String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n";
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput)); ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput));
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
assertEquals(CssParser.parseNextToken(input, builder), "lorem"); assertEquals("lorem", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), ":"); assertEquals(":", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "ipsum"); assertEquals("ipsum", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "{"); assertEquals("{", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "dolor"); assertEquals("dolor", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "}"); assertEquals("}", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "#sit"); assertEquals("#sit", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), ","); assertEquals(",", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "amet"); assertEquals("amet", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), ";"); assertEquals(";", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "lorem"); assertEquals("lorem", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), ":"); assertEquals(":", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "ipsum"); assertEquals("ipsum", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "dolor"); assertEquals("dolor", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "("); assertEquals("(", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), "("); assertEquals("(", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), ")"); assertEquals(")", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), ")"); assertEquals(")", CssParser.parseNextToken(input, builder));
assertEquals(CssParser.parseNextToken(input, builder), null); assertEquals(null, CssParser.parseNextToken(input, builder));
} }
public void testStyleScoreSystem() { public void testStyleScoreSystem() {

View File

@ -0,0 +1,196 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.trackselection;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.util.MimeTypes;
import junit.framework.TestCase;
/**
* Unit tests for {@link MappingTrackSelector}.
*/
public final class MappingTrackSelectorTest extends TestCase {
private static final RendererCapabilities VIDEO_CAPABILITIES =
new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO);
private static final RendererCapabilities AUDIO_CAPABILITIES =
new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO);
private static final RendererCapabilities[] RENDERER_CAPABILITIES = new RendererCapabilities[] {
VIDEO_CAPABILITIES, AUDIO_CAPABILITIES
};
private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup(
Format.createVideoSampleFormat("video", MimeTypes.VIDEO_H264, null, Format.NO_VALUE,
Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, null));
private static final TrackGroup AUDIO_TRACK_GROUP = new TrackGroup(
Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,
Format.NO_VALUE, 2, 44100, null, null, 0, null));
private static final TrackGroupArray TRACK_GROUPS = new TrackGroupArray(
VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP);
private static final TrackSelection[] TRACK_SELECTIONS = new TrackSelection[] {
new FixedTrackSelection(VIDEO_TRACK_GROUP, 0),
new FixedTrackSelection(AUDIO_TRACK_GROUP, 0)
};
/**
* Tests that the video and audio track groups are mapped onto the correct renderers.
*/
public void testMapping() throws ExoPlaybackException {
FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector();
trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS);
trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP);
trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP);
}
/**
* Tests that the video and audio track groups are mapped onto the correct renderers when the
* renderer ordering is reversed.
*/
public void testMappingReverseOrder() throws ExoPlaybackException {
FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector();
RendererCapabilities[] reverseOrderRendererCapabilities = new RendererCapabilities[] {
AUDIO_CAPABILITIES, VIDEO_CAPABILITIES};
trackSelector.selectTracks(reverseOrderRendererCapabilities, TRACK_GROUPS);
trackSelector.assertMappedTrackGroups(0, AUDIO_TRACK_GROUP);
trackSelector.assertMappedTrackGroups(1, VIDEO_TRACK_GROUP);
}
/**
* Tests video and audio track groups are mapped onto the correct renderers when there are
* multiple track groups of the same type.
*/
public void testMappingMulti() throws ExoPlaybackException {
FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector();
TrackGroupArray multiTrackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP,
VIDEO_TRACK_GROUP);
trackSelector.selectTracks(RENDERER_CAPABILITIES, multiTrackGroups);
trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP, VIDEO_TRACK_GROUP);
trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP);
}
/**
* Tests the result of {@link MappingTrackSelector#selectTracks(RendererCapabilities[],
* TrackGroupArray[], int[][][])} is propagated correctly to the result of
* {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)}.
*/
public void testSelectTracks() throws ExoPlaybackException {
FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS);
TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS);
assertEquals(TRACK_SELECTIONS[0], result.selections.get(0));
assertEquals(TRACK_SELECTIONS[1], result.selections.get(1));
}
/**
* Tests that a null override clears a track selection.
*/
public void testSelectTracksWithNullOverride() throws ExoPlaybackException {
FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS);
trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null);
TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS);
assertNull(result.selections.get(0));
assertEquals(TRACK_SELECTIONS[1], result.selections.get(1));
}
/**
* Tests that a null override can be cleared.
*/
public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException {
FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS);
trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null);
trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP));
TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS);
assertEquals(TRACK_SELECTIONS[0], result.selections.get(0));
assertEquals(TRACK_SELECTIONS[1], result.selections.get(1));
}
/**
* Tests that an override is not applied for a different set of available track groups.
*/
public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException {
FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS);
trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null);
TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES,
new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP));
assertEquals(TRACK_SELECTIONS[0], result.selections.get(0));
assertEquals(TRACK_SELECTIONS[1], result.selections.get(1));
}
/**
* A {@link MappingTrackSelector} that returns a fixed result from
* {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])}.
*/
private static final class FakeMappingTrackSelector extends MappingTrackSelector {
private final TrackSelection[] result;
private TrackGroupArray[] lastRendererTrackGroupArrays;
public FakeMappingTrackSelector(TrackSelection... result) {
this.result = result.length == 0 ? null : result;
}
@Override
protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities,
TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports)
throws ExoPlaybackException {
lastRendererTrackGroupArrays = rendererTrackGroupArrays;
return result == null ? new TrackSelection[rendererCapabilities.length] : result;
}
public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) {
assertEquals(expected.length, lastRendererTrackGroupArrays[rendererIndex].length);
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], lastRendererTrackGroupArrays[rendererIndex].get(i));
}
}
}
/**
* A {@link RendererCapabilities} that advertises adaptive support for all tracks of a given type.
*/
private static final class FakeRendererCapabilities implements RendererCapabilities {
private final int trackType;
public FakeRendererCapabilities(int trackType) {
this.trackType = trackType;
}
@Override
public int getTrackType() {
return trackType;
}
@Override
public int supportsFormat(Format format) throws ExoPlaybackException {
return MimeTypes.getTrackType(format.sampleMimeType) == trackType
? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE;
}
@Override
public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
return ADAPTIVE_SEAMLESS;
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream;
import android.net.Uri;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
/**
* Unit tests for {@link AssetDataSource}.
*/
public final class AssetDataSourceTest extends InstrumentationTestCase {
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
public void testReadFileUri() throws Exception {
AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext());
DataSpec dataSpec = new DataSpec(Uri.parse("file:///android_asset/" + DATA_PATH));
TestUtil.assertDataSourceContent(dataSource, dataSpec,
TestUtil.getByteArray(getInstrumentation(), DATA_PATH));
}
public void testReadAssetUri() throws Exception {
AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext());
DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + DATA_PATH));
TestUtil.assertDataSourceContent(dataSource, dataSpec,
TestUtil.getByteArray(getInstrumentation(), DATA_PATH));
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* Unit tests for {@link ContentDataSource}.
*/
public final class ContentDataSourceTest extends InstrumentationTestCase {
private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
public void testReadValidUri() throws Exception {
ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext());
Uri contentUri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY)
.path(DATA_PATH).build();
DataSpec dataSpec = new DataSpec(contentUri);
TestUtil.assertDataSourceContent(dataSource, dataSpec,
TestUtil.getByteArray(getInstrumentation(), DATA_PATH));
}
public void testReadInvalidUri() throws Exception {
ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext());
Uri contentUri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY)
.build();
DataSpec dataSpec = new DataSpec(contentUri);
try {
dataSource.open(dataSpec);
fail();
} catch (ContentDataSource.ContentDataSourceException e) {
// Expected.
assertTrue(e.getCause() instanceof FileNotFoundException);
} finally {
dataSource.close();
}
}
/**
* A {@link ContentProvider} for the test.
*/
public static final class TestContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
throw new UnsupportedOperationException();
}
@Override
public AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
if (uri.getPath() == null) {
return null;
}
try {
return getContext().getAssets().openFd(uri.getPath().replaceFirst("/", ""));
} catch (IOException e) {
FileNotFoundException exception = new FileNotFoundException(e.getMessage());
exception.initCause(e);
throw exception;
}
}
@Override
public String getType(@NonNull Uri uri) {
throw new UnsupportedOperationException();
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
throw new UnsupportedOperationException();
}
@Override
public int delete(@NonNull Uri uri, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException();
}
@Override
public int update(@NonNull Uri uri, ContentValues values,
String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
}
}

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
import android.net.Uri; import android.net.Uri;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import android.test.MoreAsserts; import android.test.MoreAsserts;
@ -38,27 +40,29 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private static final String KEY_1 = "key 1"; private static final String KEY_1 = "key 1";
private static final String KEY_2 = "key 2"; private static final String KEY_2 = "key 2";
private File cacheDir; private File tempFolder;
private SimpleCache simpleCache; private SimpleCache cache;
@Override @Override
protected void setUp() throws Exception { public void setUp() throws Exception {
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); super.setUp();
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
} }
@Override @Override
protected void tearDown() throws Exception { public void tearDown() throws Exception {
Util.recursiveDelete(cacheDir); Util.recursiveDelete(tempFolder);
super.tearDown();
} }
public void testMaxCacheFileSize() throws Exception { public void testMaxCacheFileSize() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false); CacheDataSource cacheDataSource = createCacheDataSource(false, false);
assertReadDataContentLength(cacheDataSource, false, false); assertReadDataContentLength(cacheDataSource, false, false);
File[] files = cacheDir.listFiles(); for (String key : cache.getKeys()) {
for (File file : files) { for (CacheSpan cacheSpan : cache.getCachedSpans(key)) {
if (!file.getName().equals(CachedContentIndex.FILE_NAME)) { assertTrue(cacheSpan.length <= MAX_CACHE_FILE_SIZE);
assertTrue(file.length() <= MAX_CACHE_FILE_SIZE); assertTrue(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE);
} }
} }
} }
@ -104,7 +108,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
// Read partial at EOS but don't cross it so length is unknown // Read partial at EOS but don't cross it so length is unknown
CacheDataSource cacheDataSource = createCacheDataSource(false, true); CacheDataSource cacheDataSource = createCacheDataSource(false, true);
assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2); assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2);
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1)); assertEquals(C.LENGTH_UNSET, cache.getContentLength(KEY_1));
// Now do an unbounded request for whole data. This will cause a bounded request from upstream. // Now do an unbounded request for whole data. This will cause a bounded request from upstream.
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong. // End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
@ -124,13 +128,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
CacheDataSource cacheDataSource = createCacheDataSource(false, true, CacheDataSource cacheDataSource = createCacheDataSource(false, true,
CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS); CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET); assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET);
MoreAsserts.assertEmpty(simpleCache.getKeys()); MoreAsserts.assertEmpty(cache.getKeys());
} }
public void testReadOnlyCache() throws Exception { public void testReadOnlyCache() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
assertReadDataContentLength(cacheDataSource, false, false); assertReadDataContentLength(cacheDataSource, false, false);
assertEquals(0, cacheDir.list().length); assertCacheEmpty(cache);
} }
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
@ -155,30 +159,30 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
assertReadData(cacheDataSource, unknownLength, 0, length); assertReadData(cacheDataSource, unknownLength, 0, length);
assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache " assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache "
+ "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length, + "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length,
simpleCache.getContentLength(KEY_1)); cache.getContentLength(KEY_1));
} }
private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position, private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position,
int length) throws IOException { int length) throws IOException {
int actualLength = TEST_DATA.length - position; int testDataLength = TEST_DATA.length - position;
if (length != C.LENGTH_UNSET) { if (length != C.LENGTH_UNSET) {
actualLength = Math.min(actualLength, length); testDataLength = Math.min(testDataLength, length);
} }
assertEquals(unknownLength ? length : actualLength, assertEquals(unknownLength ? length : testDataLength,
cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1))); cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1)));
byte[] buffer = new byte[100]; byte[] buffer = new byte[100];
int index = 0; int totalBytesRead = 0;
while (true) { while (true) {
int read = cacheDataSource.read(buffer, index, buffer.length - index); int read = cacheDataSource.read(buffer, totalBytesRead, buffer.length - totalBytesRead);
if (read == C.RESULT_END_OF_INPUT) { if (read == C.RESULT_END_OF_INPUT) {
break; break;
} }
index += read; totalBytesRead += read;
} }
assertEquals(actualLength, index); assertEquals(testDataLength, totalBytesRead);
MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + actualLength), MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + testDataLength),
Arrays.copyOf(buffer, index)); Arrays.copyOf(buffer, totalBytesRead));
cacheDataSource.close(); cacheDataSource.close();
} }
@ -192,7 +196,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private CacheDataSource createCacheDataSource(boolean setReadException, private CacheDataSource createCacheDataSource(boolean setReadException,
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
return createCacheDataSource(setReadException, simulateUnknownLength, flags, return createCacheDataSource(setReadException, simulateUnknownLength, flags,
new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE)); new CacheDataSink(cache, MAX_CACHE_FILE_SIZE));
} }
private CacheDataSource createCacheDataSource(boolean setReadException, private CacheDataSource createCacheDataSource(boolean setReadException,
@ -204,7 +208,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
if (setReadException) { if (setReadException) {
fakeData.appendReadError(new IOException("Shouldn't read from upstream")); fakeData.appendReadError(new IOException("Shouldn't read from upstream"));
} }
return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink, return new CacheDataSource(cache, upstream, new FileDataSource(), cacheWriteDataSink,
flags, null); flags, null);
} }

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