Merge branch 'dev-v2' into rtmp_client
This commit is contained in:
commit
0ecbe5dc04
38
README.md
38
README.md
@ -20,6 +20,11 @@ and extend, and can be updated through Play Store application updates.
|
||||
|
||||
## Using ExoPlayer ##
|
||||
|
||||
ExoPlayer modules can be obtained via jCenter. It's also possible to clone the
|
||||
repository and depend on the modules locally.
|
||||
|
||||
### Via jCenter ###
|
||||
|
||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||
dependency. You need to make sure you have the jcenter repository included in
|
||||
the `build.gradle` file in the root of your project:
|
||||
@ -64,6 +69,39 @@ latest versions, see the [Release notes][].
|
||||
[Bintray]: https://bintray.com/google/exoplayer
|
||||
[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||
|
||||
### Locally ###
|
||||
|
||||
Cloning the repository and depending on the modules locally is required when
|
||||
using some ExoPlayer extension modules. It's also a suitable approach if you
|
||||
want to make local changes to ExoPlayer, or if you want to use a development
|
||||
branch.
|
||||
|
||||
First, clone the repository into a local directory and checkout the desired
|
||||
branch:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
git checkout release-v2
|
||||
```
|
||||
|
||||
Next, add the following to your project's `settings.gradle` file, replacing
|
||||
`path/to/exoplayer` with the path to your local copy:
|
||||
|
||||
```gradle
|
||||
gradle.ext.exoplayerRoot = 'path/to/exoplayer'
|
||||
gradle.ext.exoplayerModulePrefix = 'exoplayer-'
|
||||
apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')
|
||||
```
|
||||
|
||||
You should now see the ExoPlayer modules appear as part of your project. You can
|
||||
depend on them as you would on any other local module, for example:
|
||||
|
||||
```gradle
|
||||
compile project(':exoplayer-library-core')
|
||||
compile project(':exoplayer-library-dash')
|
||||
compile project(':exoplayer-library-ui)
|
||||
```
|
||||
|
||||
## Developing ExoPlayer ##
|
||||
|
||||
#### Project branches ####
|
||||
|
@ -1,5 +1,60 @@
|
||||
# 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 ###
|
||||
|
||||
* New modular library structure. You can read more about depending on individual
|
||||
|
26
build.gradle
26
build.gradle
@ -16,7 +16,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
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'
|
||||
}
|
||||
// Workaround for the following test coverage issue. Remove when fixed:
|
||||
@ -33,23 +33,7 @@ allprojects {
|
||||
jcenter()
|
||||
}
|
||||
project.ext {
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 9 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality
|
||||
// provided by the library requires API level 16 or greater.
|
||||
minSdkVersion = 9
|
||||
compileSdkVersion = 25
|
||||
targetSdkVersion = 25
|
||||
buildToolsVersion = '25'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '25.3.1'
|
||||
dexmakerVersion = '1.2'
|
||||
mockitoVersion = '1.9.5'
|
||||
releaseRepoName = getBintrayRepo()
|
||||
releaseUserOrg = 'google'
|
||||
releaseGroupId = 'com.google.android.exoplayer'
|
||||
releaseVersion = 'r2.4.0'
|
||||
releaseWebsite = 'https://github.com/google/ExoPlayer'
|
||||
exoplayerPublishEnabled = true
|
||||
}
|
||||
if (it.hasProperty('externalBuildDir')) {
|
||||
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'
|
||||
|
32
constants.gradle
Normal file
32
constants.gradle
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (C) 2017 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 9 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality provided
|
||||
// by the library requires API level 16 or greater.
|
||||
minSdkVersion = 9
|
||||
compileSdkVersion = 25
|
||||
targetSdkVersion = 25
|
||||
buildToolsVersion = '25'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '25.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
54
core_settings.gradle
Normal 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')
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
@ -45,13 +46,14 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-core')
|
||||
compile project(':library-dash')
|
||||
compile project(':library-hls')
|
||||
compile project(':library-smoothstreaming')
|
||||
compile project(':library-ui')
|
||||
withExtensionsCompile project(path: ':extension-ffmpeg')
|
||||
withExtensionsCompile project(path: ':extension-flac')
|
||||
withExtensionsCompile project(path: ':extension-opus')
|
||||
withExtensionsCompile project(path: ':extension-vp9')
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'library-smoothstreaming')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-flac')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-ima')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-opus')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-vp9')
|
||||
}
|
||||
|
@ -16,8 +16,8 @@
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2400"
|
||||
android:versionName="2.4.0">
|
||||
android:versionCode="2403"
|
||||
android:versionName="2.4.3">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
@ -138,28 +138,76 @@
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (MP4,H264)",
|
||||
"name": "WV: Secure SD & HD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (MP4,H264)",
|
||||
"name": "WV: Secure SD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (MP4,H264)",
|
||||
"name": "WV: Secure HD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (MP4,H264)",
|
||||
"name": "WV: Secure UHD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -341,7 +389,7 @@
|
||||
"samples": [
|
||||
{
|
||||
"name": "Dizzy",
|
||||
"uri": "http://html5demos.com/assets/dizzy.mp4"
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Apple AAC 10s",
|
||||
@ -353,7 +401,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Android screens (Matroska)",
|
||||
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
},
|
||||
{
|
||||
"name": "Big Buck Bunny (MP4 Video)",
|
||||
@ -377,7 +425,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Google Play (MP3 Audio)",
|
||||
"uri": "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (Ogg/Vorbis Audio)",
|
||||
@ -408,10 +456,10 @@
|
||||
"name": "Cats -> Dogs",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "http://html5demos.com/assets/dizzy.mp4"
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -422,7 +470,7 @@
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||
@ -435,13 +483,13 @@
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "http://html5demos.com/assets/dizzy.mp4"
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||
},
|
||||
{
|
||||
"uri": "http://html5demos.com/assets/dizzy.mp4"
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||
@ -452,5 +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="
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -95,6 +95,11 @@ import java.util.Locale;
|
||||
+ getStateString(state) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(@ExoPlayer.RepeatMode int repeatMode) {
|
||||
Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
Log.d(TAG, "positionDiscontinuity");
|
||||
@ -276,7 +281,7 @@ import java.util.Locale;
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio) {
|
||||
// Do nothing.
|
||||
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -461,4 +466,16 @@ import java.util.Locale;
|
||||
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 "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
@ -26,7 +27,9 @@ import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@ -69,6 +72,7 @@ import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
@ -92,6 +96,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||
public static final String URI_LIST_EXTRA = "uri_list";
|
||||
public static final String EXTENSION_LIST_EXTRA = "extension_list";
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
|
||||
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
||||
@ -200,10 +205,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// Show the controls on any key event.
|
||||
simpleExoPlayerView.showController();
|
||||
// If the event was not handled then see if the player view can handle it as a media key event.
|
||||
return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event);
|
||||
// If the event was not handled then see if the player view can handle it.
|
||||
return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
// OnClickListener methods
|
||||
@ -234,7 +237,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
Intent intent = getIntent();
|
||||
boolean needNewPlayer = player == null;
|
||||
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.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : 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 =
|
||||
((DemoApplication) getApplication()).useExtensionRenderers()
|
||||
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
@ -261,16 +271,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
|
||||
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.addListener(this);
|
||||
|
||||
eventLogger = new EventLogger(trackSelector);
|
||||
player.addListener(eventLogger);
|
||||
player.setAudioDebugListener(eventLogger);
|
||||
player.setVideoDebugListener(eventLogger);
|
||||
@ -312,6 +314,26 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
}
|
||||
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
|
||||
: new ConcatenatingMediaSource(mediaSources);
|
||||
String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
|
||||
if (adTagUriString != null) {
|
||||
Uri adTagUri = Uri.parse(adTagUriString);
|
||||
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;
|
||||
if (haveResumePosition) {
|
||||
player.seekTo(resumeWindow, resumePosition);
|
||||
@ -424,6 +446,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
updateButtonVisibilities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
if (needRetrySource) {
|
||||
|
@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity {
|
||||
String[] drmKeyRequestProperties = null;
|
||||
boolean preferExtensionDecoders = false;
|
||||
ArrayList<UriSample> playlistSamples = null;
|
||||
String adTagUri = null;
|
||||
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
@ -233,6 +234,9 @@ public class SampleChooserActivity extends Activity {
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
case "ad_tag_uri":
|
||||
adTagUri = reader.nextString();
|
||||
break;
|
||||
default:
|
||||
throw new ParserException("Unsupported attribute name: " + name);
|
||||
}
|
||||
@ -246,7 +250,7 @@ public class SampleChooserActivity extends Activity {
|
||||
preferExtensionDecoders, playlistSamplesArray);
|
||||
} else {
|
||||
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
|
||||
preferExtensionDecoders, uri, extension);
|
||||
preferExtensionDecoders, uri, extension, adTagUri);
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,13 +406,15 @@ public class SampleChooserActivity extends Activity {
|
||||
|
||||
public final String uri;
|
||||
public final String extension;
|
||||
public final String adTagUri;
|
||||
|
||||
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
|
||||
String extension) {
|
||||
String extension, String adTagUri) {
|
||||
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.adTagUri = adTagUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -416,6 +422,7 @@ public class SampleChooserActivity extends Activity {
|
||||
return super.buildIntent(context)
|
||||
.setData(Uri.parse(uri))
|
||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
||||
.setAction(PlayerActivity.ACTION_VIEW);
|
||||
}
|
||||
|
||||
|
@ -58,4 +58,6 @@
|
||||
|
||||
<string name="sample_list_load_error">One or more sample lists failed to load</string>
|
||||
|
||||
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
||||
|
||||
</resources>
|
||||
|
@ -11,13 +11,10 @@ The Cronet Extension is an [HttpDataSource][] implementation using [Cronet][].
|
||||
|
||||
## Build Instructions ##
|
||||
|
||||
* Checkout ExoPlayer along with Extensions:
|
||||
|
||||
```
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
```
|
||||
|
||||
* Get the Cronet libraries:
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][]. In addition, it's necessary to get the Cronet libraries
|
||||
and enable the extension:
|
||||
|
||||
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
|
||||
directory
|
||||
@ -27,6 +24,12 @@ git clone https://github.com/google/ExoPlayer.git
|
||||
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
|
||||
directory of this extension
|
||||
|
||||
* In ExoPlayer's `settings.gradle` file, uncomment the Cronet extension
|
||||
* In your `settings.gradle` file, add the following line before the line that
|
||||
applies `core_settings.gradle`:
|
||||
|
||||
```gradle
|
||||
gradle.ext.exoplayerIncludeCronetExtension = true;
|
||||
```
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
|
||||
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
@ -29,11 +30,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-core')
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile files('libs/cronet_api.jar')
|
||||
compile files('libs/cronet_impl_common_java.jar')
|
||||
compile files('libs/cronet_impl_native_java.jar')
|
||||
androidTestCompile project(':library')
|
||||
androidTestCompile project(modulePrefix + 'library')
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
|
@ -28,7 +28,6 @@
|
||||
|
||||
<instrumentation
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="com.google.android.exoplayer.ext.cronet"
|
||||
tools:replace="android:targetPackage"/>
|
||||
android:targetPackage="com.google.android.exoplayer.ext.cronet"/>
|
||||
|
||||
</manifest>
|
||||
|
@ -16,9 +16,11 @@
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
import java.util.concurrent.Executor;
|
||||
@ -34,43 +36,143 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
*/
|
||||
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS =
|
||||
CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
|
||||
|
||||
/**
|
||||
* The default read timeout, in milliseconds.
|
||||
*/
|
||||
public static final int DEFAULT_READ_TIMEOUT_MILLIS =
|
||||
CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
|
||||
|
||||
private final CronetEngine cronetEngine;
|
||||
private final CronetEngineWrapper cronetEngineWrapper;
|
||||
private final Executor executor;
|
||||
private final Predicate<String> contentTypePredicate;
|
||||
private final TransferListener<? super DataSource> transferListener;
|
||||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
private final HttpDataSource.Factory fallbackFactory;
|
||||
|
||||
public CronetDataSourceFactory(CronetEngine cronetEngine,
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* <p>
|
||||
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
*
|
||||
* Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
|
||||
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||
* {@link CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
|
||||
* no suitable CronetEngine can be build.
|
||||
*/
|
||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor, Predicate<String> contentTypePredicate,
|
||||
TransferListener<? super DataSource> transferListener) {
|
||||
this(cronetEngine, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false);
|
||||
TransferListener<? super DataSource> transferListener,
|
||||
HttpDataSource.Factory fallbackFactory) {
|
||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
|
||||
}
|
||||
|
||||
public CronetDataSourceFactory(CronetEngine cronetEngine,
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* <p>
|
||||
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
|
||||
* {@link DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
|
||||
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||
* {@link CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||
*/
|
||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor, Predicate<String> contentTypePredicate,
|
||||
TransferListener<? super DataSource> transferListener, String userAgent) {
|
||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
|
||||
new DefaultHttpDataSourceFactory(userAgent, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* <p>
|
||||
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
|
||||
* {@link DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||
* {@link CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||
*/
|
||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor, Predicate<String> contentTypePredicate,
|
||||
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
|
||||
int readTimeoutMs, boolean resetTimeoutOnRedirects) {
|
||||
this.cronetEngine = cronetEngine;
|
||||
int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) {
|
||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
|
||||
new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
|
||||
readTimeoutMs, resetTimeoutOnRedirects));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* <p>
|
||||
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||
* {@link CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
|
||||
* no suitable CronetEngine can be build.
|
||||
*/
|
||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor, Predicate<String> contentTypePredicate,
|
||||
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
|
||||
int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||
HttpDataSource.Factory fallbackFactory) {
|
||||
this.cronetEngineWrapper = cronetEngineWrapper;
|
||||
this.executor = executor;
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
this.transferListener = transferListener;
|
||||
this.connectTimeoutMs = connectTimeoutMs;
|
||||
this.readTimeoutMs = readTimeoutMs;
|
||||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||
this.fallbackFactory = fallbackFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties
|
||||
protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties
|
||||
defaultRequestProperties) {
|
||||
CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();
|
||||
if (cronetEngine == null) {
|
||||
return fallbackFactory.createDataSource();
|
||||
}
|
||||
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
|
||||
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
|
||||
}
|
||||
|
@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.util.Log;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import org.chromium.net.CronetEngine;
|
||||
import org.chromium.net.CronetProvider;
|
||||
|
||||
/**
|
||||
* A wrapper class for a {@link CronetEngine}.
|
||||
*/
|
||||
public final class CronetEngineWrapper {
|
||||
|
||||
private static final String TAG = "CronetEngineWrapper";
|
||||
|
||||
private final CronetEngine cronetEngine;
|
||||
private final @CronetEngineSource int cronetEngineSource;
|
||||
|
||||
/**
|
||||
* Source of {@link CronetEngine}.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})
|
||||
public @interface CronetEngineSource {}
|
||||
/**
|
||||
* Natively bundled Cronet implementation.
|
||||
*/
|
||||
public static final int SOURCE_NATIVE = 0;
|
||||
/**
|
||||
* Cronet implementation from GMSCore.
|
||||
*/
|
||||
public static final int SOURCE_GMS = 1;
|
||||
/**
|
||||
* Other (unknown) Cronet implementation.
|
||||
*/
|
||||
public static final int SOURCE_UNKNOWN = 2;
|
||||
/**
|
||||
* User-provided Cronet engine.
|
||||
*/
|
||||
public static final int SOURCE_USER_PROVIDED = 3;
|
||||
/**
|
||||
* No Cronet implementation available. Fallback Http provider is used if possible.
|
||||
*/
|
||||
public static final int SOURCE_UNAVAILABLE = 4;
|
||||
|
||||
/**
|
||||
* Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
|
||||
* {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet
|
||||
* if both are available.
|
||||
*
|
||||
* @param context A context.
|
||||
*/
|
||||
public CronetEngineWrapper(Context context) {
|
||||
this(context, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
|
||||
* {@link CronetProvider} based on user preference.
|
||||
*
|
||||
* @param context A context.
|
||||
* @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively
|
||||
* bundled Cronet if both are available.
|
||||
*/
|
||||
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
|
||||
CronetEngine cronetEngine = null;
|
||||
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
|
||||
List<CronetProvider> cronetProviders = CronetProvider.getAllProviders(context);
|
||||
// Remove disabled and fallback Cronet providers from list
|
||||
for (int i = cronetProviders.size() - 1; i >= 0; i--) {
|
||||
if (!cronetProviders.get(i).isEnabled()
|
||||
|| CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) {
|
||||
cronetProviders.remove(i);
|
||||
}
|
||||
}
|
||||
// Sort remaining providers by type and version.
|
||||
CronetProviderComparator providerComparator = new CronetProviderComparator(preferGMSCoreCronet);
|
||||
Collections.sort(cronetProviders, providerComparator);
|
||||
for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) {
|
||||
String providerName = cronetProviders.get(i).getName();
|
||||
try {
|
||||
cronetEngine = cronetProviders.get(i).createBuilder().build();
|
||||
if (providerComparator.isNativeProvider(providerName)) {
|
||||
cronetEngineSource = SOURCE_NATIVE;
|
||||
} else if (providerComparator.isGMSCoreProvider(providerName)) {
|
||||
cronetEngineSource = SOURCE_GMS;
|
||||
} else {
|
||||
cronetEngineSource = SOURCE_UNKNOWN;
|
||||
}
|
||||
Log.d(TAG, "CronetEngine built using " + providerName);
|
||||
} catch (SecurityException e) {
|
||||
Log.w(TAG, "Failed to build CronetEngine. Please check if current process has "
|
||||
+ "android.permission.ACCESS_NETWORK_STATE.");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
Log.w(TAG, "Failed to link Cronet binaries. Please check if native Cronet binaries are "
|
||||
+ "bundled into your app.");
|
||||
}
|
||||
}
|
||||
if (cronetEngine == null) {
|
||||
Log.w(TAG, "Cronet not available. Using fallback provider.");
|
||||
}
|
||||
this.cronetEngine = cronetEngine;
|
||||
this.cronetEngineSource = cronetEngineSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapper for an existing CronetEngine.
|
||||
*
|
||||
* @param cronetEngine An existing CronetEngine.
|
||||
*/
|
||||
public CronetEngineWrapper(CronetEngine cronetEngine) {
|
||||
this.cronetEngine = cronetEngine;
|
||||
this.cronetEngineSource = SOURCE_USER_PROVIDED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source of the wrapped {@link CronetEngine}.
|
||||
*
|
||||
* @return A {@link CronetEngineSource} value.
|
||||
*/
|
||||
public @CronetEngineSource int getCronetEngineSource() {
|
||||
return cronetEngineSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wrapped {@link CronetEngine}.
|
||||
*
|
||||
* @return The CronetEngine, or null if no CronetEngine is available.
|
||||
*/
|
||||
/* package */ CronetEngine getCronetEngine() {
|
||||
return cronetEngine;
|
||||
}
|
||||
|
||||
private static class CronetProviderComparator implements Comparator<CronetProvider> {
|
||||
|
||||
private final String gmsCoreCronetName;
|
||||
private final boolean preferGMSCoreCronet;
|
||||
|
||||
public CronetProviderComparator(boolean preferGMSCoreCronet) {
|
||||
// GMSCore CronetProvider classes are only available in some configurations.
|
||||
// Thus, we use reflection to copy static name.
|
||||
String gmsCoreVersionString = null;
|
||||
try {
|
||||
Class<?> cronetProviderInstallerClass =
|
||||
Class.forName("com.google.android.gms.net.CronetProviderInstaller");
|
||||
Field providerNameField = cronetProviderInstallerClass.getDeclaredField("PROVIDER_NAME");
|
||||
gmsCoreVersionString = (String) providerNameField.get(null);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// GMSCore CronetProvider not available.
|
||||
} catch (NoSuchFieldException e) {
|
||||
// GMSCore CronetProvider not available.
|
||||
} catch (IllegalAccessException e) {
|
||||
// GMSCore CronetProvider not available.
|
||||
}
|
||||
gmsCoreCronetName = gmsCoreVersionString;
|
||||
this.preferGMSCoreCronet = preferGMSCoreCronet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(CronetProvider providerLeft, CronetProvider providerRight) {
|
||||
int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName());
|
||||
int typePreferenceRight = evaluateCronetProviderType(providerRight.getName());
|
||||
if (typePreferenceLeft != typePreferenceRight) {
|
||||
return typePreferenceLeft - typePreferenceRight;
|
||||
}
|
||||
return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion());
|
||||
}
|
||||
|
||||
public boolean isNativeProvider(String providerName) {
|
||||
return CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName);
|
||||
}
|
||||
|
||||
public boolean isGMSCoreProvider(String providerName) {
|
||||
return gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Cronet provider name into a sortable preference value.
|
||||
* Smaller values are preferred.
|
||||
*/
|
||||
private int evaluateCronetProviderType(String providerName) {
|
||||
if (isNativeProvider(providerName)) {
|
||||
return 1;
|
||||
}
|
||||
if (isGMSCoreProvider(providerName)) {
|
||||
return preferGMSCoreCronet ? 0 : 2;
|
||||
}
|
||||
// Unknown provider type.
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares version strings of format "12.123.35.23".
|
||||
*/
|
||||
private static int compareVersionStrings(String versionLeft, String versionRight) {
|
||||
if (versionLeft == null || versionRight == null) {
|
||||
return 0;
|
||||
}
|
||||
String[] versionStringsLeft = versionLeft.split("\\.");
|
||||
String[] versionStringsRight = versionRight.split("\\.");
|
||||
int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);
|
||||
for (int i = 0; i < minLength; i++) {
|
||||
if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
|
||||
try {
|
||||
int versionIntLeft = Integer.parseInt(versionStringsLeft[i]);
|
||||
int versionIntRight = Integer.parseInt(versionStringsRight[i]);
|
||||
return versionIntLeft - versionIntRight;
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -9,11 +9,10 @@ audio.
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
* Checkout ExoPlayer along with Extensions
|
||||
|
||||
```
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
```
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][]. In addition, it's necessary to build the extension's
|
||||
native components as follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
|
||||
@ -25,8 +24,6 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable:
|
||||
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
```
|
||||
@ -106,20 +103,5 @@ cd "${FFMPEG_EXT_PATH}"/jni && \
|
||||
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
|
||||
```
|
||||
|
||||
* In your project, you can add a dependency on the extension by using a rule
|
||||
like this:
|
||||
|
||||
```
|
||||
// in settings.gradle
|
||||
include ':..:ExoPlayer:library'
|
||||
include ':..:ExoPlayer:extension-ffmpeg'
|
||||
|
||||
// in build.gradle
|
||||
dependencies {
|
||||
compile project(':..:ExoPlayer:library')
|
||||
compile project(':..:ExoPlayer:extension-ffmpeg')
|
||||
}
|
||||
```
|
||||
|
||||
* Now, when you build your app, the extension will be built and the native
|
||||
libraries will be packaged along with the APK.
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
@ -30,7 +31,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-core')
|
||||
compile project(modulePrefix + 'library-core')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -10,11 +10,10 @@ ExoPlayer to play Flac audio on Android devices.
|
||||
|
||||
## Build Instructions ##
|
||||
|
||||
* Checkout ExoPlayer along with Extensions:
|
||||
|
||||
```
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
```
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][]. In addition, it's necessary to build the extension's
|
||||
native components as follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
|
||||
@ -26,8 +25,6 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable:
|
||||
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
```
|
||||
@ -47,20 +44,5 @@ cd "${FLAC_EXT_PATH}"/jni && \
|
||||
${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||
```
|
||||
|
||||
* In your project, you can add a dependency to the Flac Extension by using a
|
||||
rule like this:
|
||||
|
||||
```
|
||||
// in settings.gradle
|
||||
include ':..:ExoPlayer:library'
|
||||
include ':..:ExoPlayer:extension-flac'
|
||||
|
||||
// in build.gradle
|
||||
dependencies {
|
||||
compile project(':..:ExoPlayer:library')
|
||||
compile project(':..:ExoPlayer:extension-flac')
|
||||
}
|
||||
```
|
||||
|
||||
* Now, when you build your app, the Flac extension will be built and the native
|
||||
libraries will be packaged along with the APK.
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
@ -30,8 +31,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-core')
|
||||
androidTestCompile project(':testutils')
|
||||
compile project(modulePrefix + 'library-core')
|
||||
androidTestCompile project(modulePrefix + 'testutils')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -28,7 +28,6 @@
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="com.google.android.exoplayer2.ext.flac.test"
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
tools:replace="android:targetPackage"/>
|
||||
android:name="android.test.InstrumentationTestRunner"/>
|
||||
|
||||
</manifest>
|
||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Unit test for {@link FlacExtractor}.
|
||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public class FlacExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testSample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FlacExtractor();
|
||||
|
@ -126,6 +126,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#define LOG_TAG "FLACParser"
|
||||
#define ALOGE(...) \
|
||||
|
@ -6,7 +6,10 @@ The GVR extension wraps the [Google VR SDK for Android][]. It provides a
|
||||
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
|
||||
of surround sound and ambisonic soundfields.
|
||||
|
||||
## Using the extension ##
|
||||
[Google VR SDK for Android]: https://developers.google.com/vr/android/
|
||||
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency. You
|
||||
need to make sure you have the jcenter repository included in the `build.gradle`
|
||||
@ -27,12 +30,15 @@ compile 'com.google.android.exoplayer:extension-gvr:rX.X.X'
|
||||
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
||||
library being used.
|
||||
|
||||
## Using GvrAudioProcessor ##
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
|
||||
return a GvrAudioProcessor.
|
||||
* If constructing renderers directly, pass a GvrAudioProcessor to
|
||||
MediaCodecAudioRenderer's constructor.
|
||||
|
||||
[Google VR SDK for Android]: https://developers.google.com/vr/android/
|
||||
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
@ -24,8 +25,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-core')
|
||||
compile 'com.google.vr:sdk-audio:1.30.0'
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.google.vr:sdk-audio:1.60.1'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -82,6 +82,9 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
maybeReleaseGvrAudioSurround();
|
||||
int surroundFormat;
|
||||
switch (channelCount) {
|
||||
case 1:
|
||||
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
|
||||
break;
|
||||
case 2:
|
||||
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
|
||||
break;
|
||||
|
43
extensions/ima/README.md
Normal file
43
extensions/ima/README.md
Normal 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.
|
25
extensions/ima/build.gradle
Normal file
25
extensions/ima/build.gradle
Normal 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
|
||||
}
|
5
extensions/ima/src/main/AndroidManifest.xml
Normal file
5
extensions/ima/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.ext.ima">
|
||||
<meta-data android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version"/>
|
||||
</manifest>
|
@ -0,0 +1,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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -5,19 +5,12 @@
|
||||
The OkHttp Extension is an [HttpDataSource][] implementation using Square's
|
||||
[OkHttp][].
|
||||
|
||||
## Using the extension ##
|
||||
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
||||
[OkHttp]: https://square.github.io/okhttp/
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency. You
|
||||
need to make sure you have the jcenter repository included in the `build.gradle`
|
||||
file in the root of your project:
|
||||
## Getting the extension ##
|
||||
|
||||
```gradle
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
```
|
||||
|
||||
Next, include the following in your module's `build.gradle` file:
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
|
||||
@ -26,5 +19,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
|
||||
library being used.
|
||||
|
||||
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
||||
[OkHttp]: https://square.github.io/okhttp/
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
@ -29,7 +30,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-core')
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile('com.squareup.okhttp3:okhttp:3.6.0') {
|
||||
exclude group: 'org.json'
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
package com.google.android.exoplayer2.ext.okhttp;
|
||||
|
||||
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.upstream.DataSourceException;
|
||||
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 final Call.Factory callFactory;
|
||||
private final String userAgent;
|
||||
private final Predicate<String> contentTypePredicate;
|
||||
private final TransferListener<? super OkHttpDataSource> listener;
|
||||
private final CacheControl cacheControl;
|
||||
private final RequestProperties defaultRequestProperties;
|
||||
private final RequestProperties requestProperties;
|
||||
@NonNull private final Call.Factory callFactory;
|
||||
@NonNull private final RequestProperties requestProperties;
|
||||
|
||||
@Nullable private final String userAgent;
|
||||
@Nullable private final Predicate<String> contentTypePredicate;
|
||||
@Nullable private final TransferListener<? super OkHttpDataSource> listener;
|
||||
@Nullable private final CacheControl cacheControl;
|
||||
@Nullable private final RequestProperties defaultRequestProperties;
|
||||
|
||||
private DataSpec dataSpec;
|
||||
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
|
||||
* 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
|
||||
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
|
||||
*/
|
||||
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
||||
Predicate<String> contentTypePredicate) {
|
||||
public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||
@Nullable Predicate<String> contentTypePredicate) {
|
||||
this(callFactory, userAgent, contentTypePredicate, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||
* 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
|
||||
* predicate then a {@link InvalidContentTypeException} is thrown from
|
||||
* {@link #open(DataSpec)}.
|
||||
* @param listener An optional listener.
|
||||
*/
|
||||
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener) {
|
||||
public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||
@Nullable Predicate<String> contentTypePredicate,
|
||||
@Nullable TransferListener<? super OkHttpDataSource> listener) {
|
||||
this(callFactory, userAgent, contentTypePredicate, listener, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||
* 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
|
||||
* predicate then a {@link InvalidContentTypeException} is thrown from
|
||||
* {@link #open(DataSpec)}.
|
||||
@ -102,11 +106,12 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
|
||||
* the server as HTTP headers on every request.
|
||||
*/
|
||||
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,
|
||||
CacheControl cacheControl, RequestProperties defaultRequestProperties) {
|
||||
public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||
@Nullable Predicate<String> contentTypePredicate,
|
||||
@Nullable TransferListener<? super OkHttpDataSource> listener,
|
||||
@Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) {
|
||||
this.callFactory = Assertions.checkNotNull(callFactory);
|
||||
this.userAgent = Assertions.checkNotEmpty(userAgent);
|
||||
this.userAgent = userAgent;
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
this.listener = listener;
|
||||
this.cacheControl = cacheControl;
|
||||
@ -280,7 +285,10 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||
}
|
||||
builder.addHeader("Range", rangeRequest);
|
||||
}
|
||||
builder.addHeader("User-Agent", userAgent);
|
||||
if (userAgent != null) {
|
||||
builder.addHeader("User-Agent", userAgent);
|
||||
}
|
||||
|
||||
if (!allowGzip) {
|
||||
builder.addHeader("Accept-Encoding", "identity");
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
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.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
@ -28,31 +30,32 @@ import okhttp3.Call;
|
||||
*/
|
||||
public final class OkHttpDataSourceFactory extends BaseFactory {
|
||||
|
||||
private final Call.Factory callFactory;
|
||||
private final String userAgent;
|
||||
private final TransferListener<? super DataSource> listener;
|
||||
private final CacheControl cacheControl;
|
||||
@NonNull private final Call.Factory callFactory;
|
||||
@Nullable private final String userAgent;
|
||||
@Nullable private final TransferListener<? super DataSource> listener;
|
||||
@Nullable private final CacheControl cacheControl;
|
||||
|
||||
/**
|
||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||
* 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.
|
||||
*/
|
||||
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
|
||||
TransferListener<? super DataSource> listener) {
|
||||
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||
@Nullable TransferListener<? super DataSource> listener) {
|
||||
this(callFactory, userAgent, listener, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||
* 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 cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
||||
*/
|
||||
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
|
||||
TransferListener<? super DataSource> listener, CacheControl cacheControl) {
|
||||
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||
@Nullable TransferListener<? super DataSource> listener,
|
||||
@Nullable CacheControl cacheControl) {
|
||||
this.callFactory = callFactory;
|
||||
this.userAgent = userAgent;
|
||||
this.listener = listener;
|
||||
|
@ -10,11 +10,10 @@ ExoPlayer to play Opus audio on Android devices.
|
||||
|
||||
## Build Instructions ##
|
||||
|
||||
* Checkout ExoPlayer along with Extensions:
|
||||
|
||||
```
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
```
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][]. In addition, it's necessary to build the extension's
|
||||
native components as follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
|
||||
@ -26,8 +25,6 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main"
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable:
|
||||
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
```
|
||||
@ -52,23 +49,8 @@ cd "${OPUS_EXT_PATH}"/jni && \
|
||||
${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||
```
|
||||
|
||||
* In your project, you can add a dependency to the Opus Extension by using a
|
||||
rule like this:
|
||||
|
||||
```
|
||||
// in settings.gradle
|
||||
include ':..:ExoPlayer:library'
|
||||
include ':..:ExoPlayer:extension-opus'
|
||||
|
||||
// in build.gradle
|
||||
dependencies {
|
||||
compile project(':..:ExoPlayer:library')
|
||||
compile project(':..:ExoPlayer:extension-opus')
|
||||
}
|
||||
```
|
||||
|
||||
* Now, when you build your app, the Opus extension will be built and the native
|
||||
libraries will be packaged along with the APK.
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
|
||||
## Notes ##
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
@ -30,7 +31,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-core')
|
||||
compile project(modulePrefix + 'library-core')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -28,7 +28,6 @@
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="com.google.android.exoplayer2.ext.opus.test"
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
tools:replace="android:targetPackage"/>
|
||||
android:name="android.test.InstrumentationTestRunner"/>
|
||||
|
||||
</manifest>
|
||||
|
@ -126,6 +126,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
|
@ -10,11 +10,10 @@ VP9 video on Android devices.
|
||||
|
||||
## Build Instructions ##
|
||||
|
||||
* Checkout ExoPlayer along with Extensions:
|
||||
|
||||
```
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
```
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][]. In addition, it's necessary to build the extension's
|
||||
native components as follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
|
||||
@ -26,8 +25,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable:
|
||||
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
```
|
||||
@ -66,23 +63,8 @@ cd "${VP9_EXT_PATH}"/jni && \
|
||||
${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||
```
|
||||
|
||||
* In your project, you can add a dependency to the VP9 Extension by using a the
|
||||
following rule:
|
||||
|
||||
```
|
||||
// in settings.gradle
|
||||
include ':..:ExoPlayer:library'
|
||||
include ':..:ExoPlayer:extension-vp9'
|
||||
|
||||
// in build.gradle
|
||||
dependencies {
|
||||
compile project(':..:ExoPlayer:library')
|
||||
compile project(':..:ExoPlayer:extension-vp9')
|
||||
}
|
||||
```
|
||||
|
||||
* Now, when you build your app, the VP9 extension will be built and the native
|
||||
libraries will be packaged along with the APK.
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
|
||||
## Notes ##
|
||||
|
||||
@ -94,4 +76,3 @@ dependencies {
|
||||
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
|
||||
please note that `generate_libvpx_android_configs.sh` and the makefiles need
|
||||
to be modified to work with arbitrary versions of libvpx and libyuv.
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
@ -30,7 +31,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-core')
|
||||
compile project(modulePrefix + 'library-core')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -28,7 +28,6 @@
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="com.google.android.exoplayer2.ext.vp9.test"
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
tools:replace="android:targetPackage"/>
|
||||
android:name="android.test.InstrumentationTestRunner"/>
|
||||
|
||||
</manifest>
|
||||
|
@ -158,6 +158,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
|
@ -20,6 +20,7 @@ import android.graphics.Canvas;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.view.Surface;
|
||||
import com.google.android.exoplayer2.BaseRenderer;
|
||||
import com.google.android.exoplayer2.C;
|
||||
@ -30,6 +31,7 @@ import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
@ -38,12 +40,35 @@ import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Decodes and renders video using the native VP9 decoder.
|
||||
*/
|
||||
public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
|
||||
REINITIALIZATION_STATE_WAIT_END_OF_STREAM})
|
||||
private @interface ReinitializationState {}
|
||||
/**
|
||||
* The decoder does not need to be re-initialized.
|
||||
*/
|
||||
private static final int REINITIALIZATION_STATE_NONE = 0;
|
||||
/**
|
||||
* The input format has changed in a way that requires the decoder to be re-initialized, but we
|
||||
* haven't yet signaled an end of stream to the existing decoder. We need to do so in order to
|
||||
* ensure that it outputs any remaining buffers before we release it.
|
||||
*/
|
||||
private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
|
||||
/**
|
||||
* The input format has changed in a way that requires the decoder to be re-initialized, and we've
|
||||
* signaled an end of stream to the existing decoder. We're waiting for the decoder to output an
|
||||
* end of stream signal to indicate that it has output any remaining buffers before we release it.
|
||||
*/
|
||||
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
|
||||
|
||||
/**
|
||||
* The type of a message that can be passed to an instance of this class via
|
||||
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
|
||||
@ -71,12 +96,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
private DecoderCounters decoderCounters;
|
||||
private Format format;
|
||||
private VpxDecoder decoder;
|
||||
private DecoderInputBuffer inputBuffer;
|
||||
private VpxInputBuffer inputBuffer;
|
||||
private VpxOutputBuffer outputBuffer;
|
||||
private VpxOutputBuffer nextOutputBuffer;
|
||||
private DrmSession<ExoMediaCrypto> drmSession;
|
||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
||||
|
||||
@ReinitializationState
|
||||
private int decoderReinitializationState;
|
||||
private boolean decoderReceivedBuffers;
|
||||
|
||||
private Bitmap bitmap;
|
||||
private boolean renderedFirstFrame;
|
||||
private long joiningDeadlineMs;
|
||||
@ -153,6 +182,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
outputMode = VpxDecoder.OUTPUT_MODE_NONE;
|
||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -185,49 +215,25 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// We have a format.
|
||||
drmSession = pendingDrmSession;
|
||||
ExoMediaCrypto mediaCrypto = null;
|
||||
if (drmSession != null) {
|
||||
int drmSessionState = drmSession.getState();
|
||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||
} else if (drmSessionState == DrmSession.STATE_OPENED
|
||||
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
|
||||
mediaCrypto = drmSession.getMediaCrypto();
|
||||
} else {
|
||||
// The drm session isn't open yet.
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (decoder == null) {
|
||||
// If we don't have a decoder yet, we need to instantiate one.
|
||||
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||
TraceUtil.beginSection("createVpxDecoder");
|
||||
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
|
||||
decoder.setOutputMode(outputMode);
|
||||
// If we don't have a decoder yet, we need to instantiate one.
|
||||
maybeInitDecoder();
|
||||
|
||||
if (decoder != null) {
|
||||
try {
|
||||
// Rendering loop.
|
||||
TraceUtil.beginSection("drainAndFeed");
|
||||
while (drainOutputBuffer(positionUs)) {}
|
||||
while (feedInputBuffer()) {}
|
||||
TraceUtil.endSection();
|
||||
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
|
||||
codecInitializedTimestamp - codecInitializingTimestamp);
|
||||
decoderCounters.decoderInitCount++;
|
||||
} catch (VpxDecoderException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
TraceUtil.beginSection("drainAndFeed");
|
||||
while (drainOutputBuffer(positionUs)) {}
|
||||
while (feedInputBuffer()) {}
|
||||
TraceUtil.endSection();
|
||||
} catch (VpxDecoderException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
decoderCounters.ensureUpdated();
|
||||
}
|
||||
decoderCounters.ensureUpdated();
|
||||
}
|
||||
|
||||
private boolean drainOutputBuffer(long positionUs) throws VpxDecoderException {
|
||||
if (outputStreamEnded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException,
|
||||
VpxDecoderException {
|
||||
// Acquire outputBuffer either from nextOutputBuffer or from the decoder.
|
||||
if (outputBuffer == null) {
|
||||
if (nextOutputBuffer != null) {
|
||||
@ -247,15 +253,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
}
|
||||
|
||||
if (outputBuffer.isEndOfStream()) {
|
||||
outputStreamEnded = true;
|
||||
outputBuffer.release();
|
||||
outputBuffer = null;
|
||||
if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
|
||||
// We're waiting to re-initialize the decoder, and have now processed all final buffers.
|
||||
releaseDecoder();
|
||||
maybeInitDecoder();
|
||||
} else {
|
||||
outputBuffer.release();
|
||||
outputBuffer = null;
|
||||
outputStreamEnded = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
|
||||
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
|
||||
if (outputBuffer.timeUs <= positionUs) {
|
||||
if (isBufferLate(outputBuffer.timeUs - positionUs)) {
|
||||
skipBuffer();
|
||||
return true;
|
||||
}
|
||||
@ -280,23 +292,20 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether the current frame should be dropped.
|
||||
*
|
||||
* @param outputBufferTimeUs The timestamp of the current output buffer.
|
||||
* @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 joiningDeadlineMs The joining deadline.
|
||||
* @return Returns whether to drop the current output buffer.
|
||||
*/
|
||||
protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs,
|
||||
long positionUs, long joiningDeadlineMs) {
|
||||
// Drop the frame if we're joining and are more than 30ms late, or if we have the next frame
|
||||
// and that's also late. Else we'll render what we have.
|
||||
return (joiningDeadlineMs != C.TIME_UNSET && outputBufferTimeUs < positionUs - 30000)
|
||||
|| (nextOutputBufferTimeUs != C.TIME_UNSET && nextOutputBufferTimeUs < positionUs);
|
||||
return isBufferLate(outputBufferTimeUs - positionUs)
|
||||
&& (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
|
||||
}
|
||||
|
||||
private void renderBuffer() {
|
||||
@ -356,7 +365,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
}
|
||||
|
||||
private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
|
||||
if (inputStreamEnded) {
|
||||
if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
|
||||
|| inputStreamEnded) {
|
||||
// We need to reinitialize the decoder or the input stream has ended.
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -367,6 +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;
|
||||
if (waitingForKeys) {
|
||||
// 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;
|
||||
}
|
||||
inputBuffer.flip();
|
||||
inputBuffer.colorInfo = formatHolder.format.colorInfo;
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
decoderReceivedBuffers = true;
|
||||
decoderCounters.inputBufferCount++;
|
||||
inputBuffer = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||
if (drmSession == null) {
|
||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||
return false;
|
||||
}
|
||||
int drmSessionState = drmSession.getState();
|
||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||
}
|
||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
|
||||
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
|
||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||
}
|
||||
|
||||
private void flushDecoder() {
|
||||
inputBuffer = null;
|
||||
private void flushDecoder() throws ExoPlaybackException {
|
||||
waitingForKeys = false;
|
||||
if (outputBuffer != null) {
|
||||
outputBuffer.release();
|
||||
outputBuffer = null;
|
||||
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
|
||||
releaseDecoder();
|
||||
maybeInitDecoder();
|
||||
} else {
|
||||
inputBuffer = null;
|
||||
if (outputBuffer != null) {
|
||||
outputBuffer.release();
|
||||
outputBuffer = null;
|
||||
}
|
||||
if (nextOutputBuffer != null) {
|
||||
nextOutputBuffer.release();
|
||||
nextOutputBuffer = null;
|
||||
}
|
||||
decoder.flush();
|
||||
decoderReceivedBuffers = false;
|
||||
}
|
||||
if (nextOutputBuffer != null) {
|
||||
nextOutputBuffer.release();
|
||||
nextOutputBuffer = null;
|
||||
}
|
||||
decoder.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -461,7 +487,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPositionReset(long positionUs, boolean joining) {
|
||||
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
clearRenderedFirstFrame();
|
||||
@ -480,18 +506,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
protected void onStarted() {
|
||||
droppedFrames = 0;
|
||||
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
|
||||
joiningDeadlineMs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopped() {
|
||||
joiningDeadlineMs = C.TIME_UNSET;
|
||||
maybeNotifyDroppedFrames();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
inputBuffer = null;
|
||||
outputBuffer = null;
|
||||
format = null;
|
||||
waitingForKeys = false;
|
||||
clearReportedVideoSize();
|
||||
@ -518,20 +542,53 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseDecoder() {
|
||||
private void maybeInitDecoder() throws ExoPlaybackException {
|
||||
if (decoder != null) {
|
||||
decoder.release();
|
||||
decoder = null;
|
||||
decoderCounters.decoderReleaseCount++;
|
||||
waitingForKeys = false;
|
||||
if (drmSession != null && pendingDrmSession != drmSession) {
|
||||
try {
|
||||
drmSessionManager.releaseSession(drmSession);
|
||||
} finally {
|
||||
drmSession = null;
|
||||
return;
|
||||
}
|
||||
|
||||
drmSession = pendingDrmSession;
|
||||
ExoMediaCrypto mediaCrypto = null;
|
||||
if (drmSession != null) {
|
||||
mediaCrypto = drmSession.getMediaCrypto();
|
||||
if (mediaCrypto == null) {
|
||||
DrmSessionException drmError = drmSession.getError();
|
||||
if (drmError != null) {
|
||||
throw ExoPlaybackException.createForRenderer(drmError, getIndex());
|
||||
}
|
||||
// The drm session isn't open yet.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||
TraceUtil.beginSection("createVpxDecoder");
|
||||
decoder = new VpxDecoder(NUM_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 {
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.drm.DecryptionException;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
@ -27,7 +26,7 @@ import java.nio.ByteBuffer;
|
||||
* Vpx decoder.
|
||||
*/
|
||||
/* package */ final class VpxDecoder extends
|
||||
SimpleDecoder<DecoderInputBuffer, VpxOutputBuffer, VpxDecoderException> {
|
||||
SimpleDecoder<VpxInputBuffer, VpxOutputBuffer, VpxDecoderException> {
|
||||
|
||||
public static final int OUTPUT_MODE_NONE = -1;
|
||||
public static final int OUTPUT_MODE_YUV = 0;
|
||||
@ -54,7 +53,7 @@ import java.nio.ByteBuffer;
|
||||
*/
|
||||
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
||||
ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
|
||||
super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
||||
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
||||
if (!VpxLibrary.isAvailable()) {
|
||||
throw new VpxDecoderException("Failed to load decoder native libraries.");
|
||||
}
|
||||
@ -85,8 +84,8 @@ import java.nio.ByteBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DecoderInputBuffer createInputBuffer() {
|
||||
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
|
||||
protected VpxInputBuffer createInputBuffer() {
|
||||
return new VpxInputBuffer();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -100,7 +99,7 @@ import java.nio.ByteBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VpxDecoderException decode(DecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
|
||||
protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
|
||||
boolean reset) {
|
||||
ByteBuffer inputData = inputBuffer.data;
|
||||
int inputSize = inputData.limit();
|
||||
@ -128,6 +127,7 @@ import java.nio.ByteBuffer;
|
||||
} else if (getFrameResult == -1) {
|
||||
return new VpxDecoderException("Buffer initialization failed.");
|
||||
}
|
||||
outputBuffer.colorInfo = inputBuffer.colorInfo;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.ext.vp9;
|
||||
|
||||
import com.google.android.exoplayer2.decoder.OutputBuffer;
|
||||
import com.google.android.exoplayer2.video.ColorInfo;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
@ -37,6 +38,8 @@ import java.nio.ByteBuffer;
|
||||
public ByteBuffer data;
|
||||
public int width;
|
||||
public int height;
|
||||
public ColorInfo colorInfo;
|
||||
|
||||
/**
|
||||
* YUV planes for YUV mode.
|
||||
*/
|
||||
|
@ -51,8 +51,7 @@ config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
|
||||
config[3]+=" --disable-avx2 --enable-pic"
|
||||
|
||||
arch[4]="arm64-v8a"
|
||||
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --disable-neon"
|
||||
config[4]+=" --disable-neon-asm"
|
||||
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
|
||||
|
||||
arch[5]="x86_64"
|
||||
config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"
|
||||
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
@ -24,11 +25,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-core')
|
||||
compile project(':library-dash')
|
||||
compile project(':library-hls')
|
||||
compile project(':library-smoothstreaming')
|
||||
compile project(':library-ui')
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'library-smoothstreaming')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -12,6 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply plugin: 'com.android.library'
|
||||
apply from: '../../constants.gradle'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
@ -22,6 +23,7 @@ android {
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
// Workaround to prevent circular dependency on project :testutils.
|
||||
sourceSets {
|
||||
androidTest {
|
||||
java.srcDirs += "../../testutils/src/main/java/"
|
||||
|
@ -24,11 +24,13 @@
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
<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>
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="com.google.android.exoplayer2.core.test"
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
tools:replace="android:targetPackage"/>
|
||||
android:name="android.test.InstrumentationTestRunner"/>
|
||||
|
||||
</manifest>
|
||||
|
Binary file not shown.
@ -30,5 +30,6 @@ track 1:
|
||||
time = 0
|
||||
flags = 1073741824
|
||||
data = length 39, hash B7FE77F4
|
||||
crypto mode = 1
|
||||
encryption key = length 16, hash 4CE944CF
|
||||
tracksEnded = true
|
||||
|
@ -30,5 +30,6 @@ track 1:
|
||||
time = 0
|
||||
flags = 1073741824
|
||||
data = length 24, hash E58668B1
|
||||
crypto mode = 1
|
||||
encryption key = length 16, hash 4CE944CF
|
||||
tracksEnded = true
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -8,7 +8,7 @@ track 0:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/x-flac
|
||||
sampleMimeType = audio/flac
|
||||
maxInputSize = 768000
|
||||
width = -1
|
||||
height = -1
|
||||
|
@ -9,7 +9,7 @@ track 0:
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/vorbis
|
||||
maxInputSize = 65025
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
|
@ -9,7 +9,7 @@ track 0:
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/vorbis
|
||||
maxInputSize = 65025
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
|
@ -9,7 +9,7 @@ track 0:
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/vorbis
|
||||
maxInputSize = 65025
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
|
@ -9,7 +9,7 @@ track 0:
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/vorbis
|
||||
maxInputSize = 65025
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
|
@ -9,7 +9,7 @@ track 0:
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/vorbis
|
||||
maxInputSize = 65025
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
|
@ -15,28 +15,20 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MediaClock;
|
||||
import com.google.android.exoplayer2.testutil.ExoPlayerWrapper;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeRenderer;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
@ -62,7 +54,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
* error.
|
||||
*/
|
||||
public void testPlayEmptyTimeline() throws Exception {
|
||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = Timeline.EMPTY;
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
||||
FakeRenderer renderer = new FakeRenderer(null);
|
||||
@ -79,7 +71,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
* Tests playback of a source that exposes a single period.
|
||||
*/
|
||||
public void testPlaySinglePeriodTimeline() throws Exception {
|
||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||
Object manifest = new Object();
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
|
||||
@ -98,7 +90,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
* Tests playback of a source that exposes three periods.
|
||||
*/
|
||||
public void testPlayMultiPeriodTimeline() throws Exception {
|
||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
@ -119,7 +111,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
* source.
|
||||
*/
|
||||
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
|
||||
final PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
@ -166,7 +158,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
}
|
||||
|
||||
public void testRepreparationGivesFreshSourceInfo() throws Exception {
|
||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
|
||||
@ -218,501 +210,54 @@ public final class ExoPlayerTest extends TestCase {
|
||||
Pair.create(timeline, thirdSourceManifest));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a player with its own handler thread.
|
||||
*/
|
||||
private static final class PlayerWrapper implements ExoPlayer.EventListener {
|
||||
|
||||
private final CountDownLatch sourceInfoCountDownLatch;
|
||||
private final CountDownLatch endedCountDownLatch;
|
||||
private final HandlerThread playerThread;
|
||||
private final Handler handler;
|
||||
private final LinkedList<Pair<Timeline, Object>> sourceInfos;
|
||||
|
||||
private ExoPlayer player;
|
||||
private TrackGroupArray trackGroups;
|
||||
private Exception exception;
|
||||
|
||||
// Written only on the main thread.
|
||||
private volatile int positionDiscontinuityCount;
|
||||
|
||||
public PlayerWrapper() {
|
||||
sourceInfoCountDownLatch = new CountDownLatch(1);
|
||||
endedCountDownLatch = new CountDownLatch(1);
|
||||
playerThread = new HandlerThread("ExoPlayerTest thread");
|
||||
playerThread.start();
|
||||
handler = new Handler(playerThread.getLooper());
|
||||
sourceInfos = new LinkedList<>();
|
||||
}
|
||||
|
||||
// Called on the test thread.
|
||||
|
||||
public void blockUntilEnded(long timeoutMs) throws Exception {
|
||||
if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
|
||||
exception = new TimeoutException("Test playback timed out waiting for playback to end.");
|
||||
}
|
||||
release();
|
||||
// Throw any pending exception (from playback, timing out or releasing).
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception {
|
||||
if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
|
||||
throw new TimeoutException("Test playback timed out waiting for source info.");
|
||||
}
|
||||
}
|
||||
|
||||
public void setup(final MediaSource mediaSource, final Renderer... renderers) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector());
|
||||
player.addListener(PlayerWrapper.this);
|
||||
player.setPlayWhenReady(true);
|
||||
player.prepare(mediaSource);
|
||||
} catch (Exception e) {
|
||||
handleError(e);
|
||||
}
|
||||
public void testRepeatModeChanges() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000));
|
||||
final int[] actionSchedule = { // 0 -> 1
|
||||
ExoPlayer.REPEAT_MODE_ONE, // 1 -> 1
|
||||
ExoPlayer.REPEAT_MODE_OFF, // 1 -> 2
|
||||
ExoPlayer.REPEAT_MODE_ONE, // 2 -> 2
|
||||
ExoPlayer.REPEAT_MODE_ALL, // 2 -> 0
|
||||
ExoPlayer.REPEAT_MODE_ONE, // 0 -> 0
|
||||
-1, // 0 -> 0
|
||||
ExoPlayer.REPEAT_MODE_OFF, // 0 -> 1
|
||||
-1, // 1 -> 2
|
||||
-1 // 2 -> ended
|
||||
};
|
||||
int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2};
|
||||
final LinkedList<Integer> windowIndices = new LinkedList<>();
|
||||
final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length);
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() {
|
||||
@Override
|
||||
@SuppressWarnings("ResourceType")
|
||||
public void onPositionDiscontinuity() {
|
||||
super.onPositionDiscontinuity();
|
||||
int actionIndex = actionSchedule.length - (int) actionCounter.getCount();
|
||||
if (actionSchedule[actionIndex] != -1) {
|
||||
player.setRepeatMode(actionSchedule[actionIndex]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void prepare(final MediaSource mediaSource) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
player.prepare(mediaSource);
|
||||
} catch (Exception e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void release() throws InterruptedException {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (player != null) {
|
||||
player.release();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
playerThread.quit();
|
||||
}
|
||||
}
|
||||
});
|
||||
playerThread.join();
|
||||
}
|
||||
|
||||
private void handleError(Exception exception) {
|
||||
if (this.exception == null) {
|
||||
this.exception = exception;
|
||||
windowIndices.add(player.getCurrentWindowIndex());
|
||||
actionCounter.countDown();
|
||||
}
|
||||
endedCountDownLatch.countDown();
|
||||
};
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
playerWrapper.release();
|
||||
assertTrue("Test playback timed out waiting for action schedule to end.", finished);
|
||||
if (playerWrapper.exception != null) {
|
||||
throw playerWrapper.exception;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final void assertSourceInfosEquals(Pair<Timeline, Object>... sourceInfos) {
|
||||
assertEquals(sourceInfos.length, this.sourceInfos.size());
|
||||
for (Pair<Timeline, Object> sourceInfo : sourceInfos) {
|
||||
assertEquals(sourceInfo, this.sourceInfos.remove());
|
||||
}
|
||||
assertEquals(expectedWindowIndices.length, windowIndices.size());
|
||||
for (int i = 0; i < expectedWindowIndices.length; i++) {
|
||||
assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue());
|
||||
}
|
||||
|
||||
// ExoPlayer.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == ExoPlayer.STATE_ENDED) {
|
||||
endedCountDownLatch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
sourceInfos.add(Pair.create(timeline, manifest));
|
||||
sourceInfoCountDownLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
this.trackGroups = trackGroups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException exception) {
|
||||
handleError(exception);
|
||||
}
|
||||
|
||||
@SuppressWarnings("NonAtomicVolatileUpdate")
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
positionDiscontinuityCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class TimelineWindowDefinition {
|
||||
|
||||
public final boolean isSeekable;
|
||||
public final boolean isDynamic;
|
||||
public final long durationUs;
|
||||
|
||||
public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) {
|
||||
this.isSeekable = isSeekable;
|
||||
this.isDynamic = isDynamic;
|
||||
this.durationUs = durationUs;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class FakeTimeline extends Timeline {
|
||||
|
||||
private final TimelineWindowDefinition[] windowDefinitions;
|
||||
|
||||
public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {
|
||||
this.windowDefinitions = windowDefinitions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return windowDefinitions.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
|
||||
Object id = setIds ? windowIndex : null;
|
||||
return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable,
|
||||
windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return windowDefinitions.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||
TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
|
||||
Object id = setIds ? periodIndex : null;
|
||||
return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexOfPeriod(Object uid) {
|
||||
if (!(uid instanceof Integer)) {
|
||||
return C.INDEX_UNSET;
|
||||
}
|
||||
int index = (Integer) uid;
|
||||
return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating
|
||||
* the period will return a {@link FakeMediaPeriod}.
|
||||
*/
|
||||
private static class FakeMediaSource implements MediaSource {
|
||||
|
||||
private final Timeline timeline;
|
||||
private final Object manifest;
|
||||
private final TrackGroupArray trackGroupArray;
|
||||
private final ArrayList<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;
|
||||
}
|
||||
|
||||
assertEquals(9, playerWrapper.positionDiscontinuityCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -53,9 +53,9 @@ public final class FormatTest extends TestCase {
|
||||
}
|
||||
|
||||
public void testParcelable() {
|
||||
DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4,
|
||||
DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||
DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM,
|
||||
DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM,
|
||||
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
|
||||
byte[] projectionData = new byte[] {1, 2, 3};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -31,16 +31,16 @@ import junit.framework.TestCase;
|
||||
*/
|
||||
public class DrmInitDataTest extends TestCase {
|
||||
|
||||
private static final SchemeData DATA_1 =
|
||||
new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */));
|
||||
private static final SchemeData DATA_2 =
|
||||
new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */));
|
||||
private static final SchemeData DATA_1B =
|
||||
new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */));
|
||||
private static final SchemeData DATA_2B =
|
||||
new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */));
|
||||
private static final SchemeData DATA_UNIVERSAL =
|
||||
new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */));
|
||||
private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||
private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 2 /* data seed */));
|
||||
private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||
private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 2 /* data seed */));
|
||||
private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, null, VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 3 /* data seed */));
|
||||
|
||||
public void testParcelable() {
|
||||
DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2);
|
||||
|
@ -154,7 +154,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
private static DrmInitData newDrmInitData() {
|
||||
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType",
|
||||
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "cenc", "mimeType",
|
||||
new byte[] {1, 4, 7, 0, 3, 6}));
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.flv;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Unit test for {@link FlvExtractor}.
|
||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public final class FlvExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testSample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FlvExtractor();
|
||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mkv;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Tests for {@link MatroskaExtractor}.
|
||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public final class MatroskaExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testMkvSample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new MatroskaExtractor();
|
||||
@ -34,7 +35,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
public void testWebmSubsampleEncryption() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new MatroskaExtractor();
|
||||
@ -43,7 +44,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new MatroskaExtractor();
|
||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mp3;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Unit test for {@link Mp3Extractor}.
|
||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public final class Mp3ExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testMp3Sample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new Mp3Extractor();
|
||||
@ -34,7 +35,7 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
public void testTrimmedMp3Sample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new Mp3Extractor();
|
||||
|
@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Unit test for {@link FragmentedMp4Extractor}.
|
||||
@ -26,26 +27,28 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testSample() throws Exception {
|
||||
TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
|
||||
ExtractorAsserts
|
||||
.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
|
||||
}
|
||||
|
||||
public void testSampleWithSeiPayloadParsing() throws Exception {
|
||||
// Enabling the CEA-608 track enables SEI payload parsing.
|
||||
TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
|
||||
ExtractorAsserts.assertOutput(
|
||||
getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
|
||||
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
|
||||
}
|
||||
|
||||
public void testAtomWithZeroSize() throws Exception {
|
||||
TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
|
||||
ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
|
||||
getInstrumentation(), ParserException.class);
|
||||
}
|
||||
|
||||
private static TestUtil.ExtractorFactory getExtractorFactory() {
|
||||
private static ExtractorFactory getExtractorFactory() {
|
||||
return getExtractorFactory(0);
|
||||
}
|
||||
|
||||
private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) {
|
||||
return new TestUtil.ExtractorFactory() {
|
||||
private static ExtractorFactory getExtractorFactory(final int flags) {
|
||||
return new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FragmentedMp4Extractor(flags, null);
|
||||
|
@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
|
||||
import android.annotation.TargetApi;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Tests for {@link Mp4Extractor}.
|
||||
@ -27,7 +28,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public final class Mp4ExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testMp4Sample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new Mp4Extractor();
|
||||
|
@ -17,9 +17,10 @@ package com.google.android.exoplayer2.extractor.ogg;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil.ExtractorFactory;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
@ -35,20 +36,21 @@ public final class OggExtractorTest extends InstrumentationTestCase {
|
||||
};
|
||||
|
||||
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 {
|
||||
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 {
|
||||
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg",
|
||||
ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg",
|
||||
getInstrumentation());
|
||||
}
|
||||
|
||||
public void testVorbis() throws Exception {
|
||||
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", getInstrumentation());
|
||||
ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg",
|
||||
getInstrumentation());
|
||||
}
|
||||
|
||||
public void testSniffVorbis() throws Exception {
|
||||
|
@ -19,7 +19,8 @@ import android.annotation.TargetApi;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
/**
|
||||
@ -29,8 +30,8 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
||||
public final class RawCcExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testRawCcSample() throws Exception {
|
||||
TestUtil.assertOutput(
|
||||
new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(
|
||||
new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new RawCcExtractor(
|
||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Unit test for {@link Ac3Extractor}.
|
||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public final class Ac3ExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testSample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new Ac3Extractor();
|
||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Unit test for {@link AdtsExtractor}.
|
||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public final class AdtsExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testSample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new AdtsExtractor();
|
||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Unit test for {@link PsExtractor}.
|
||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public final class PsExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testSample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new PsExtractor();
|
||||
|
@ -25,6 +25,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||
@ -43,7 +45,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
||||
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
|
||||
|
||||
public void testSample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new TsExtractor();
|
||||
@ -65,7 +67,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
||||
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
|
||||
fileData = out.toByteArray();
|
||||
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new TsExtractor();
|
||||
@ -75,7 +77,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testCustomPesReader() throws Exception {
|
||||
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);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
||||
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
|
||||
@ -100,7 +102,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testCustomInitialSectionReader() throws Exception {
|
||||
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);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
||||
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
|
||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.wav;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
|
||||
/**
|
||||
* Unit test for {@link WavExtractor}.
|
||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
public final class WavExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testSample() throws Exception {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new WavExtractor();
|
||||
|
@ -15,20 +15,17 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.Timeline.Window;
|
||||
import com.google.android.exoplayer2.source.MediaSource.Listener;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ClippingMediaSource}.
|
||||
@ -38,15 +35,11 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
|
||||
private static final long TEST_PERIOD_DURATION_US = 1000000;
|
||||
private static final long TEST_CLIP_AMOUNT_US = 300000;
|
||||
|
||||
@Mock
|
||||
private MediaSource mockMediaSource;
|
||||
private Timeline clippedTimeline;
|
||||
private Window window;
|
||||
private Period period;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
TestUtil.setUpMockito(this);
|
||||
window = new Timeline.Window();
|
||||
period = new Timeline.Period();
|
||||
}
|
||||
@ -109,35 +102,30 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
|
||||
clippedTimeline.getPeriod(0, period).getDurationUs());
|
||||
}
|
||||
|
||||
public void testWindowAndPeriodIndices() {
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US));
|
||||
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
|
||||
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
|
||||
TimelineAsserts.assertWindowIds(clippedTimeline, 111);
|
||||
TimelineAsserts.assertPeriodCounts(clippedTimeline, 1);
|
||||
TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, 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.
|
||||
*/
|
||||
private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
|
||||
mockMediaSourceSourceWithTimeline(timeline);
|
||||
new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true,
|
||||
new Listener() {
|
||||
@Override
|
||||
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
|
||||
clippedTimeline = timeline;
|
||||
}
|
||||
});
|
||||
return clippedTimeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info.
|
||||
*/
|
||||
private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) {
|
||||
doAnswer(new Answer<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;
|
||||
private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
||||
return TestUtil.extractTimelineFromMediaSource(
|
||||
new ClippingMediaSource(mediaSource, startMs, endMs));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -157,39 +157,43 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
|
||||
assertEquals(2, output.size());
|
||||
Cue ttmlCue = output.get(0);
|
||||
assertEquals("lorem", ttmlCue.text.toString());
|
||||
assertEquals(10.f / 100.f, ttmlCue.position);
|
||||
assertEquals(10.f / 100.f, ttmlCue.line);
|
||||
assertEquals(20.f / 100.f, ttmlCue.size);
|
||||
assertEquals(10f / 100f, ttmlCue.position);
|
||||
assertEquals(10f / 100f, ttmlCue.line);
|
||||
assertEquals(20f / 100f, ttmlCue.size);
|
||||
|
||||
ttmlCue = output.get(1);
|
||||
assertEquals("amet", ttmlCue.text.toString());
|
||||
assertEquals(60.f / 100.f, ttmlCue.position);
|
||||
assertEquals(10.f / 100.f, ttmlCue.line);
|
||||
assertEquals(20.f / 100.f, ttmlCue.size);
|
||||
assertEquals(60f / 100f, ttmlCue.position);
|
||||
assertEquals(10f / 100f, ttmlCue.line);
|
||||
assertEquals(20f / 100f, ttmlCue.size);
|
||||
|
||||
output = subtitle.getCues(5000000);
|
||||
assertEquals(1, output.size());
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("ipsum", ttmlCue.text.toString());
|
||||
assertEquals(40.f / 100.f, ttmlCue.position);
|
||||
assertEquals(40.f / 100.f, ttmlCue.line);
|
||||
assertEquals(20.f / 100.f, ttmlCue.size);
|
||||
assertEquals(40f / 100f, ttmlCue.position);
|
||||
assertEquals(40f / 100f, ttmlCue.line);
|
||||
assertEquals(20f / 100f, ttmlCue.size);
|
||||
|
||||
output = subtitle.getCues(9000000);
|
||||
assertEquals(1, output.size());
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("dolor", ttmlCue.text.toString());
|
||||
assertEquals(10.f / 100.f, ttmlCue.position);
|
||||
assertEquals(80.f / 100.f, ttmlCue.line);
|
||||
assertEquals(Cue.DIMEN_UNSET, ttmlCue.position);
|
||||
assertEquals(Cue.DIMEN_UNSET, ttmlCue.line);
|
||||
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);
|
||||
assertEquals(1, output.size());
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("She first said this", ttmlCue.text.toString());
|
||||
assertEquals(45.f / 100.f, ttmlCue.position);
|
||||
assertEquals(45.f / 100.f, ttmlCue.line);
|
||||
assertEquals(35.f / 100.f, ttmlCue.size);
|
||||
assertEquals(45f / 100f, ttmlCue.position);
|
||||
assertEquals(45f / 100f, ttmlCue.line);
|
||||
assertEquals(35f / 100f, ttmlCue.size);
|
||||
output = subtitle.getCues(25000000);
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("She first said this\nThen this", ttmlCue.text.toString());
|
||||
@ -197,8 +201,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
|
||||
assertEquals(1, output.size());
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString());
|
||||
assertEquals(45.f / 100.f, ttmlCue.position);
|
||||
assertEquals(45.f / 100.f, ttmlCue.line);
|
||||
assertEquals(45f / 100f, ttmlCue.position);
|
||||
assertEquals(45f / 100f, ttmlCue.line);
|
||||
}
|
||||
|
||||
public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException {
|
||||
|
@ -125,25 +125,25 @@ public final class CssParserTest extends InstrumentationTestCase {
|
||||
String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n";
|
||||
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "lorem");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), ":");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "ipsum");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "{");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "dolor");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "}");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "#sit");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), ",");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "amet");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), ";");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "lorem");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), ":");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "ipsum");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "dolor");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "(");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), "(");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), ")");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), ")");
|
||||
assertEquals(CssParser.parseNextToken(input, builder), null);
|
||||
assertEquals("lorem", CssParser.parseNextToken(input, builder));
|
||||
assertEquals(":", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("ipsum", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("{", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("dolor", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("}", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("#sit", CssParser.parseNextToken(input, builder));
|
||||
assertEquals(",", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("amet", CssParser.parseNextToken(input, builder));
|
||||
assertEquals(";", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("lorem", CssParser.parseNextToken(input, builder));
|
||||
assertEquals(":", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("ipsum", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("dolor", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("(", CssParser.parseNextToken(input, builder));
|
||||
assertEquals("(", CssParser.parseNextToken(input, builder));
|
||||
assertEquals(")", CssParser.parseNextToken(input, builder));
|
||||
assertEquals(")", CssParser.parseNextToken(input, builder));
|
||||
assertEquals(null, CssParser.parseNextToken(input, builder));
|
||||
}
|
||||
|
||||
public void testStyleScoreSystem() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.test.MoreAsserts;
|
||||
@ -38,27 +40,29 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
private static final String KEY_1 = "key 1";
|
||||
private static final String KEY_2 = "key 2";
|
||||
|
||||
private File cacheDir;
|
||||
private SimpleCache simpleCache;
|
||||
private File tempFolder;
|
||||
private SimpleCache cache;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
Util.recursiveDelete(cacheDir);
|
||||
public void tearDown() throws Exception {
|
||||
Util.recursiveDelete(tempFolder);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
public void testMaxCacheFileSize() throws Exception {
|
||||
CacheDataSource cacheDataSource = createCacheDataSource(false, false);
|
||||
assertReadDataContentLength(cacheDataSource, false, false);
|
||||
File[] files = cacheDir.listFiles();
|
||||
for (File file : files) {
|
||||
if (!file.getName().equals(CachedContentIndex.FILE_NAME)) {
|
||||
assertTrue(file.length() <= MAX_CACHE_FILE_SIZE);
|
||||
for (String key : cache.getKeys()) {
|
||||
for (CacheSpan cacheSpan : cache.getCachedSpans(key)) {
|
||||
assertTrue(cacheSpan.length <= MAX_CACHE_FILE_SIZE);
|
||||
assertTrue(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,7 +108,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
// Read partial at EOS but don't cross it so length is unknown
|
||||
CacheDataSource cacheDataSource = createCacheDataSource(false, true);
|
||||
assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2);
|
||||
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
|
||||
assertEquals(C.LENGTH_UNSET, cache.getContentLength(KEY_1));
|
||||
|
||||
// Now do an unbounded request for whole data. This will cause a bounded request from upstream.
|
||||
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
|
||||
@ -124,13 +128,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
CacheDataSource cacheDataSource = createCacheDataSource(false, true,
|
||||
CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
|
||||
assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET);
|
||||
MoreAsserts.assertEmpty(simpleCache.getKeys());
|
||||
MoreAsserts.assertEmpty(cache.getKeys());
|
||||
}
|
||||
|
||||
public void testReadOnlyCache() throws Exception {
|
||||
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
|
||||
assertReadDataContentLength(cacheDataSource, false, false);
|
||||
assertEquals(0, cacheDir.list().length);
|
||||
assertCacheEmpty(cache);
|
||||
}
|
||||
|
||||
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
|
||||
@ -155,30 +159,30 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
assertReadData(cacheDataSource, unknownLength, 0, length);
|
||||
assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache "
|
||||
+ "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length,
|
||||
simpleCache.getContentLength(KEY_1));
|
||||
cache.getContentLength(KEY_1));
|
||||
}
|
||||
|
||||
private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position,
|
||||
int length) throws IOException {
|
||||
int actualLength = TEST_DATA.length - position;
|
||||
int testDataLength = TEST_DATA.length - position;
|
||||
if (length != C.LENGTH_UNSET) {
|
||||
actualLength = Math.min(actualLength, length);
|
||||
testDataLength = Math.min(testDataLength, length);
|
||||
}
|
||||
assertEquals(unknownLength ? length : actualLength,
|
||||
assertEquals(unknownLength ? length : testDataLength,
|
||||
cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1)));
|
||||
|
||||
byte[] buffer = new byte[100];
|
||||
int index = 0;
|
||||
int totalBytesRead = 0;
|
||||
while (true) {
|
||||
int read = cacheDataSource.read(buffer, index, buffer.length - index);
|
||||
int read = cacheDataSource.read(buffer, totalBytesRead, buffer.length - totalBytesRead);
|
||||
if (read == C.RESULT_END_OF_INPUT) {
|
||||
break;
|
||||
}
|
||||
index += read;
|
||||
totalBytesRead += read;
|
||||
}
|
||||
assertEquals(actualLength, index);
|
||||
MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + actualLength),
|
||||
Arrays.copyOf(buffer, index));
|
||||
assertEquals(testDataLength, totalBytesRead);
|
||||
MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + testDataLength),
|
||||
Arrays.copyOf(buffer, totalBytesRead));
|
||||
|
||||
cacheDataSource.close();
|
||||
}
|
||||
@ -192,7 +196,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
private CacheDataSource createCacheDataSource(boolean setReadException,
|
||||
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
|
||||
return createCacheDataSource(setReadException, simulateUnknownLength, flags,
|
||||
new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE));
|
||||
new CacheDataSink(cache, MAX_CACHE_FILE_SIZE));
|
||||
}
|
||||
|
||||
private CacheDataSource createCacheDataSource(boolean setReadException,
|
||||
@ -204,7 +208,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
if (setReadException) {
|
||||
fakeData.appendReadError(new IOException("Shouldn't read from upstream"));
|
||||
}
|
||||
return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink,
|
||||
return new CacheDataSource(cache, upstream, new FileDataSource(), cacheWriteDataSink,
|
||||
flags, null);
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user